1
1
from __future__ import annotations
2
2
3
+ import textwrap
3
4
from logging import getLogger
5
+ from pathlib import Path
4
6
from uuid import uuid4
5
7
6
8
import dill as pickle
9
+ import jsonpointer
10
+ import orjson
11
+ import reactpy
7
12
from django import template
8
13
from django .http import HttpRequest
9
14
from django .urls import NoReverseMatch , reverse
10
15
from reactpy .backend .hooks import ConnectionContext
11
16
from reactpy .backend .types import Connection , Location
12
- from reactpy .core .types import ComponentConstructor
17
+ from reactpy .core .types import ComponentConstructor , ComponentType , VdomDict
13
18
from reactpy .utils import vdom_to_html
14
19
15
- from reactpy_django import config , models
20
+ from reactpy_django import config as reactpy_config
21
+ from reactpy_django import models , pyscript
16
22
from reactpy_django .exceptions import (
17
23
ComponentCarrierError ,
18
24
ComponentDoesNotExistError ,
30
36
register = template .Library ()
31
37
_logger = getLogger (__name__ )
32
38
39
+ pyscript_template = (Path (pyscript .__file__ ).parent / "executor.py" ).read_text (
40
+ encoding = "utf-8"
41
+ )
42
+
33
43
34
44
@register .inclusion_tag ("reactpy/component.html" , takes_context = True )
35
45
def component (
36
46
context : template .RequestContext ,
37
47
dotted_path : str ,
38
48
* args ,
39
49
host : str | None = None ,
40
- prerender : str = str (config .REACTPY_PRERENDER ),
50
+ prerender : str = str (reactpy_config .REACTPY_PRERENDER ),
41
51
offline : str = "" ,
42
52
** kwargs ,
43
53
):
@@ -73,7 +83,11 @@ def component(
73
83
perceived_host = (request .get_host () if request else "" ).strip ("/" )
74
84
host = (
75
85
host
76
- or (next (config .REACTPY_DEFAULT_HOSTS ) if config .REACTPY_DEFAULT_HOSTS else "" )
86
+ or (
87
+ next (reactpy_config .REACTPY_DEFAULT_HOSTS )
88
+ if reactpy_config .REACTPY_DEFAULT_HOSTS
89
+ else ""
90
+ )
77
91
).strip ("/" )
78
92
is_local = not host or host .startswith (perceived_host )
79
93
uuid = str (uuid4 ())
@@ -84,22 +98,22 @@ def component(
84
98
_offline_html = ""
85
99
86
100
# Validate the host
87
- if host and config .REACTPY_DEBUG_MODE :
101
+ if host and reactpy_config .REACTPY_DEBUG_MODE :
88
102
try :
89
103
validate_host (host )
90
104
except InvalidHostError as e :
91
105
return failure_context (dotted_path , e )
92
106
93
107
# Fetch the component
94
108
if is_local :
95
- user_component = config .REACTPY_REGISTERED_COMPONENTS .get (dotted_path )
109
+ user_component = reactpy_config .REACTPY_REGISTERED_COMPONENTS .get (dotted_path )
96
110
if not user_component :
97
111
msg = f"Component '{ dotted_path } ' is not registered as a root component. "
98
112
_logger .error (msg )
99
113
return failure_context (dotted_path , ComponentDoesNotExistError (msg ))
100
114
101
115
# Validate the component args & kwargs
102
- if is_local and config .REACTPY_DEBUG_MODE :
116
+ if is_local and reactpy_config .REACTPY_DEBUG_MODE :
103
117
try :
104
118
validate_component_args (user_component , * args , ** kwargs )
105
119
except ComponentParamError as e :
@@ -140,7 +154,7 @@ def component(
140
154
141
155
# Fetch the offline component's HTML, if requested
142
156
if offline :
143
- offline_component = config .REACTPY_REGISTERED_COMPONENTS .get (offline )
157
+ offline_component = reactpy_config .REACTPY_REGISTERED_COMPONENTS .get (offline )
144
158
if not offline_component :
145
159
msg = f"Cannot render offline component '{ offline } '. It is not registered as a component."
146
160
_logger .error (msg )
@@ -159,13 +173,13 @@ def component(
159
173
"reactpy_class" : class_ ,
160
174
"reactpy_uuid" : uuid ,
161
175
"reactpy_host" : host or perceived_host ,
162
- "reactpy_url_prefix" : config .REACTPY_URL_PREFIX ,
176
+ "reactpy_url_prefix" : reactpy_config .REACTPY_URL_PREFIX ,
163
177
"reactpy_component_path" : f"{ dotted_path } /{ uuid } /{ int (has_args )} /" ,
164
178
"reactpy_resolved_web_modules_path" : RESOLVED_WEB_MODULES_PATH ,
165
- "reactpy_reconnect_interval" : config .REACTPY_RECONNECT_INTERVAL ,
166
- "reactpy_reconnect_max_interval" : config .REACTPY_RECONNECT_MAX_INTERVAL ,
167
- "reactpy_reconnect_backoff_multiplier" : config .REACTPY_RECONNECT_BACKOFF_MULTIPLIER ,
168
- "reactpy_reconnect_max_retries" : config .REACTPY_RECONNECT_MAX_RETRIES ,
179
+ "reactpy_reconnect_interval" : reactpy_config .REACTPY_RECONNECT_INTERVAL ,
180
+ "reactpy_reconnect_max_interval" : reactpy_config .REACTPY_RECONNECT_MAX_INTERVAL ,
181
+ "reactpy_reconnect_backoff_multiplier" : reactpy_config .REACTPY_RECONNECT_BACKOFF_MULTIPLIER ,
182
+ "reactpy_reconnect_max_retries" : reactpy_config .REACTPY_RECONNECT_MAX_RETRIES ,
169
183
"reactpy_prerender_html" : _prerender_html ,
170
184
"reactpy_offline_html" : _offline_html ,
171
185
}
@@ -174,7 +188,7 @@ def component(
174
188
def failure_context (dotted_path : str , error : Exception ):
175
189
return {
176
190
"reactpy_failure" : True ,
177
- "reactpy_debug_mode" : config .REACTPY_DEBUG_MODE ,
191
+ "reactpy_debug_mode" : reactpy_config .REACTPY_DEBUG_MODE ,
178
192
"reactpy_dotted_path" : dotted_path ,
179
193
"reactpy_error" : type (error ).__name__ ,
180
194
}
@@ -219,3 +233,57 @@ def prerender_component(
219
233
vdom_tree = layout .render ()["model" ]
220
234
221
235
return vdom_to_html (vdom_tree )
236
+
237
+
238
+ # TODO: Add micropython support
239
+ @register .inclusion_tag ("reactpy/pyscript_component.html" , takes_context = True )
240
+ def pyscript_component (
241
+ context : template .RequestContext ,
242
+ file_path : str ,
243
+ * extra_packages : str ,
244
+ initial : str | VdomDict | ComponentType = "" ,
245
+ config : str | dict = "" ,
246
+ root : str = "root" ,
247
+ ):
248
+ uuid = uuid4 ().hex
249
+ request : HttpRequest | None = context .get ("request" )
250
+ pyscript_config = {
251
+ "packages" : [
252
+ f"reactpy=={ reactpy .__version__ } " ,
253
+ f"jsonpointer=={ jsonpointer .__version__ } " ,
254
+ "ssl" ,
255
+ * extra_packages ,
256
+ ]
257
+ }
258
+ if config and isinstance (config , str ):
259
+ pyscript_config .update (orjson .loads (config ))
260
+ elif isinstance (config , dict ):
261
+ pyscript_config .update (config )
262
+
263
+ # Convert the user provided initial HTML to a string, if needed
264
+ if isinstance (initial , dict ):
265
+ initial = vdom_to_html (initial )
266
+ elif hasattr (initial , "render" ):
267
+ if not request :
268
+ raise ValueError (
269
+ "Cannot render a component without a HTTP request. Are you missing the request "
270
+ "context processor in settings.py:TEMPLATES['OPTIONS']['context_processors']?"
271
+ )
272
+ initial = prerender_component (initial , [], {}, uuid , request )
273
+
274
+ # Create a valid PyScript executor by replacing the template values
275
+ executor = pyscript_template .replace ("UUID" , uuid )
276
+ executor = executor .replace ("return root()" , f"return { root } ()" )
277
+
278
+ # Insert the user code into the template
279
+ user_code = Path (file_path ).read_text (encoding = "utf-8" )
280
+ user_code = user_code .strip ().replace ("\t " , " " ) # Normalize the code text
281
+ user_code = textwrap .indent (user_code , " " ) # Add indentation to match template
282
+ executor = executor .replace (" def root(): ..." , user_code )
283
+
284
+ return {
285
+ "reactpy_executor" : executor ,
286
+ "reactpy_uuid" : uuid ,
287
+ "reactpy_initial_html" : initial ,
288
+ "reactpy_config" : orjson .dumps (pyscript_config ).decode (),
289
+ }
0 commit comments