Skip to content

Commit 9dccf73

Browse files
committed
prototype pysript executor
1 parent 6dea8b3 commit 9dccf73

File tree

8 files changed

+131
-33
lines changed

8 files changed

+131
-33
lines changed

src/js/packages/@reactpy/client/src/mount.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function mountReactPy(props: MountProps) {
88
const wsProtocol = `ws${window.location.protocol === "https:" ? "s" : ""}:`;
99
const wsOrigin = `${wsProtocol}//${window.location.host}`;
1010
const componentUrl = new URL(
11-
`${wsOrigin}${props.pathPrefix}${props.appendComponentPath || ""}`,
11+
`${wsOrigin}${props.pathPrefix}${props.componentPath || ""}`,
1212
);
1313

1414
// Embed the initial HTTP path into the WebSocket URL

src/js/packages/@reactpy/client/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export type GenericReactPyClientProps = {
3535
export type MountProps = {
3636
mountElement: HTMLElement;
3737
pathPrefix: string;
38-
appendComponentPath?: string;
38+
componentPath?: string;
3939
reconnectInterval?: number;
4040
reconnectMaxInterval?: number;
4141
reconnectMaxRetries?: number;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from __future__ import annotations
2+
3+
import hashlib
4+
import re
5+
from dataclasses import dataclass
6+
from datetime import datetime, timezone
7+
from email.utils import formatdate
8+
from pathlib import Path
9+
10+
from typing_extensions import Unpack
11+
12+
from reactpy import html
13+
from reactpy.asgi.executors.standalone import ReactPy, ReactPyApp
14+
from reactpy.asgi.middleware import ReactPyMiddleware
15+
from reactpy.asgi.utils import vdom_head_to_html
16+
from reactpy.pyscript.utils import pyscript_component_html, pyscript_setup_html
17+
from reactpy.types import (
18+
ReactPyConfig,
19+
VdomDict,
20+
)
21+
22+
23+
class ReactPyCSR(ReactPy):
24+
def __init__(
25+
self,
26+
*component_paths: str | Path,
27+
extra_py: tuple[str, ...] = (),
28+
extra_js: dict | str = "",
29+
pyscript_config: dict | str = "",
30+
root_name: str = "root",
31+
initial: str | VdomDict = "",
32+
http_headers: dict[str, str] | None = None,
33+
html_head: VdomDict | None = None,
34+
html_lang: str = "en",
35+
**settings: Unpack[ReactPyConfig],
36+
) -> None:
37+
"""Variant of ReactPy's standalone that only performs Client-Side Rendering (CSR).
38+
39+
Parameters:
40+
...
41+
"""
42+
ReactPyMiddleware.__init__(
43+
self, app=ReactPyAppCSR(self), root_components=[], **settings
44+
)
45+
if not component_paths:
46+
raise ValueError("At least one component file path must be provided.")
47+
self.component_paths = tuple(str(path) for path in component_paths)
48+
self.extra_py = extra_py
49+
self.extra_js = extra_js
50+
self.pyscript_config = pyscript_config
51+
self.root_name = root_name
52+
self.initial = initial
53+
self.extra_headers = http_headers or {}
54+
self.dispatcher_pattern = re.compile(f"^{self.dispatcher_path}?")
55+
self.html_head = html_head or html.head()
56+
self.html_lang = html_lang
57+
58+
59+
@dataclass
60+
class ReactPyAppCSR(ReactPyApp):
61+
"""ReactPy's standalone ASGI application for Client-Side Rendering (CSR)."""
62+
63+
parent: ReactPyCSR
64+
_index_html = ""
65+
_etag = ""
66+
_last_modified = ""
67+
68+
def render_index_html(self) -> None:
69+
"""Process the index.html and store the results in this class."""
70+
head_content = vdom_head_to_html(self.parent.html_head)
71+
pyscript_setup = pyscript_setup_html(
72+
extra_py=self.parent.extra_py,
73+
extra_js=self.parent.extra_js,
74+
config=self.parent.pyscript_config,
75+
)
76+
pyscript_component = pyscript_component_html(
77+
file_paths=self.parent.component_paths,
78+
initial=self.parent.initial,
79+
root=self.parent.root_name,
80+
)
81+
head_content.replace("</head>", f"{pyscript_setup}</head>")
82+
83+
self._index_html = (
84+
"<!doctype html>"
85+
f'<html lang="{self.parent.html_lang}">'
86+
f"{head_content}"
87+
"<body>"
88+
f"{pyscript_component}"
89+
"</body>"
90+
"</html>"
91+
)
92+
self._etag = f'"{hashlib.md5(self._index_html.encode(), usedforsecurity=False).hexdigest()}"'
93+
self._last_modified = formatdate(
94+
datetime.now(tz=timezone.utc).timestamp(), usegmt=True
95+
)

src/reactpy/asgi/executors/standalone.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
RootComponentConstructor,
2525
VdomDict,
2626
)
27-
from reactpy.utils import render_mount_template
27+
from reactpy.utils import asgi_component_html
2828

2929
_logger = getLogger(__name__)
3030

@@ -174,7 +174,7 @@ async def __call__(
174174

175175
# Store the HTTP response in memory for performance
176176
if not self._index_html:
177-
self.render_index_template()
177+
self.render_index_html()
178178

179179
# Response headers for `index.html` responses
180180
request_headers = dict(scope["headers"])
@@ -206,14 +206,14 @@ async def __call__(
206206
response = ResponseHTML(self._index_html, headers=response_headers)
207207
await response(scope, receive, send) # type: ignore
208208

209-
def render_index_template(self) -> None:
209+
def render_index_html(self) -> None:
210210
"""Process the index.html and store the results in this class."""
211211
self._index_html = (
212212
"<!doctype html>"
213213
f'<html lang="{self.parent.html_lang}">'
214214
f"{vdom_head_to_html(self.parent.html_head)}"
215215
"<body>"
216-
f"{render_mount_template('app', '', '')}"
216+
f"{asgi_component_html(element_id='app', class_='', component_path='')}"
217217
"</body>"
218218
"</html>"
219219
)

src/reactpy/jinja.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,22 @@
33

44
from jinja2_simple_tags import StandaloneTag
55

6-
from reactpy.config import REACTPY_DEBUG, REACTPY_PATH_PREFIX
76
from reactpy.pyscript.utils import (
8-
PYSCRIPT_LAYOUT_HANDLER,
9-
extend_pyscript_config,
10-
render_pyscript_template,
7+
pyscript_component_html,
8+
pyscript_setup_html,
119
)
12-
from reactpy.utils import render_mount_template
10+
from reactpy.utils import asgi_component_html
1311

1412

1513
class Component(StandaloneTag): # type: ignore
1614
safe_output = True
1715
tags: ClassVar[set[str]] = {"component"}
1816

1917
def render(self, dotted_path: str, **kwargs: str) -> str:
20-
return render_mount_template(
18+
return asgi_component_html(
2119
element_id=uuid4().hex,
2220
class_=kwargs.pop("class", ""),
23-
append_component_path=f"{dotted_path}/",
21+
component_path=f"{dotted_path}/",
2422
)
2523

2624

@@ -29,7 +27,7 @@ class PyScriptComponent(StandaloneTag): # type: ignore
2927
tags: ClassVar[set[str]] = {"pyscript_component"}
3028

3129
def render(self, *file_paths: str, initial: str = "", root: str = "root") -> str:
32-
return render_pyscript_template(
30+
return pyscript_component_html(
3331
file_paths=file_paths, initial=initial, root=root
3432
)
3533

@@ -55,14 +53,4 @@ def render(
5553
configuration values.
5654
"""
5755

58-
hide_pyscript_debugger = f'<link rel="stylesheet" href="{REACTPY_PATH_PREFIX.current}static/pyscript-hide-debug.css" />'
59-
pyscript_config = extend_pyscript_config(extra_py, extra_js, config)
60-
61-
return (
62-
f'<link rel="stylesheet" href="{REACTPY_PATH_PREFIX.current}static/pyscript/core.css" />'
63-
f'<link rel="stylesheet" href="{REACTPY_PATH_PREFIX.current}static/pyscript-custom.css" />'
64-
f"{'' if REACTPY_DEBUG.current else hide_pyscript_debugger}"
65-
f'<script type="module" async crossorigin="anonymous" src="{REACTPY_PATH_PREFIX.current}static/pyscript/core.js">'
66-
"</script>"
67-
f'<py-script async config="{pyscript_config}">{PYSCRIPT_LAYOUT_HANDLER}</py-script>'
68-
)
56+
return pyscript_setup_html(extra_py=extra_py, extra_js=extra_js, config=config)

src/reactpy/pyscript/layout_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
import asyncio
33
import logging
44

5+
import js
56
from jsonpointer import set_pointer
67
from pyodide.ffi.wrappers import add_event_listener
78
from pyscript.js_modules import morphdom
89

9-
import js
1010
from reactpy.core.layout import Layout
1111

1212

src/reactpy/pyscript/utils.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
import json
55
import textwrap
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, Callable
7+
from typing import TYPE_CHECKING
88
from uuid import uuid4
99

1010
import jsonpointer
1111
import orjson
1212

1313
import reactpy
14-
from reactpy.config import REACTPY_PATH_PREFIX
14+
from reactpy.config import REACTPY_DEBUG, REACTPY_PATH_PREFIX
1515
from reactpy.types import VdomDict
1616
from reactpy.utils import vdom_to_html
1717

@@ -46,7 +46,7 @@ def render_pyscript_executor(file_paths: tuple[str, ...], uuid: str, root: str)
4646
return executor.replace(" def root(): ...", user_code)
4747

4848

49-
def render_pyscript_template(
49+
def pyscript_component_html(
5050
file_paths: tuple[str, ...], initial: str | VdomDict, root: str
5151
) -> str:
5252
"""Renders a PyScript component with the user's code."""
@@ -64,6 +64,23 @@ def render_pyscript_template(
6464
)
6565

6666

67+
def pyscript_setup_html(
68+
extra_py: Sequence[str], extra_js: dict | str, config: dict | str
69+
) -> str:
70+
"""Renders the PyScript setup code."""
71+
hide_pyscript_debugger = f'<link rel="stylesheet" href="{REACTPY_PATH_PREFIX.current}static/pyscript-hide-debug.css" />'
72+
pyscript_config = extend_pyscript_config(extra_py, extra_js, config)
73+
74+
return (
75+
f'<link rel="stylesheet" href="{REACTPY_PATH_PREFIX.current}static/pyscript/core.css" />'
76+
f'<link rel="stylesheet" href="{REACTPY_PATH_PREFIX.current}static/pyscript-custom.css" />'
77+
f"{'' if REACTPY_DEBUG.current else hide_pyscript_debugger}"
78+
f'<script type="module" async crossorigin="anonymous" src="{REACTPY_PATH_PREFIX.current}static/pyscript/core.js">'
79+
"</script>"
80+
f'<py-script async config="{pyscript_config}">{PYSCRIPT_LAYOUT_HANDLER}</py-script>'
81+
)
82+
83+
6784
def extend_pyscript_config(
6885
extra_py: Sequence[str], extra_js: dict | str, config: dict | str
6986
) -> str:

src/reactpy/utils.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,17 +316,15 @@ def _vdom_attr_to_html_str(key: str, value: Any) -> tuple[str, str]:
316316
_CAMEL_CASE_SUB_PATTERN = re.compile(r"(?<!^)(?=[A-Z])")
317317

318318

319-
def render_mount_template(
320-
element_id: str, class_: str, append_component_path: str
321-
) -> str:
319+
def asgi_component_html(element_id: str, class_: str, component_path: str) -> str:
322320
return (
323321
f'<div id="{element_id}" class="{class_}"></div>'
324322
'<script type="module" crossorigin="anonymous">'
325323
f'import {{ mountReactPy }} from "{config.REACTPY_PATH_PREFIX.current}static/index.js";'
326324
"mountReactPy({"
327325
f' mountElement: document.getElementById("{element_id}"),'
328326
f' pathPrefix: "{config.REACTPY_PATH_PREFIX.current}",'
329-
f' appendComponentPath: "{append_component_path}",'
327+
f' componentPath: "{component_path}",'
330328
f" reconnectInterval: {config.REACTPY_RECONNECT_INTERVAL.current},"
331329
f" reconnectMaxInterval: {config.REACTPY_RECONNECT_MAX_INTERVAL.current},"
332330
f" reconnectMaxRetries: {config.REACTPY_RECONNECT_MAX_RETRIES.current},"

0 commit comments

Comments
 (0)