Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit babc2de

Browse filesBrowse files
authored
Client-Side Python Components (#1269)
- Add template tags for rendering pyscript components - Add `pyscript_component` component to embed pyscript components into standard ReactPy server-side applications - Create new ASGI app that can run standalone client-side ReactPy - Convert all ASGI dependencies into an optional `reactpy[asgi]` parameter to minimize client-side install size - Start throwing 404 errors when static files are not found
1 parent 49bdda1 commit babc2de
Copy full SHA for babc2de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Dismiss banner

47 files changed

+1320
-291
lines changed

‎.gitignore

Copy file name to clipboardExpand all lines: .gitignore
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# --- Build Artifacts ---
2-
src/reactpy/static/*
2+
src/reactpy/static/index.js*
3+
src/reactpy/static/morphdom/
4+
src/reactpy/static/pyscript/
35

46
# --- Jupyter ---
57
*.ipynb_checkpoints

‎docs/source/about/changelog.rst

Copy file name to clipboardExpand all lines: docs/source/about/changelog.rst
+6-4Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ Unreleased
1616
----------
1717

1818
**Added**
19-
- :pull:`1113` - Added ``reactpy.ReactPy`` that can be used to run ReactPy in standalone mode.
20-
- :pull:`1113` - Added ``reactpy.ReactPyMiddleware`` that can be used to run ReactPy with any ASGI compatible framework.
21-
- :pull:`1113` - Added ``reactpy.jinja.Component`` that can be used alongside ``ReactPyMiddleware`` to embed several ReactPy components into your existing application.
22-
- :pull:`1113` - Added ``standard``, ``uvicorn``, ``jinja`` installation extras (for example ``pip install reactpy[standard]``).
19+
- :pull:`1113` - Added ``reactpy.executors.asgi.ReactPy`` that can be used to run ReactPy in standalone mode via ASGI.
20+
- :pull:`1269` - Added ``reactpy.executors.asgi.ReactPyPyodide`` that can be used to run ReactPy in standalone mode via ASGI, but rendered entirely client-sided.
21+
- :pull:`1113` - Added ``reactpy.executors.asgi.ReactPyMiddleware`` that can be used to utilize ReactPy within any ASGI compatible framework.
22+
- :pull:`1113` :pull:`1269` - Added ``reactpy.templatetags.Jinja`` that can be used alongside ``ReactPyMiddleware`` to embed several ReactPy components into your existing application. This includes the following template tags: ``{% component %}``, ``{% pyscript_component %}``, and ``{% pyscript_setup %}``.
23+
- :pull:`1269` - Added ``reactpy.pyscript_component`` that can be used to embed ReactPy components into your existing application.
24+
- :pull:`1113` - Added ``uvicorn`` and ``jinja`` installation extras (for example ``pip install reactpy[jinja]``).
2325
- :pull:`1113` - Added support for Python 3.12 and 3.13.
2426
- :pull:`1264` - Added ``reactpy.use_async_effect`` hook.
2527
- :pull:`1267` - Added ``shutdown_timeout`` parameter to the ``reactpy.use_async_effect`` hook.

‎pyproject.toml

Copy file name to clipboardExpand all lines: pyproject.toml
+18-22Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ readme = "README.md"
1313
keywords = ["react", "javascript", "reactpy", "component"]
1414
license = "MIT"
1515
authors = [
16-
{ name = "Ryan Morshead", email = "ryan.morshead@gmail.com" },
1716
{ name = "Mark Bakhit", email = "archiethemonger@gmail.com" },
17+
{ name = "Ryan Morshead", email = "ryan.morshead@gmail.com" },
1818
]
1919
requires-python = ">=3.9"
2020
classifiers = [
@@ -28,24 +28,24 @@ classifiers = [
2828
"Programming Language :: Python :: Implementation :: PyPy",
2929
]
3030
dependencies = [
31-
"exceptiongroup >=1.0",
32-
"typing-extensions >=3.10",
33-
"anyio >=3",
34-
"jsonpatch >=1.32",
3531
"fastjsonschema >=2.14.5",
3632
"requests >=2",
37-
"colorlog >=6",
38-
"asgiref >=3",
3933
"lxml >=4",
40-
"servestatic >=3.0.0",
41-
"orjson >=3",
42-
"asgi-tools",
34+
"anyio >=3",
35+
"typing-extensions >=3.10",
4336
]
4437
dynamic = ["version"]
4538
urls.Changelog = "https://reactpy.dev/docs/about/changelog.html"
4639
urls.Documentation = "https://reactpy.dev/"
4740
urls.Source = "https://github.com/reactive-python/reactpy"
4841

42+
[project.optional-dependencies]
43+
all = ["reactpy[asgi,jinja,uvicorn,testing]"]
44+
asgi = ["asgiref", "asgi-tools", "servestatic", "orjson", "pip"]
45+
jinja = ["jinja2-simple-tags", "jinja2 >=3"]
46+
uvicorn = ["uvicorn[standard]"]
47+
testing = ["playwright"]
48+
4949
[tool.hatch.version]
5050
path = "src/reactpy/__init__.py"
5151

@@ -75,32 +75,24 @@ commands = [
7575
'bun run --cwd "src/js/packages/@reactpy/client" build',
7676
'bun install --cwd "src/js/packages/@reactpy/app"',
7777
'bun run --cwd "src/js/packages/@reactpy/app" build',
78-
'python "src/build_scripts/copy_dir.py" "src/js/packages/@reactpy/app/dist" "src/reactpy/static"',
78+
'python "src/build_scripts/copy_dir.py" "src/js/packages/@reactpy/app/node_modules/@pyscript/core/dist" "src/reactpy/static/pyscript"',
79+
'python "src/build_scripts/copy_dir.py" "src/js/packages/@reactpy/app/node_modules/morphdom/dist" "src/reactpy/static/morphdom"',
7980
]
8081
artifacts = []
8182

82-
[project.optional-dependencies]
83-
all = ["reactpy[jinja,uvicorn,testing]"]
84-
standard = ["reactpy[jinja,uvicorn]"]
85-
jinja = ["jinja2-simple-tags", "jinja2 >=3"]
86-
uvicorn = ["uvicorn[standard]"]
87-
testing = ["playwright"]
88-
8983

9084
#############################
9185
# >>> Hatch Test Runner <<< #
9286
#############################
9387

9488
[tool.hatch.envs.hatch-test]
9589
extra-dependencies = [
90+
"reactpy[all]",
9691
"pytest-sugar",
9792
"pytest-asyncio",
9893
"responses",
99-
"playwright",
94+
"exceptiongroup",
10095
"jsonpointer",
101-
"uvicorn[standard]",
102-
"jinja2-simple-tags",
103-
"jinja2",
10496
"starlette",
10597
]
10698

@@ -160,6 +152,7 @@ serve = [
160152

161153
[tool.hatch.envs.python]
162154
extra-dependencies = [
155+
"reactpy[all]",
163156
"ruff",
164157
"toml",
165158
"mypy==1.8",
@@ -240,6 +233,8 @@ omit = [
240233
"src/reactpy/__init__.py",
241234
"src/reactpy/_console/*",
242235
"src/reactpy/__main__.py",
236+
"src/reactpy/pyscript/layout_handler.py",
237+
"src/reactpy/pyscript/component_template.py",
243238
]
244239

245240
[tool.coverage.report]
@@ -325,6 +320,7 @@ lint.unfixable = [
325320

326321
[tool.ruff.lint.isort]
327322
known-first-party = ["reactpy"]
323+
known-third-party = ["js"]
328324

329325
[tool.ruff.lint.per-file-ignores]
330326
# Tests can use magic values, assertions, and relative imports
8.39 KB
Binary file not shown.

‎src/js/packages/@reactpy/app/package.json

Copy file name to clipboardExpand all lines: src/js/packages/@reactpy/app/package.json
+4-2Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
"preact": "^10.25.4"
99
},
1010
"devDependencies": {
11-
"typescript": "^5.7.3"
11+
"typescript": "^5.7.3",
12+
"@pyscript/core": "^0.6",
13+
"morphdom": "^2"
1214
},
1315
"scripts": {
14-
"build": "bun build \"src/index.ts\" --outdir=\"dist\" --minify --sourcemap=\"linked\"",
16+
"build": "bun build \"src/index.ts\" --outdir=\"../../../../reactpy/static/\" --minify --sourcemap=\"linked\"",
1517
"checkTypes": "tsc --noEmit"
1618
}
1719
}

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

Copy file name to clipboardExpand all lines: src/js/packages/@reactpy/client/src/mount.tsx
+1-1Lines 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

Copy file name to clipboardExpand all lines: src/js/packages/@reactpy/client/src/types.ts
+1-1Lines 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;

‎src/reactpy/__init__.py

Copy file name to clipboardExpand all lines: src/reactpy/__init__.py
+3-6Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
from reactpy import asgi, config, logging, types, web, widgets
1+
from reactpy import config, logging, types, web, widgets
22
from reactpy._html import html
3-
from reactpy.asgi.middleware import ReactPyMiddleware
4-
from reactpy.asgi.standalone import ReactPy
53
from reactpy.core import hooks
64
from reactpy.core.component import component
75
from reactpy.core.events import event
@@ -22,17 +20,15 @@
2220
)
2321
from reactpy.core.layout import Layout
2422
from reactpy.core.vdom import vdom
23+
from reactpy.pyscript.components import pyscript_component
2524
from reactpy.utils import Ref, html_to_vdom, vdom_to_html
2625

2726
__author__ = "The Reactive Python Team"
2827
__version__ = "2.0.0a1"
2928

3029
__all__ = [
3130
"Layout",
32-
"ReactPy",
33-
"ReactPyMiddleware",
3431
"Ref",
35-
"asgi",
3632
"component",
3733
"config",
3834
"create_context",
@@ -41,6 +37,7 @@
4137
"html",
4238
"html_to_vdom",
4339
"logging",
40+
"pyscript_component",
4441
"types",
4542
"use_async_effect",
4643
"use_callback",

‎src/reactpy/core/hooks.py

Copy file name to clipboardExpand all lines: src/reactpy/core/hooks.py
+4-3Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
overload,
1717
)
1818

19-
from asgiref import typing as asgi_types
2019
from typing_extensions import TypeAlias
2120

2221
from reactpy.config import REACTPY_DEBUG
@@ -25,9 +24,11 @@
2524
from reactpy.utils import Ref
2625

2726
if not TYPE_CHECKING:
28-
# make flake8 think that this variable exists
2927
ellipsis = type(...)
3028

29+
if TYPE_CHECKING:
30+
from asgiref import typing as asgi_types
31+
3132

3233
__all__ = [
3334
"use_async_effect",
@@ -339,7 +340,7 @@ def use_connection() -> Connection[Any]:
339340

340341
def use_scope() -> asgi_types.HTTPScope | asgi_types.WebSocketScope:
341342
"""Get the current :class:`~reactpy.types.Connection`'s scope."""
342-
return use_connection().scope
343+
return use_connection().scope # type: ignore
343344

344345

345346
def use_location() -> Location:
File renamed without changes.
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from reactpy.executors.asgi.middleware import ReactPyMiddleware
2+
from reactpy.executors.asgi.pyscript import ReactPyPyscript
3+
from reactpy.executors.asgi.standalone import ReactPy
4+
5+
__all__ = ["ReactPy", "ReactPyMiddleware", "ReactPyPyscript"]

‎src/reactpy/asgi/middleware.py renamed to ‎src/reactpy/executors/asgi/middleware.py

Copy file name to clipboardExpand all lines: src/reactpy/executors/asgi/middleware.py
+23-17Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,26 @@
1111
from typing import Any
1212

1313
import orjson
14-
from asgi_tools import ResponseWebSocket
14+
from asgi_tools import ResponseText, ResponseWebSocket
1515
from asgiref import typing as asgi_types
1616
from asgiref.compatibility import guarantee_single_callable
1717
from servestatic import ServeStaticASGI
1818
from typing_extensions import Unpack
1919

2020
from reactpy import config
21-
from reactpy.asgi.utils import check_path, import_components, process_settings
2221
from reactpy.core.hooks import ConnectionContext
2322
from reactpy.core.layout import Layout
2423
from reactpy.core.serve import serve_layout
25-
from reactpy.types import (
24+
from reactpy.executors.asgi.types import (
2625
AsgiApp,
2726
AsgiHttpApp,
2827
AsgiLifespanApp,
2928
AsgiWebsocketApp,
3029
AsgiWebsocketReceive,
3130
AsgiWebsocketSend,
32-
Connection,
33-
Location,
34-
ReactPyConfig,
35-
RootComponentConstructor,
3631
)
32+
from reactpy.executors.utils import check_path, import_components, process_settings
33+
from reactpy.types import Connection, Location, ReactPyConfig, RootComponentConstructor
3734

3835
_logger = logging.getLogger(__name__)
3936

@@ -81,8 +78,6 @@ def __init__(
8178
self.dispatcher_pattern = re.compile(
8279
f"^{self.dispatcher_path}(?P<dotted_path>[a-zA-Z0-9_.]+)/$"
8380
)
84-
self.js_modules_pattern = re.compile(f"^{self.web_modules_path}.*")
85-
self.static_pattern = re.compile(f"^{self.static_path}.*")
8681

8782
# User defined ASGI apps
8883
self.extra_http_routes: dict[str, AsgiHttpApp] = {}
@@ -95,7 +90,7 @@ def __init__(
9590

9691
# Directory attributes
9792
self.web_modules_dir = config.REACTPY_WEB_MODULES_DIR.current
98-
self.static_dir = Path(__file__).parent.parent / "static"
93+
self.static_dir = Path(__file__).parent.parent.parent / "static"
9994

10095
# Initialize the sub-applications
10196
self.component_dispatch_app = ComponentDispatchApp(parent=self)
@@ -134,14 +129,14 @@ def match_dispatch_path(self, scope: asgi_types.WebSocketScope) -> bool:
134129
return bool(re.match(self.dispatcher_pattern, scope["path"]))
135130

136131
def match_static_path(self, scope: asgi_types.HTTPScope) -> bool:
137-
return bool(re.match(self.static_pattern, scope["path"]))
132+
return scope["path"].startswith(self.static_path)
138133

139134
def match_web_modules_path(self, scope: asgi_types.HTTPScope) -> bool:
140-
return bool(re.match(self.js_modules_pattern, scope["path"]))
135+
return scope["path"].startswith(self.web_modules_path)
141136

142137
def match_extra_paths(self, scope: asgi_types.Scope) -> AsgiApp | None:
143-
# Custom defined routes are unused within middleware to encourage users to handle
144-
# routing within their root ASGI application.
138+
# Custom defined routes are unused by default to encourage users to handle
139+
# routing within their ASGI framework of choice.
145140
return None
146141

147142

@@ -224,7 +219,7 @@ async def run_dispatcher(self) -> None:
224219
self.scope["query_string"].decode(), strict_parsing=True
225220
)
226221
connection = Connection(
227-
scope=self.scope,
222+
scope=self.scope, # type: ignore
228223
location=Location(
229224
path=ws_query_string.get("http_pathname", [""])[0],
230225
query_string=ws_query_string.get("http_query_string", [""])[0],
@@ -263,7 +258,7 @@ async def __call__(
263258
"""ASGI app for ReactPy static files."""
264259
if not self._static_file_server:
265260
self._static_file_server = ServeStaticASGI(
266-
self.parent.asgi_app,
261+
Error404App(),
267262
root=self.parent.static_dir,
268263
prefix=self.parent.static_path,
269264
)
@@ -285,10 +280,21 @@ async def __call__(
285280
"""ASGI app for ReactPy web modules."""
286281
if not self._static_file_server:
287282
self._static_file_server = ServeStaticASGI(
288-
self.parent.asgi_app,
283+
Error404App(),
289284
root=self.parent.web_modules_dir,
290285
prefix=self.parent.web_modules_path,
291286
autorefresh=True,
292287
)
293288

294289
await self._static_file_server(scope, receive, send)
290+
291+
292+
class Error404App:
293+
async def __call__(
294+
self,
295+
scope: asgi_types.HTTPScope,
296+
receive: asgi_types.ASGIReceiveCallable,
297+
send: asgi_types.ASGISendCallable,
298+
) -> None:
299+
response = ResponseText("Resource not found on this server.", status_code=404)
300+
await response(scope, receive, send) # type: ignore

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.