Skip to content

Commit 6148799

Browse files
authored
Improve v2 ReactPy-Django compatibility (#1340)
- Add `shield` option to `use_async_effect` - Fix edge case where `use_state` could throw an exception when converting a value from `set_state(<Object>)` to `set_state(None)`. - Add a `REACTPY_MAX_QUEUE_SIZE` setting to allow users to constrain ReactPy memory usage - Make `pyscript` utils more extensible so Django can leverage them - Switch the pyscript executor hook's state from `ThreadLocal` to `ContextVar` to avoid hook state clashes - Note: ContextVar wasn't used prior since it was broken in a previous release of PyScript. - `@reactpy/client` will now always embed the HTTP URL into the websocket in order to provide immediate URL access to `reactpy-router`. - Events now support debounce, which can now be configured per event with `event.debounce = <milliseconds>`. - Modify build workflow to allow ReactPy to be installed via `pip install git+...` for development purposes
1 parent 613b256 commit 6148799

48 files changed

Lines changed: 2220 additions & 275 deletions

Some content is hidden

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

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
uses: ./.github/workflows/.hatch-run.yml
1515
with:
1616
job-name: "Publish to PyPI"
17-
run-cmd: "hatch run javascript:build && hatch build --clean && hatch publish --yes"
17+
run-cmd: "hatch build --clean && hatch publish --yes"
1818
secrets:
1919
pypi-username: ${{ secrets.PYPI_USERNAME }}
2020
pypi-password: ${{ secrets.PYPI_PASSWORD }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
src/reactpy/static/*.js*
33
src/reactpy/static/morphdom/
44
src/reactpy/static/pyscript/
5+
src/reactpy/static/wheels/
56
src/js/**/*.tgz
67
src/js/**/LICENSE
78

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Don't forget to remove deprecated code on each major release!
3636
- Added `reactpy.reactjs.component_from_string` to import ReactJS components from a string.
3737
- Added `reactpy.reactjs.component_from_npm` to import ReactJS components from NPM.
3838
- Added `reactpy.h` as a shorthand alias for `reactpy.html`.
39+
- Added `reactpy.config.REACTPY_MAX_QUEUE_SIZE` to configure the maximum size of all ReactPy asyncio queues (e.g. receive buffer, send buffer, event buffer) before ReactPy begins waiting until a slot frees up. This can be used to constraint memory usage.
3940

4041
### Changed
4142

@@ -61,6 +62,7 @@ Don't forget to remove deprecated code on each major release!
6162
- `reactpy.types.VdomDictConstructor` has been renamed to `reactpy.types.VdomConstructor`.
6263
- `REACTPY_ASYNC_RENDERING` can now de-duplicate and cascade renders where necessary.
6364
- `REACTPY_ASYNC_RENDERING` is now defaulted to `True` for up to 40x performance improvements in environments with high concurrency.
65+
- Events now support debounce, which can now be configured per event with `event.debounce = <milliseconds>`. Note that `input`, `select`, and `textarea` elements default to 200ms debounce.
6466

6567
### Deprecated
6668

@@ -85,6 +87,7 @@ Don't forget to remove deprecated code on each major release!
8587
- Removed `reactpy.run`. See the documentation for the new method to run ReactPy applications.
8688
- Removed `reactpy.backend.*`. See the documentation for the new method to run ReactPy applications.
8789
- Removed `reactpy.core.types` module. Use `reactpy.types` instead.
90+
- Removed `reactpy.utils.str_to_bool`.
8891
- Removed `reactpy.utils.html_to_vdom`. Use `reactpy.utils.string_to_reactpy` instead.
8992
- Removed `reactpy.utils.vdom_to_html`. Use `reactpy.utils.reactpy_to_string` instead.
9093
- Removed `reactpy.vdom`. Use `reactpy.Vdom` instead.
@@ -101,6 +104,7 @@ Don't forget to remove deprecated code on each major release!
101104
- Fixed a bug where script elements would not render to the DOM as plain text.
102105
- Fixed a bug where the `key` property provided within server-side ReactPy code was failing to propagate to the front-end JavaScript components.
103106
- Fixed a bug where `RuntimeError("Hook stack is in an invalid state")` errors could be generated when using a webserver that reuses threads.
107+
- Fixed a bug where events on controlled inputs (e.g. `html.input({"onChange": ...})`) could be lost during rapid actions.
104108
- Allow for ReactPy and ReactJS components to be arbitrarily inserted onto the page with any possible hierarchy.
105109

106110
## [1.1.0] - 2024-11-24

pyproject.toml

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,24 @@ installer = "uv"
7171
reactpy = "reactpy._console.cli:entry_point"
7272

7373
[[tool.hatch.build.hooks.build-scripts.scripts]]
74-
commands = []
75-
artifacts = []
74+
commands = ['python "src/build_scripts/build_py_wheel.py"']
75+
artifacts = ["src/reactpy/static/wheels/*.whl"]
7676

7777
#############################
7878
# >>> Hatch Test Runner <<< #
7979
#############################
8080
[tool.hatch.envs.hatch-test.scripts]
8181
run = [
82-
'hatch --env default run "src/build_scripts/install_playwright.py"',
82+
'hatch --env default run "{root}/src/build_scripts/install_playwright.py"',
8383
"hatch --env default run javascript:build --dev",
8484
"hatch --env default build -t wheel",
8585
"pytest{env:HATCH_TEST_ARGS:} {args} --max-worker-restart 10",
8686
]
8787
run-cov = [
88-
'hatch --env default run "src/build_scripts/install_playwright.py"',
88+
'hatch --env default run "{root}/src/build_scripts/install_playwright.py"',
8989
"hatch --env default run javascript:build --dev",
9090
"hatch --env default build -t wheel",
91-
'hatch --env default run "src/build_scripts/delete_old_coverage.py"',
91+
'hatch --env default run "{root}/src/build_scripts/delete_old_coverage.py"',
9292
"coverage run -m pytest{env:HATCH_TEST_ARGS:} {args} --max-worker-restart 10",
9393
]
9494
cov-combine = "coverage combine"
@@ -136,25 +136,25 @@ detached = true
136136

137137
[tool.hatch.envs.docs.scripts]
138138
build = [
139-
"cd docs && poetry install",
140-
"cd docs && poetry run sphinx-build -a -T -W --keep-going -b doctest source build",
139+
'cd "{root}/docs" && poetry install',
140+
'cd "{root}/docs" && poetry run sphinx-build -a -T -W --keep-going -b doctest "{root}/docs/source" "{root}/docs/build"',
141141
]
142142
docker_build = [
143143
"hatch run docs:build",
144-
"docker build . --file ./docs/Dockerfile --tag reactpy-docs:latest",
144+
'docker build --file "{root}/docs/Dockerfile" --tag reactpy-docs:latest "{root}"',
145145
]
146146
docker_serve = [
147147
"hatch run docs:docker_build",
148148
"docker run --rm -p 5000:5000 reactpy-docs:latest",
149149
]
150150
check = [
151-
"cd docs && poetry install",
152-
"cd docs && poetry run sphinx-build -a -T -W --keep-going -b doctest source build",
153-
"docker build . --file ./docs/Dockerfile",
151+
'cd "{root}/docs" && poetry install',
152+
'cd "{root}/docs" && poetry run sphinx-build -a -T -W --keep-going -b doctest "{root}/docs/source" "{root}/docs/build"',
153+
'docker build --file "{root}/docs/Dockerfile" "{root}"',
154154
]
155155
serve = [
156-
"cd docs && poetry install",
157-
"cd docs && poetry run python main.py --watch=../src/ --ignore=**/_auto/* --ignore=**/custom.js --ignore=**/node_modules/* --ignore=**/package-lock.json -a -E -b html source build",
156+
'cd "{root}/docs" && poetry install',
157+
'cd "{root}/docs" && poetry run python "{root}/docs/main.py" --watch="{root}/src" --ignore=**/_auto/* --ignore=**/custom.js --ignore=**/node_modules/* --ignore=**/package-lock.json -a -E -b html "{root}/docs/source" "{root}/docs/build"',
158158
]
159159

160160
################################
@@ -174,7 +174,7 @@ extra-dependencies = [
174174
]
175175

176176
[tool.hatch.envs.python.scripts]
177-
type_check = ["pyright src/reactpy"]
177+
type_check = ['pyright "{root}/src/reactpy"']
178178

179179
############################
180180
# >>> Hatch JS Scripts <<< #
@@ -186,39 +186,39 @@ detached = true
186186
[tool.hatch.envs.javascript.scripts]
187187
check = [
188188
'hatch run javascript:build',
189-
'bun run --cwd "src/js" lint',
190-
'bun run --cwd "src/js/packages/event-to-object" checkTypes',
191-
'bun run --cwd "src/js/packages/@reactpy/client" checkTypes',
192-
'bun run --cwd "src/js/packages/@reactpy/app" checkTypes',
189+
'bun run --cwd "{root}/src/js" lint',
190+
'bun run --cwd "{root}/src/js/packages/event-to-object" checkTypes',
191+
'bun run --cwd "{root}/src/js/packages/@reactpy/client" checkTypes',
192+
'bun run --cwd "{root}/src/js/packages/@reactpy/app" checkTypes',
193193
]
194-
fix = ['bun install --cwd "src/js"', 'bun run --cwd "src/js" format']
194+
fix = ['bun install --cwd "{root}/src/js"', 'bun run --cwd "{root}/src/js" format']
195195
test = ['hatch run javascript:build_event_to_object --dev', 'bun test']
196196
build = [
197-
'hatch run "src/build_scripts/clean_js_dir.py"',
198-
'bun install --cwd "src/js"',
197+
'hatch run "{root}/src/build_scripts/clean_js_dir.py"',
198+
'bun install --cwd "{root}/src/js"',
199199
'hatch run javascript:build_event_to_object {args}',
200200
'hatch run javascript:build_client {args}',
201201
'hatch run javascript:build_app {args}',
202-
'hatch --env default run "src/build_scripts/copy_dir.py" "src/js/node_modules/@pyscript/core/dist" "src/reactpy/static/pyscript"',
203-
'hatch --env default run "src/build_scripts/copy_dir.py" "src/js/node_modules/morphdom/dist" "src/reactpy/static/morphdom"',
202+
'hatch --env default run "{root}/src/build_scripts/copy_dir.py" "{root}/src/js/node_modules/@pyscript/core/dist" "{root}/src/reactpy/static/pyscript"',
203+
'hatch --env default run "{root}/src/build_scripts/copy_dir.py" "{root}/src/js/node_modules/morphdom/dist" "{root}/src/reactpy/static/morphdom"',
204204

205205
]
206206
build_event_to_object = [
207-
'hatch run "src/build_scripts/build_js_event_to_object.py" {args}',
207+
'hatch run "{root}/src/build_scripts/build_js_event_to_object.py" {args}',
208208
]
209-
build_client = ['hatch run "src/build_scripts/build_js_client.py" {args}']
210-
build_app = ['hatch run "src/build_scripts/build_js_app.py" {args}']
209+
build_client = ['hatch run "{root}/src/build_scripts/build_js_client.py" {args}']
210+
build_app = ['hatch run "{root}/src/build_scripts/build_js_app.py" {args}']
211211
publish_event_to_object = [
212212
'hatch run javascript:build_event_to_object',
213213
# FIXME: This is a temporary workaround. We are using `bun pm pack`->`npm publish` to fix missing "Trusted Publishing" support in `bun publish`
214214
# See the following ticket https://github.com/oven-sh/bun/issues/15601
215-
'cd "src/js/packages/event-to-object" && bun pm pack --filename "packages/event-to-object/dist.tgz" && bunx npm@11.8.0 publish dist.tgz --provenance --access public',
215+
'cd "{root}/src/js/packages/event-to-object" && bun pm pack --filename "{root}/src/js/packages/event-to-object/dist.tgz" && bunx npm@11.8.0 publish "{root}/src/js/packages/event-to-object/dist.tgz" --provenance --access public',
216216
]
217217
publish_client = [
218218
'hatch run javascript:build_client',
219219
# FIXME: This is a temporary workaround. We are using `bun pm pack`->`npm publish` to fix missing "Trusted Publishing" support in `bun publish`
220220
# See the following ticket https://github.com/oven-sh/bun/issues/15601
221-
'cd "src/js/packages/@reactpy/client" && bun pm pack --filename "packages/@reactpy/client/dist.tgz" && bunx npm@11.8.0 publish dist.tgz --provenance --access public',
221+
'cd "{root}/src/js/packages/@reactpy/client" && bun pm pack --filename "{root}/src/js/packages/@reactpy/client/dist.tgz" && bunx npm@11.8.0 publish "{root}/src/js/packages/@reactpy/client/dist.tgz" --provenance --access public',
222222
]
223223

224224
#########################
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# /// script
2+
# requires-python = ">=3.11"
3+
# dependencies = []
4+
# ///
5+
6+
from __future__ import annotations
7+
8+
import importlib.util
9+
import logging
10+
import os
11+
import re
12+
import shutil
13+
import subprocess
14+
import sys
15+
from pathlib import Path
16+
17+
_logger = logging.getLogger(__name__)
18+
_SKIP_ENV_VAR = "REACTPY_SKIP_PY_WHEEL_BUILD"
19+
20+
21+
def _reactpy_version(root_dir: Path) -> str:
22+
init_file = root_dir / "src" / "reactpy" / "__init__.py"
23+
if match := re.search(
24+
r'^__version__ = "([^"]+)"$',
25+
init_file.read_text(encoding="utf-8"),
26+
re.MULTILINE,
27+
):
28+
return match[1]
29+
raise RuntimeError("Could not determine the current ReactPy version.")
30+
31+
32+
def _matching_reactpy_wheel(dist_dir: Path, version: str) -> Path | None:
33+
matching_wheels = sorted(
34+
dist_dir.glob(f"reactpy-{version}-*.whl"),
35+
key=lambda path: path.stat().st_mtime,
36+
reverse=True,
37+
)
38+
return matching_wheels[0] if matching_wheels else None
39+
40+
41+
def _hatch_command(root_dir: Path, *args: str) -> list[str] | None:
42+
for candidate in (
43+
root_dir / ".venv" / "Scripts" / "hatch.exe",
44+
root_dir / ".venv" / "bin" / "hatch",
45+
):
46+
if candidate.exists():
47+
return [str(candidate), *args]
48+
49+
if hatch_command := shutil.which("hatch"):
50+
return [hatch_command, *args]
51+
52+
if importlib.util.find_spec("hatch") is not None:
53+
return [sys.executable, "-m", "hatch", *args]
54+
55+
return None
56+
57+
58+
def _hatch_build_command(root_dir: Path) -> list[str] | None:
59+
return _hatch_command(root_dir, "build", "-t", "wheel")
60+
61+
62+
def _without_hatch_env_vars(env: dict[str, str]) -> dict[str, str]:
63+
cleaned_env = env.copy()
64+
for key in tuple(cleaned_env):
65+
if key.startswith("HATCH_ENV_"):
66+
cleaned_env.pop(key)
67+
return cleaned_env
68+
69+
70+
def _run_hatch_command(root_dir: Path, command: list[str], failure_message: str) -> int:
71+
result = subprocess.run( # noqa: S603
72+
command,
73+
capture_output=True,
74+
text=True,
75+
check=False,
76+
cwd=root_dir,
77+
env=_without_hatch_env_vars(os.environ.copy()),
78+
)
79+
80+
if result.returncode != 0:
81+
_logger.error(
82+
"%s\nstdout:\n%s\nstderr:\n%s",
83+
failure_message,
84+
result.stdout,
85+
result.stderr,
86+
)
87+
return result.returncode
88+
89+
return 0
90+
91+
92+
def _build_packaged_static_assets(root_dir: Path) -> int:
93+
hatch_command = _hatch_command(root_dir, "run", "javascript:build")
94+
if not hatch_command:
95+
_logger.error("Could not locate Hatch while building packaged static assets.")
96+
return 1
97+
98+
return _run_hatch_command(
99+
root_dir,
100+
hatch_command,
101+
"Failed to build packaged static assets.",
102+
)
103+
104+
105+
def main() -> int:
106+
if os.environ.get(_SKIP_ENV_VAR):
107+
print("Skipping local ReactPy wheel build.") # noqa: T201
108+
return 0
109+
110+
root_dir = Path(__file__).parent.parent.parent
111+
112+
if static_assets_result := _build_packaged_static_assets(root_dir):
113+
return static_assets_result
114+
115+
version = _reactpy_version(root_dir)
116+
static_wheels_dir = root_dir / "src" / "reactpy" / "static" / "wheels"
117+
dist_dir = root_dir / "dist"
118+
hatch_build_command = _hatch_build_command(root_dir)
119+
120+
if not hatch_build_command:
121+
_logger.error("Could not locate Hatch while building the embedded wheel.")
122+
return 1
123+
124+
static_wheels_dir.mkdir(parents=True, exist_ok=True)
125+
for wheel_file in static_wheels_dir.glob("reactpy-*.whl"):
126+
wheel_file.unlink()
127+
128+
os.environ[_SKIP_ENV_VAR] = "1"
129+
try:
130+
if wheel_build_result := _run_hatch_command(
131+
root_dir,
132+
hatch_build_command,
133+
"Failed to build the embedded ReactPy wheel.",
134+
):
135+
return wheel_build_result
136+
finally:
137+
os.environ.pop(_SKIP_ENV_VAR, None)
138+
139+
built_wheel = _matching_reactpy_wheel(dist_dir, version)
140+
if not built_wheel:
141+
_logger.error("Failed to locate the newly built ReactPy wheel in %s", dist_dir)
142+
return 1
143+
144+
shutil.copy2(built_wheel, static_wheels_dir / built_wheel.name)
145+
print(f"Embedded local ReactPy wheel at '{static_wheels_dir / built_wheel.name}'") # noqa: T201
146+
return 0
147+
148+
149+
if __name__ == "__main__":
150+
raise SystemExit(main())

src/js/bun.lockb

6.16 KB
Binary file not shown.

src/js/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
"event-to-object": "2.0.0"
1616
},
1717
"devDependencies": {
18-
"@eslint/js": "^9.39.1",
19-
"bun-types": "^1.3.3",
20-
"eslint": "^9.39.1",
21-
"globals": "^16.5.0",
22-
"prettier": "^3.6.2",
23-
"typescript-eslint": "^8.47.0"
18+
"@eslint/js": "^10.0.1",
19+
"bun-types": "^1.3.12",
20+
"eslint": "^10.2.1",
21+
"globals": "^17.5.0",
22+
"prettier": "^3.8.3",
23+
"typescript-eslint": "^8.58.2"
2424
},
2525
"license": "MIT",
2626
"scripts": {

src/js/packages/@reactpy/client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@
3636
"checkTypes": "tsc --noEmit"
3737
},
3838
"type": "module",
39-
"version": "1.1.0"
39+
"version": "1.1.1"
4040
}

0 commit comments

Comments
 (0)