Skip to content

Commit 5d2877b

Browse files
committed
Add configuration option to standlone reactpy to auto-load pyscript
1 parent 5f680b8 commit 5d2877b

File tree

4 files changed

+44
-23
lines changed

4 files changed

+44
-23
lines changed

src/reactpy/executors/asgi/pyscript.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import hashlib
44
import re
5+
from collections.abc import Sequence
56
from dataclasses import dataclass
67
from datetime import datetime, timezone
78
from email.utils import formatdate
@@ -23,7 +24,7 @@ class ReactPyPyscript(ReactPy):
2324
def __init__(
2425
self,
2526
*file_paths: str | Path,
26-
extra_py: tuple[str, ...] = (),
27+
extra_py: Sequence[str] = (),
2728
extra_js: dict[str, str] | None = None,
2829
pyscript_config: dict[str, Any] | None = None,
2930
root_name: str = "root",

src/reactpy/executors/asgi/standalone.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@
2121
AsgiWebsocketApp,
2222
)
2323
from reactpy.executors.utils import server_side_component_html, vdom_head_to_html
24-
from reactpy.types import ReactPyConfig, RootComponentConstructor, VdomDict
25-
from reactpy.utils import import_dotted_path
24+
from reactpy.pyscript.utils import pyscript_setup_html
25+
from reactpy.types import (
26+
PyScriptOptions,
27+
ReactPyConfig,
28+
RootComponentConstructor,
29+
VdomDict,
30+
)
31+
from reactpy.utils import html_to_vdom, import_dotted_path
2632

2733
_logger = getLogger(__name__)
2834

@@ -37,6 +43,8 @@ def __init__(
3743
http_headers: dict[str, str] | None = None,
3844
html_head: VdomDict | None = None,
3945
html_lang: str = "en",
46+
pyscript_setup: bool = False,
47+
pyscript_options: PyScriptOptions | None = None,
4048
**settings: Unpack[ReactPyConfig],
4149
) -> None:
4250
"""ReactPy's standalone ASGI application.
@@ -46,6 +54,8 @@ def __init__(
4654
http_headers: Additional headers to include in the HTTP response for the base HTML document.
4755
html_head: Additional head elements to include in the HTML response.
4856
html_lang: The language of the HTML document.
57+
pyscript_setup: Whether to automatically load PyScript within your HTML head.
58+
pyscript_options: Options to configure PyScript behavior.
4959
settings: Global ReactPy configuration settings that affect behavior and performance.
5060
"""
5161
super().__init__(app=ReactPyApp(self), root_components=[], **settings)
@@ -55,6 +65,16 @@ def __init__(
5565
self.html_head = html_head or html.head()
5666
self.html_lang = html_lang
5767

68+
if pyscript_setup:
69+
self.html_head.setdefault("children", [])
70+
pyscript_options = pyscript_options or {}
71+
extra_py = pyscript_options.get("extra_py", [])
72+
extra_js = pyscript_options.get("extra_js", {})
73+
config = pyscript_options.get("config", {})
74+
self.html_head["children"].append( # type: ignore
75+
html_to_vdom(pyscript_setup_html(extra_py, extra_js, config))
76+
)
77+
5878
def match_dispatch_path(self, scope: asgi_types.WebSocketScope) -> bool:
5979
"""Method override to remove `dotted_path` from the dispatcher URL."""
6080
return str(scope["path"]) == self.dispatcher_path

src/reactpy/pyscript/components.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,44 @@
11
from __future__ import annotations
22

3+
from pathlib import Path
34
from typing import TYPE_CHECKING
4-
from uuid import uuid4
55

6-
from reactpy import component, hooks, html
7-
from reactpy.pyscript.utils import render_pyscript_executor
6+
from reactpy import component, hooks
7+
from reactpy.pyscript.utils import pyscript_component_html
88
from reactpy.types import ComponentType
9-
from reactpy.utils import vdom_to_html
9+
from reactpy.utils import html_to_vdom
1010

1111
if TYPE_CHECKING:
1212
from reactpy.types import VdomDict
1313

1414

1515
@component
1616
def _pyscript_component(
17-
*file_paths: str,
17+
*file_paths: str | Path,
1818
initial: str | VdomDict = "",
1919
root: str = "root",
2020
) -> None | VdomDict:
2121
if not file_paths:
2222
raise ValueError("At least one file path must be provided.")
2323

2424
rendered, set_rendered = hooks.use_state(False)
25-
uuid = hooks.use_ref(uuid4().hex.replace("-", "")).current
26-
initial = initial if isinstance(initial, str) else vdom_to_html(initial)
27-
executor = render_pyscript_executor(file_paths=file_paths, uuid=uuid, root=root)
25+
initial = html_to_vdom(initial) if isinstance(initial, str) else initial
2826

2927
if not rendered:
3028
# FIXME: This is needed to properly re-render PyScript during a WebSocket
3129
# disconnection / reconnection. There may be a better way to do this in the future.
3230
set_rendered(True)
3331
return None
3432

35-
return html.fragment(
36-
html.div(
37-
{"id": f"pyscript-{uuid}", "className": "pyscript", "data-uuid": uuid},
38-
initial,
39-
),
40-
html.script({"type": "py"}, executor),
33+
result = html_to_vdom(
34+
pyscript_component_html(tuple(str(fp) for fp in file_paths), initial, root)
4135
)
36+
result["tagName"] = ""
37+
return result
4238

4339

4440
def pyscript_component(
45-
*file_paths: str,
41+
*file_paths: str | Path,
4642
initial: str | VdomDict | ComponentType = "",
4743
root: str = "root",
4844
) -> ComponentType:

src/reactpy/pyscript/utils.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def minify_python(source: str):
4848
)
4949

5050

51-
def render_pyscript_executor(file_paths: tuple[str, ...], uuid: str, root: str) -> str:
51+
def pyscript_executor_html(file_paths: Sequence[str], uuid: str, root: str) -> str:
5252
"""Inserts the user's code into the PyScript template using pattern matching."""
5353
# Create a valid PyScript executor by replacing the template values
5454
executor = PYSCRIPT_COMPONENT_TEMPLATE.replace("UUID", uuid)
@@ -63,19 +63,23 @@ def render_pyscript_executor(file_paths: tuple[str, ...], uuid: str, root: str)
6363
user_code = user_code.replace("\t", " ") # Normalize the text
6464
user_code = textwrap.indent(user_code, " ") # Add indentation to match template
6565

66+
# Ensure the root component exists
67+
if f"def {root}():" not in user_code:
68+
raise ValueError(
69+
f"Could not find the root component function '{root}' in your PyScript file(s)."
70+
)
71+
6672
# Insert the user code into the PyScript template
6773
return executor.replace(" def root(): ...", user_code)
6874

6975

7076
def pyscript_component_html(
71-
file_paths: tuple[str, ...], initial: str | VdomDict, root: str
77+
file_paths: Sequence[str], initial: str | VdomDict, root: str
7278
) -> str:
7379
"""Renders a PyScript component with the user's code."""
7480
_initial = initial if isinstance(initial, str) else vdom_to_html(initial)
7581
uuid = uuid4().hex
76-
executor_code = render_pyscript_executor(
77-
file_paths=file_paths, uuid=uuid, root=root
78-
)
82+
executor_code = pyscript_executor_html(file_paths=file_paths, uuid=uuid, root=root)
7983

8084
return (
8185
f'<div id="pyscript-{uuid}" class="pyscript" data-uuid="{uuid}">'

0 commit comments

Comments
 (0)