Skip to content

Commit bdf1960

Browse files
committed
first attempt at pyscript components with server-side parent components
1 parent d529fed commit bdf1960

File tree

11 files changed

+233
-103
lines changed

11 files changed

+233
-103
lines changed

src/reactpy_django/components.py

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,29 @@
44
import os
55
from typing import Any, Callable, Sequence, Union, cast, overload
66
from urllib.parse import urlencode
7+
from uuid import uuid4
78
from warnings import warn
89

10+
import orjson
911
from django.contrib.staticfiles.finders import find
1012
from django.core.cache import caches
1113
from django.http import HttpRequest
14+
from django.templatetags.static import static
1215
from django.urls import reverse
1316
from django.views import View
1417
from reactpy import component, hooks, html, utils
15-
from reactpy.types import Key, VdomDict
18+
from reactpy.types import ComponentType, Key, VdomDict
1619

1720
from reactpy_django.exceptions import ViewNotRegisteredError
18-
from reactpy_django.utils import generate_obj_name, import_module, render_view
21+
from reactpy_django.utils import (
22+
PYSCRIPT_TAG,
23+
extend_pyscript_config,
24+
generate_obj_name,
25+
import_module,
26+
render_pyscript_template,
27+
render_view,
28+
vdom_or_component_to_string,
29+
)
1930

2031

2132
# Type hints for:
@@ -27,8 +38,7 @@ def view_to_component(
2738
compatibility: bool = False,
2839
transforms: Sequence[Callable[[VdomDict], Any]] = (),
2940
strict_parsing: bool = True,
30-
) -> Any:
31-
...
41+
) -> Any: ...
3242

3343

3444
# Type hints for:
@@ -39,8 +49,7 @@ def view_to_component(
3949
compatibility: bool = False,
4050
transforms: Sequence[Callable[[VdomDict], Any]] = (),
4151
strict_parsing: bool = True,
42-
) -> Callable[[Callable], Any]:
43-
...
52+
) -> Callable[[Callable], Any]: ...
4453

4554

4655
def view_to_component(
@@ -148,6 +157,24 @@ def django_js(static_path: str, key: Key | None = None):
148157
return _django_js(static_path=static_path, key=key)
149158

150159

160+
def python_to_pyscript(
161+
file_path: str,
162+
*extra_packages: str,
163+
extra_props: dict[str, Any] | None = None,
164+
initial: str | VdomDict | ComponentType = "",
165+
config: str | dict = "",
166+
root: str = "root",
167+
):
168+
return _python_to_pyscript(
169+
file_path,
170+
*extra_packages,
171+
extra_props=extra_props,
172+
initial=initial,
173+
config=config,
174+
root=root,
175+
)
176+
177+
151178
@component
152179
def _view_to_component(
153180
view: Callable | View | str,
@@ -284,3 +311,44 @@ def _cached_static_contents(static_path: str) -> str:
284311
)
285312

286313
return file_contents
314+
315+
316+
@component
317+
def _python_to_pyscript(
318+
file_path: str,
319+
*extra_packages: str,
320+
extra_props: dict[str, Any] | None = None,
321+
initial: str | VdomDict | ComponentType = "",
322+
config: str | dict = "",
323+
root: str = "root",
324+
):
325+
uuid = uuid4().hex.replace("-", "")
326+
initial = vdom_or_component_to_string(initial, uuid=uuid)
327+
executor = render_pyscript_template(file_path, uuid, root)
328+
new_config = extend_pyscript_config(config, extra_packages)
329+
330+
return html.div(
331+
html.link(
332+
{"rel": "stylesheet", "href": static("reactpy_django/pyscript/core.css")}
333+
),
334+
html.script(
335+
{
336+
"type": "module",
337+
"src": static(
338+
"reactpy_django/pyscript/core.js",
339+
),
340+
}
341+
),
342+
html.div((extra_props or {}) | {"id": f"pyscript-{uuid}"}, initial),
343+
PYSCRIPT_TAG(
344+
{
345+
"async": "",
346+
"config": orjson.dumps(new_config).decode(),
347+
"id": f"script-{uuid}",
348+
},
349+
executor,
350+
),
351+
html.script(
352+
f"if (document.querySelector('#pyscript-{uuid}') && document.querySelector('#pyscript-{uuid}').childElementCount != 0 && document.querySelector('#script-{uuid}')) document.querySelector('#script-{uuid}').remove();"
353+
),
354+
)

src/reactpy_django/pyscript/__init__.py

Whitespace-only changes.

src/reactpy_django/pyscript/executor.py renamed to src/reactpy_django/pyscript_template.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,18 @@ def apply_update(update, root_model):
3333
else:
3434
root_model.update(update["model"])
3535

36-
def render_model(self, layout, model):
37-
container = js.document.getElementById("UUID")
36+
def render(self, layout, model):
37+
container = js.document.getElementById("pyscript-UUID")
3838
container.innerHTML = ""
39-
self._render_model(layout, container, model)
39+
self.build_element_tree(layout, container, model)
4040

41-
def _render_model(self, layout, parent, model):
41+
def build_element_tree(self, layout, parent, model):
4242
if isinstance(model, str):
4343
parent.appendChild(js.document.createTextNode(model))
4444
elif isinstance(model, dict):
4545
if not model["tagName"]:
4646
for child in model.get("children", []):
47-
self._render_model(layout, parent, child)
47+
self.build_element_tree(layout, parent, child)
4848
return
4949
tag = model["tagName"]
5050
attributes = model.get("attributes", {})
@@ -59,28 +59,22 @@ def _render_model(self, layout, parent, model):
5959
for event_name, event_handler_model in model.get(
6060
"eventHandlers", {}
6161
).items():
62-
self._create_event_handler(
62+
self.create_event_handler(
6363
layout, element, event_name, event_handler_model
6464
)
6565
for child in children:
66-
self._render_model(layout, element, child)
66+
self.build_element_tree(layout, element, child)
6767
parent.appendChild(element)
6868
else:
6969
raise ValueError(f"Unknown model type: {type(model)}")
7070

7171
@staticmethod
72-
def _create_event_handler(layout, element, event_name, event_handler_model):
72+
def create_event_handler(layout, element, event_name, event_handler_model):
7373
target = event_handler_model["target"]
7474

7575
def event_handler(*args):
7676
asyncio.create_task(
77-
layout.deliver(
78-
{
79-
"type": "layout-event",
80-
"target": target,
81-
"data": args,
82-
}
83-
)
77+
layout.deliver({"type": "layout-event", "target": target, "data": args})
8478
)
8579

8680
event_name = event_name.lstrip("on_").lower().replace("_", "")
@@ -92,7 +86,7 @@ async def run(self):
9286
while True:
9387
update = await layout.render()
9488
self.apply_update(update, root_model)
95-
self.render_model(layout, root_model)
89+
self.render(layout, root_model)
9690

9791

9892
asyncio.create_task(LayoutManagerUUID().run())
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{% load static %}
22
<link rel="stylesheet" href="{% static 'reactpy_django/pyscript/core.css' %}" />
33
<script type="module" src="{% static 'reactpy_django/pyscript/core.js' %}"></script>
4-
{% if reactpy_class %}<div id="{{reactpy_uuid}}" class="{{reactpy_class}}">{{reactpy_initial_html}}</div>{% endif %}
5-
{% if not reactpy_class %}<div id="{{reactpy_uuid}}">{{reactpy_initial_html}}</div>{% endif %}
4+
{% if reactpy_class %}<div id="pyscript-{{reactpy_uuid}}" class="{{reactpy_class}}">{{reactpy_initial_html}}</div>
5+
{% endif %}
6+
{% if not reactpy_class %}<div id="pyscript-{{reactpy_uuid}}">{{reactpy_initial_html}}</div>{% endif %}
67
<py-script async config='{{ reactpy_config }}'>{{ reactpy_executor }}</py-script>

src/reactpy_django/templatetags/reactpy.py

Lines changed: 13 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,17 @@
11
from __future__ import annotations
22

3-
import textwrap
43
from logging import getLogger
5-
from pathlib import Path
64
from uuid import uuid4
75

86
import dill as pickle
9-
import jsonpointer
107
import orjson
11-
import reactpy
128
from django import template
139
from django.http import HttpRequest
1410
from django.urls import NoReverseMatch, reverse
15-
from reactpy.backend.hooks import ConnectionContext
16-
from reactpy.backend.types import Connection, Location
1711
from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict
18-
from reactpy.utils import vdom_to_html
1912

2013
from reactpy_django import config as reactpy_config
21-
from reactpy_django import models, pyscript
14+
from reactpy_django import models
2215
from reactpy_django.exceptions import (
2316
ComponentCarrierError,
2417
ComponentDoesNotExistError,
@@ -27,7 +20,14 @@
2720
OfflineComponentMissing,
2821
)
2922
from reactpy_django.types import ComponentParams
30-
from reactpy_django.utils import SyncLayout, strtobool, validate_component_args
23+
from reactpy_django.utils import (
24+
extend_pyscript_config,
25+
prerender_component,
26+
render_pyscript_template,
27+
strtobool,
28+
validate_component_args,
29+
vdom_or_component_to_string,
30+
)
3131

3232
try:
3333
RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/")
@@ -36,10 +36,6 @@
3636
register = template.Library()
3737
_logger = getLogger(__name__)
3838

39-
pyscript_template = (Path(pyscript.__file__).parent / "executor.py").read_text(
40-
encoding="utf-8"
41-
)
42-
4339

4440
@register.inclusion_tag("reactpy/component.html", takes_context=True)
4541
def component(
@@ -211,31 +207,6 @@ def validate_host(host: str):
211207
raise InvalidHostError(msg)
212208

213209

214-
def prerender_component(
215-
user_component: ComponentConstructor, args, kwargs, uuid, request: HttpRequest
216-
):
217-
search = request.GET.urlencode()
218-
scope = getattr(request, "scope", {})
219-
scope["reactpy"] = {"id": str(uuid)}
220-
221-
with SyncLayout(
222-
ConnectionContext(
223-
user_component(*args, **kwargs),
224-
value=Connection(
225-
scope=scope,
226-
location=Location(
227-
pathname=request.path, search=f"?{search}" if search else ""
228-
),
229-
carrier=request,
230-
),
231-
)
232-
) as layout:
233-
vdom_tree = layout.render()["model"]
234-
235-
return vdom_to_html(vdom_tree)
236-
237-
238-
# TODO: Add micropython support
239210
@register.inclusion_tag("reactpy/pyscript_component.html", takes_context=True)
240211
def pyscript_component(
241212
context: template.RequestContext,
@@ -247,43 +218,13 @@ def pyscript_component(
247218
):
248219
uuid = uuid4().hex
249220
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)
221+
initial = vdom_or_component_to_string(initial, request=request, uuid=uuid)
222+
executor = render_pyscript_template(file_path, uuid, root)
223+
new_config = extend_pyscript_config(config, extra_packages)
283224

284225
return {
285226
"reactpy_executor": executor,
286227
"reactpy_uuid": uuid,
287228
"reactpy_initial_html": initial,
288-
"reactpy_config": orjson.dumps(pyscript_config).decode(),
229+
"reactpy_config": orjson.dumps(new_config).decode(),
289230
}

0 commit comments

Comments
 (0)