Skip to content

Commit 300d141

Browse files
committed
reusable layout manager
1 parent 9bd02bf commit 300d141

File tree

11 files changed

+120
-105
lines changed

11 files changed

+120
-105
lines changed

src/reactpy_django/components.py

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,14 @@ def django_js(static_path: str, key: Key | None = None):
158158

159159
def python_to_pyscript(
160160
file_path: str,
161-
*extra_packages: str,
162161
extra_props: dict[str, Any] | None = None,
163162
initial: str | VdomDict | ComponentType = "",
164-
config: str | dict = "",
165163
root: str = "root",
166164
):
167165
return _python_to_pyscript(
168166
file_path,
169-
*extra_packages,
170167
extra_props=extra_props,
171168
initial=initial,
172-
config=config,
173169
root=root,
174170
)
175171

@@ -315,32 +311,26 @@ def _cached_static_contents(static_path: str) -> str:
315311
@component
316312
def _python_to_pyscript(
317313
file_path: str,
318-
*extra_packages: str,
319314
extra_props: dict[str, Any] | None = None,
320315
initial: str | VdomDict | ComponentType = "",
321-
config: str | dict = "",
322316
root: str = "root",
323317
):
324318
rendered, set_rendered = hooks.use_state(False)
325319
uuid = uuid4().hex.replace("-", "")
326320
initial = vdom_or_component_to_string(initial, uuid=uuid)
327321
executor = render_pyscript_template(file_path, uuid, root)
328-
new_config = extend_pyscript_config(config, extra_packages)
329322

330323
if not rendered:
331324
# FIXME: This is needed to properly re-render PyScript instances such as
332325
# when a component is re-rendered due to WebSocket disconnection.
333326
# There may be a better way to do this in the future.
334327
# While this solution allows re-creating PyScript components, it also
335328
# results in a browser memory leak. It currently unclear how to properly
336-
# clean up unused code (def user_workspace_UUID) from PyScript.
329+
# clean up unused code (user_workspace_UUID) from PyScript.
337330
set_rendered(True)
338331
return None
339332

340333
return html.div(
341334
html.div((extra_props or {}) | {"id": f"pyscript-{uuid}"}, initial),
342-
pyscript(
343-
{"async": "", "config": orjson.dumps(new_config).decode()},
344-
executor,
345-
),
335+
pyscript({"async": ""}, executor),
346336
)

src/reactpy_django/config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.core.cache import DEFAULT_CACHE_ALIAS
88
from django.db import DEFAULT_DB_ALIAS
99
from django.views import View
10-
from reactpy.config import REACTPY_DEBUG_MODE
10+
from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE
1111
from reactpy.core.types import ComponentConstructor
1212

1313
from reactpy_django.types import (
@@ -17,7 +17,8 @@
1717
from reactpy_django.utils import import_dotted_path
1818

1919
# Non-configurable values
20-
REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
20+
_REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG"))
21+
REACTPY_DEBUG_MODE = _REACTPY_DEBUG_MODE.current
2122
REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {}
2223
REACTPY_FAILED_COMPONENTS: set[str] = set()
2324
REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {}

src/reactpy_django/pyscript/component_template.py

Lines changed: 4 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
44
Our template tag performs string substitutions to turn this file into valid PyScript."""
55

6-
import asyncio
6+
from typing import TYPE_CHECKING
77

8-
import js
9-
from jsonpointer import set_pointer
10-
from pyodide.ffi.wrappers import add_event_listener
11-
from reactpy.core.layout import Layout
8+
if TYPE_CHECKING:
9+
from reactpy_django.pyscript.layout_manager import ReactPyLayoutManager
1210

1311

1412
# User component is inserted below by regex replacement
@@ -21,74 +19,6 @@ def root(): ...
2119
return root()
2220

2321

24-
# ReactPy layout rendering starts here
25-
class LayoutManagerUUID:
26-
"""Encapsulate an entire layout manager with a completely unique class to prevent
27-
rendering bugs caused by the PyScript global interpreter."""
28-
29-
@staticmethod
30-
def apply_update(update, root_model):
31-
if update["path"]:
32-
set_pointer(root_model, update["path"], update["model"])
33-
else:
34-
root_model.update(update["model"])
35-
36-
def render(self, layout, model):
37-
container = js.document.getElementById("pyscript-UUID")
38-
container.innerHTML = ""
39-
self.build_element_tree(layout, container, model)
40-
41-
def build_element_tree(self, layout, parent, model):
42-
if isinstance(model, str):
43-
parent.appendChild(js.document.createTextNode(model))
44-
elif isinstance(model, dict):
45-
if not model["tagName"]:
46-
for child in model.get("children", []):
47-
self.build_element_tree(layout, parent, child)
48-
return
49-
tag = model["tagName"]
50-
attributes = model.get("attributes", {})
51-
children = model.get("children", [])
52-
element = js.document.createElement(tag)
53-
for key, value in attributes.items():
54-
if key == "style":
55-
for style_key, style_value in value.items():
56-
setattr(element.style, style_key, style_value)
57-
else:
58-
element.setAttribute(key, value)
59-
for event_name, event_handler_model in model.get(
60-
"eventHandlers", {}
61-
).items():
62-
self.create_event_handler(
63-
layout, element, event_name, event_handler_model
64-
)
65-
for child in children:
66-
self.build_element_tree(layout, element, child)
67-
parent.appendChild(element)
68-
else:
69-
raise ValueError(f"Unknown model type: {type(model)}")
70-
71-
@staticmethod
72-
def create_event_handler(layout, element, event_name, event_handler_model):
73-
target = event_handler_model["target"]
74-
75-
def event_handler(*args):
76-
asyncio.create_task(
77-
layout.deliver({"type": "layout-event", "target": target, "data": args})
78-
)
79-
80-
event_name = event_name.lstrip("on_").lower().replace("_", "")
81-
add_event_listener(element, event_name, event_handler)
82-
83-
async def run(self):
84-
root_model = {}
85-
async with Layout(user_workspace_UUID()) as layout:
86-
while True:
87-
update = await layout.render()
88-
self.apply_update(update, root_model)
89-
self.render(layout, root_model)
90-
91-
9222
# PyScript allows top-level await, which allows us to not throw errors on components
9323
# that terminate early (such as hook-less components)
94-
await LayoutManagerUUID().run() # noqa: F704
24+
await ReactPyLayoutManager("UUID").run(user_workspace_UUID) # noqa: F704
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import asyncio
2+
from typing import Coroutine
3+
4+
import js
5+
from jsonpointer import set_pointer
6+
from pyodide.ffi.wrappers import add_event_listener
7+
from reactpy.core.layout import Layout
8+
9+
10+
class ReactPyLayoutManager:
11+
"""Encapsulate the entire layout manager with a class to prevent overlapping
12+
variable names between user code."""
13+
14+
def __init__(self, uuid):
15+
self.uuid = uuid
16+
17+
@staticmethod
18+
def apply_update(update, root_model):
19+
if update["path"]:
20+
set_pointer(root_model, update["path"], update["model"])
21+
else:
22+
root_model.update(update["model"])
23+
24+
def render(self, layout, model):
25+
container = js.document.getElementById(f"pyscript-{self.uuid}")
26+
container.innerHTML = ""
27+
self.build_element_tree(layout, container, model)
28+
29+
def build_element_tree(self, layout, parent, model):
30+
if isinstance(model, str):
31+
parent.appendChild(js.document.createTextNode(model))
32+
elif isinstance(model, dict):
33+
if not model["tagName"]:
34+
for child in model.get("children", []):
35+
self.build_element_tree(layout, parent, child)
36+
return
37+
tag = model["tagName"]
38+
attributes = model.get("attributes", {})
39+
children = model.get("children", [])
40+
element = js.document.createElement(tag)
41+
for key, value in attributes.items():
42+
if key == "style":
43+
for style_key, style_value in value.items():
44+
setattr(element.style, style_key, style_value)
45+
else:
46+
element.setAttribute(key, value)
47+
for event_name, event_handler_model in model.get(
48+
"eventHandlers", {}
49+
).items():
50+
self.create_event_handler(
51+
layout, element, event_name, event_handler_model
52+
)
53+
for child in children:
54+
self.build_element_tree(layout, element, child)
55+
parent.appendChild(element)
56+
else:
57+
raise ValueError(f"Unknown model type: {type(model)}")
58+
59+
@staticmethod
60+
def create_event_handler(layout, element, event_name, event_handler_model):
61+
target = event_handler_model["target"]
62+
63+
def event_handler(*args):
64+
asyncio.create_task(
65+
layout.deliver({"type": "layout-event", "target": target, "data": args})
66+
)
67+
68+
event_name = event_name.lstrip("on_").lower().replace("_", "")
69+
add_event_listener(element, event_name, event_handler)
70+
71+
async def run(self, workspace_function: Coroutine):
72+
root_model = {}
73+
async with Layout(workspace_function()) as layout:
74+
while True:
75+
update = await layout.render()
76+
self.apply_update(update, root_model)
77+
self.render(layout, root_model)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
py-script {
2+
display: none;
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.py-error {
2+
display: none;
3+
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{% if reactpy_class %}<div id="pyscript-{{reactpy_uuid}}" class="{{reactpy_class}}">{{reactpy_initial_html}}</div>
1+
{% if reactpy_class %}<div id="pyscript-{{pyscript_uuid}}" class="{{pyscript_class}}">{{pyscript_initial_html}}</div>
22
{% endif %}
3-
{% if not reactpy_class %}<div id="pyscript-{{reactpy_uuid}}">{{reactpy_initial_html}}</div>{% endif %}
4-
<py-script async config='{{ reactpy_config }}'>{{ reactpy_executor }}</py-script>
3+
{% if not reactpy_class %}<div id="pyscript-{{pyscript_uuid}}">{{pyscript_initial_html}}</div>{% endif %}
4+
<py-script async>{{pyscript_executor}}</py-script>
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{% load static %}
22
<link rel="stylesheet" href="{% static 'reactpy_django/pyscript/core.css' %}" />
33
<link rel="stylesheet" href="{% static 'reactpy_django/pyscript-custom.css' %}" />
4+
{% if not reactpy_debug_mode %}
5+
<link rel="stylesheet" href="{% static 'reactpy_django/pyscript-hide-debug.css' %}" />
6+
{% endif %}
47
<script type="module" async src="{% static 'reactpy_django/pyscript/core.js' %}"></script>
8+
<py-script async config='{{pyscript_config}}'>{{pyscript_layout_manager}}</py-script>

src/reactpy_django/templatetags/reactpy.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from uuid import uuid4
55

66
import dill as pickle
7-
import orjson
87
from django import template
98
from django.http import HttpRequest
109
from django.urls import NoReverseMatch, reverse
@@ -21,6 +20,7 @@
2120
)
2221
from reactpy_django.types import ComponentParams
2322
from reactpy_django.utils import (
23+
PYSCRIPT_LAYOUT_MANAGER,
2424
extend_pyscript_config,
2525
prerender_component,
2626
render_pyscript_template,
@@ -211,25 +211,29 @@ def validate_host(host: str):
211211
def pyscript_component(
212212
context: template.RequestContext,
213213
file_path: str,
214-
*extra_packages: str,
215214
initial: str | VdomDict | ComponentType = "",
216-
config: str | dict = "",
217215
root: str = "root",
218216
):
219217
uuid = uuid4().hex
220218
request: HttpRequest | None = context.get("request")
221219
initial = vdom_or_component_to_string(initial, request=request, uuid=uuid)
222220
executor = render_pyscript_template(file_path, uuid, root)
223-
new_config = extend_pyscript_config(config, extra_packages)
224221

225222
return {
226-
"reactpy_executor": executor,
227-
"reactpy_uuid": uuid,
228-
"reactpy_initial_html": initial,
229-
"reactpy_config": orjson.dumps(new_config).decode(),
223+
"pyscript_executor": executor,
224+
"pyscript_uuid": uuid,
225+
"pyscript_initial_html": initial,
230226
}
231227

232228

233-
@register.inclusion_tag("reactpy/pyscript_static_files.html")
234-
def pyscript_static_files():
235-
return {}
229+
@register.inclusion_tag("reactpy/pyscript_setup.html")
230+
def pyscript_setup(
231+
*extra_packages: str,
232+
config: str | dict = "",
233+
):
234+
print(reactpy_config.REACTPY_DEBUG_MODE)
235+
return {
236+
"pyscript_config": extend_pyscript_config(config, extra_packages),
237+
"pyscript_layout_manager": PYSCRIPT_LAYOUT_MANAGER,
238+
"reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE,
239+
}

src/reactpy_django/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
PYSCRIPT_COMPONENT_TEMPLATE = (
5858
Path(__file__).parent / "pyscript" / "component_template.py"
5959
).read_text(encoding="utf-8")
60+
PYSCRIPT_LAYOUT_MANAGER = (
61+
Path(__file__).parent / "pyscript" / "layout_manager.py"
62+
).read_text(encoding="utf-8")
6063
PYSCRIPT_DEFAULT_CONFIG = {
6164
"packages": [
6265
f"reactpy=={reactpy.__version__}",
@@ -468,12 +471,12 @@ def render_pyscript_template(file_path: str, uuid: str, root: str):
468471
return executor
469472

470473

471-
def extend_pyscript_config(config: dict | str, extra_packages: Sequence) -> dict:
474+
def extend_pyscript_config(config: dict | str, extra_packages: Sequence) -> str:
472475
"""Extends the default PyScript configuration with user configuration."""
473476
pyscript_config = deepcopy(PYSCRIPT_DEFAULT_CONFIG)
474477
pyscript_config["packages"].extend(extra_packages)
475478
if config and isinstance(config, str):
476479
pyscript_config.update(orjson.loads(config))
477480
elif isinstance(config, dict):
478481
pyscript_config.update(config)
479-
return pyscript_config
482+
return orjson.dumps(pyscript_config).decode("utf-8")

tests/test_app/templates/pyscript.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
99
<link rel="shortcut icon" type="image/png" href="{% static 'favicon.ico' %}" />
1010
<title>ReactPy</title>
11-
{% pyscript_static_files %}
11+
{% pyscript_setup %}
1212
</head>
1313

1414
<body>

0 commit comments

Comments
 (0)