From b88e533362e7d012c871a210b4fcd925425a3150 Mon Sep 17 00:00:00 2001 From: Shawn Crawley Date: Thu, 12 Feb 2026 11:12:07 -0700 Subject: [PATCH 1/2] Refactoring for ReactPy 2.0 --- .../pyscript_component_initial_object.py | 4 +- .../pyscript_component_initial_string.py | 4 +- .../pyscript_component_multiple_files_root.py | 4 +- .../python/pyscript_component_root.py | 4 +- docs/examples/python/pyscript_ssr_parent.py | 8 ++-- docs/examples/python/use_location.py | 2 +- .../homepage_examples/add_interactivity.py | 2 +- docs/src/reference/components.md | 2 +- docs/src/reference/hooks.md | 2 +- docs/src/reference/template-tag.md | 2 +- pyproject.toml | 9 ++--- src/js/package.json | 6 +-- src/reactpy_django/auth/components.py | 2 +- src/reactpy_django/components.py | 40 ++++--------------- src/reactpy_django/config.py | 6 +-- src/reactpy_django/decorators.py | 2 +- src/reactpy_django/forms/components.py | 15 +++---- src/reactpy_django/forms/transforms.py | 5 ++- src/reactpy_django/hooks.py | 8 ++-- src/reactpy_django/html.py | 4 +- src/reactpy_django/pyscript/components.py | 39 ------------------ src/reactpy_django/router/converters.py | 7 ---- src/reactpy_django/router/resolvers.py | 26 +++++------- src/reactpy_django/templatetags/reactpy.py | 4 +- src/reactpy_django/types.py | 6 +-- src/reactpy_django/utils.py | 14 +++---- src/reactpy_django/websocket/consumer.py | 4 +- tests/test_app/components.py | 22 +++++----- .../pyscript/components/server_side.py | 12 +++--- tests/test_app/router/components.py | 24 +++++------ .../templates/view_to_component_script.html | 4 +- tests/test_app/tests/test_components.py | 16 ++++---- 32 files changed, 113 insertions(+), 196 deletions(-) delete mode 100644 src/reactpy_django/pyscript/components.py delete mode 100644 src/reactpy_django/router/converters.py diff --git a/docs/examples/python/pyscript_component_initial_object.py b/docs/examples/python/pyscript_component_initial_object.py index d84328a4..a5156d1b 100644 --- a/docs/examples/python/pyscript_component_initial_object.py +++ b/docs/examples/python/pyscript_component_initial_object.py @@ -1,6 +1,4 @@ -from reactpy import component, html - -from reactpy_django.components import pyscript_component +from reactpy import component, html, pyscript_component @component diff --git a/docs/examples/python/pyscript_component_initial_string.py b/docs/examples/python/pyscript_component_initial_string.py index bb8f9d17..075f44e0 100644 --- a/docs/examples/python/pyscript_component_initial_string.py +++ b/docs/examples/python/pyscript_component_initial_string.py @@ -1,6 +1,4 @@ -from reactpy import component, html - -from reactpy_django.components import pyscript_component +from reactpy import component, html, pyscript_component @component diff --git a/docs/examples/python/pyscript_component_multiple_files_root.py b/docs/examples/python/pyscript_component_multiple_files_root.py index fd826137..03bce026 100644 --- a/docs/examples/python/pyscript_component_multiple_files_root.py +++ b/docs/examples/python/pyscript_component_multiple_files_root.py @@ -1,6 +1,4 @@ -from reactpy import component, html - -from reactpy_django.components import pyscript_component +from reactpy import component, html, pyscript_component @component diff --git a/docs/examples/python/pyscript_component_root.py b/docs/examples/python/pyscript_component_root.py index 3d795247..f41d8780 100644 --- a/docs/examples/python/pyscript_component_root.py +++ b/docs/examples/python/pyscript_component_root.py @@ -1,6 +1,4 @@ -from reactpy import component, html - -from reactpy_django.components import pyscript_component +from reactpy import component, html, pyscript_component @component diff --git a/docs/examples/python/pyscript_ssr_parent.py b/docs/examples/python/pyscript_ssr_parent.py index 524cdc52..84cc9b5e 100644 --- a/docs/examples/python/pyscript_ssr_parent.py +++ b/docs/examples/python/pyscript_ssr_parent.py @@ -1,11 +1,11 @@ -from reactpy import component, html - -from reactpy_django.components import pyscript_component +from reactpy import component, html, pyscript_component @component def server_side_component(): return html.div( "This text is from my server-side component", - pyscript_component("./example_project/my_app/components/root.py"), + pyscript_component( + "./example_project/my_app/components/root.py", + ), ) diff --git a/docs/examples/python/use_location.py b/docs/examples/python/use_location.py index 454da7f6..f7190a7f 100644 --- a/docs/examples/python/use_location.py +++ b/docs/examples/python/use_location.py @@ -7,4 +7,4 @@ def my_component(): location = use_location() - return html.div(location.pathname + location.search) + return html.div(location.path + location.query_string) diff --git a/docs/overrides/homepage_examples/add_interactivity.py b/docs/overrides/homepage_examples/add_interactivity.py index 9a7bf76f..d183719b 100644 --- a/docs/overrides/homepage_examples/add_interactivity.py +++ b/docs/overrides/homepage_examples/add_interactivity.py @@ -17,7 +17,7 @@ def searchable_video_list(videos): search_text, set_search_text = use_state("") found_videos = filter_videos(videos, search_text) - return html._( + return html( search_input( {"onChange": lambda event: set_search_text(event["target"]["value"])}, value=search_text, diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index 448af463..7fb681fa 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -41,7 +41,7 @@ This allows you to embedded any number of client-side PyScript components within | Name | Type | Description | Default | | --- | --- | --- | --- | | `#!python *file_paths` | `#!python str` | File path to your client-side component. If multiple paths are provided, the contents are automatically merged. | N/A | - | `#!python initial` | `#!python str | VdomDict | ComponentType` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` | + | `#!python initial` | `#!python str | VdomDict | Component` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` | | `#!python root` | `#!python str` | The name of the root component function. | `#!python "root"` | diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index b29990d5..e8c3d8fb 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -591,7 +591,7 @@ Shortcut that returns the browser's current `#!python Location`. | Type | Description | | --- | --- | - | `#!python Location` | An object containing the current URL's `#!python pathname` and `#!python search` query. | + | `#!python Location` | An object containing the current URL's `#!python path` and `#!python query_string` query. | --- diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 146dae4c..366a98fb 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -191,7 +191,7 @@ Your Python component file will be directly loaded into the browser. It must hav | Name | Type | Description | Default | | --- | --- | --- | --- | | `#!python *file_paths` | `#!python str` | File path to your client-side component. If multiple paths are provided, the contents are automatically merged. | N/A | - | `#!python initial` | `#!python str | VdomDict | ComponentType` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` | + | `#!python initial` | `#!python str | VdomDict | Component` | The initial HTML that is displayed prior to the PyScript component loads. This can either be a string containing raw HTML, a `#!python reactpy.html` snippet, or a non-interactive component. | `#!python ""` | | `#!python root` | `#!python str` | The name of the root component function. | `#!python "root"` | diff --git a/pyproject.toml b/pyproject.toml index 64d530e7..9c21a711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,8 +44,8 @@ classifiers = [ dependencies = [ "channels>=4.0.0", "django>=4.2.0", - "reactpy>=1.1.0, <2.0.0", - "reactpy-router>=1.0.3, <2.0.0", + "reactpy>=2.0.0b10, <3.0.0", + "reactpy-router>2.0.0", "dill>=0.3.5", "orjson>=3.6.0", "nest_asyncio>=1.5.0", @@ -96,18 +96,17 @@ extra-dependencies = [ "servestatic", "django-bootstrap5", "decorator", - ] matrix-name-format = "{variable}-{value}" # Django 4.2 [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.9", "3.10", "3.11", "3.12"] +python = ["3.11", "3.12"] django = ["4.2"] # Django 5.0 [[tool.hatch.envs.hatch-test.matrix]] -python = ["3.10", "3.11", "3.12"] +python = ["3.11", "3.12"] django = ["5.0"] # Django 5.1 diff --git a/src/js/package.json b/src/js/package.json index 4dbb3b6f..55facd25 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,10 +1,10 @@ { "dependencies": { "@pyscript/core": "^0.6", - "@reactpy/client": "^0.3.2", - "event-to-object": "^0.1.2", + "@reactpy/client": "^1.0.3", + "event-to-object": "^2.0.0", "morphdom": "^2.7.4", - "preact": "^10.26.9", + "preact": "^10.27.2", "react": "npm:@preact/compat@17.1.2", "react-dom": "npm:@preact/compat@17.1.2" }, diff --git a/src/reactpy_django/auth/components.py b/src/reactpy_django/auth/components.py index e0a1e065..c99ef9ef 100644 --- a/src/reactpy_django/auth/components.py +++ b/src/reactpy_django/auth/components.py @@ -56,7 +56,7 @@ def setup_asgi_scope(): any relevant actions.""" scope["reactpy"]["synchronize_auth"] = synchronize_auth - @hooks.use_effect(dependencies=[sync_needed]) + @hooks.use_async_effect(dependencies=[sync_needed]) async def synchronize_auth_watchdog(): """Detect if the client has taken too long to request a auth session synchronization. diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 9234c42e..8ae0ddf4 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -9,11 +9,10 @@ from django.http import HttpRequest from django.urls import reverse from reactpy import component, hooks, html, utils -from reactpy.types import ComponentType, Key, VdomDict +from reactpy.types import Component, Key, VdomDict from reactpy_django.exceptions import ViewNotRegisteredError from reactpy_django.forms.components import _django_form -from reactpy_django.pyscript.components import _pyscript_component from reactpy_django.utils import ( cached_static_file, del_html_head_body_transform, @@ -54,7 +53,7 @@ def constructor( *args, key: Key | None = None, **kwargs, - ) -> ComponentType: + ) -> Component: return _view_to_component( view=view, transforms=transforms, @@ -84,13 +83,13 @@ def constructor( *args, key: Key | None = None, **kwargs, - ) -> ComponentType: + ) -> Component: return _view_to_iframe(view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key) return constructor -def django_css(static_path: str, key: Key | None = None) -> ComponentType: +def django_css(static_path: str, key: Key | None = None) -> Component: """Fetches a CSS static file for use within ReactPy. This allows for deferred CSS loading. Args: @@ -103,7 +102,7 @@ def django_css(static_path: str, key: Key | None = None) -> ComponentType: return _django_css(static_path=static_path, key=key) -def django_js(static_path: str, key: Key | None = None) -> ComponentType: +def django_js(static_path: str, key: Key | None = None) -> Component: """Fetches a JS static file for use within ReactPy. This allows for deferred JS loading. Args: @@ -131,7 +130,7 @@ def django_form( top_children: Sequence[Any] = (), bottom_children: Sequence[Any] = (), key: Key | None = None, -) -> ComponentType: +) -> Component: """Converts a Django form to a ReactPy component. Args: @@ -174,29 +173,6 @@ def django_form( ) -def pyscript_component( - *file_paths: str, - initial: str | VdomDict | ComponentType = "", - root: str = "root", -) -> ComponentType: - """ - Args: - file_paths: File path to your client-side component. If multiple paths are \ - provided, the contents are automatically merged. - - Kwargs: - initial: The initial HTML that is displayed prior to the PyScript component \ - loads. This can either be a string containing raw HTML, a \ - `#!python reactpy.html` snippet, or a non-interactive component. - root: The name of the root component function. - """ - return _pyscript_component( - *file_paths, - initial=initial, - root=root, - ) - - @component def _view_to_component( view: Callable | View | str, @@ -217,7 +193,7 @@ def _view_to_component( resolved_view: Callable = import_module(view) if isinstance(view, str) else view # type: ignore # Render the view render within a hook - @hooks.use_effect( + @hooks.use_async_effect( dependencies=[ json.dumps(vars(_request), default=generate_obj_name), json.dumps([_args, _kwargs], default=generate_obj_name), @@ -228,7 +204,7 @@ async def _render_view(): # Render the view response = await render_view(resolved_view, _request, _args, _kwargs) set_converted_view( - utils.html_to_vdom( + utils.string_to_reactpy( response.content.decode("utf-8").strip(), del_html_head_body_transform, *transforms, diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index cc3ca2fc..0b71ec17 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -7,13 +7,13 @@ from django.core.cache import DEFAULT_CACHE_ALIAS from django.db import DEFAULT_DB_ALIAS from reactpy.config import REACTPY_ASYNC_RENDERING as _REACTPY_ASYNC_RENDERING -from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE +from reactpy.config import REACTPY_DEBUG as _REACTPY_DEBUG from reactpy_django.utils import import_dotted_path if TYPE_CHECKING: from django.views import View - from reactpy.core.types import ComponentConstructor + from reactpy.types import ComponentConstructor from reactpy_django.types import ( AsyncPostprocessor, @@ -27,7 +27,7 @@ # Configurable through Django settings.py DJANGO_DEBUG = settings.DEBUG # Snapshot of Django's DEBUG setting -_REACTPY_DEBUG_MODE.set_current(settings.DEBUG) +_REACTPY_DEBUG.set_current(settings.DEBUG) _REACTPY_ASYNC_RENDERING.set_current(getattr(settings, "REACTPY_ASYNC_RENDERING", _REACTPY_ASYNC_RENDERING.current)) REACTPY_URL_PREFIX: str = getattr( settings, diff --git a/src/reactpy_django/decorators.py b/src/reactpy_django/decorators.py index 6b3d220e..0454f5df 100644 --- a/src/reactpy_django/decorators.py +++ b/src/reactpy_django/decorators.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from django.contrib.auth.models import AbstractUser - from reactpy.core.types import ComponentConstructor + from reactpy.types import ComponentConstructor def user_passes_test( diff --git a/src/reactpy_django/forms/components.py b/src/reactpy_django/forms/components.py index a10d5c92..fec5225a 100644 --- a/src/reactpy_django/forms/components.py +++ b/src/reactpy_django/forms/components.py @@ -7,7 +7,7 @@ from django.forms import Form, ModelForm from reactpy import component, hooks, html, utils from reactpy.core.events import event -from reactpy.web import export, module_from_file +from reactpy.reactjs import component_from_file from reactpy_django.forms.transforms import ( convert_html_props_to_reactjs, @@ -24,11 +24,12 @@ if TYPE_CHECKING: from collections.abc import Sequence - from reactpy.core.types import VdomDict + from reactpy.types import VdomDict -DjangoForm = export( - module_from_file("reactpy-django", file=Path(__file__).parent.parent / "static" / "reactpy_django" / "index.js"), - ("DjangoForm"), +DjangoForm = component_from_file( + Path(__file__).parent.parent / "static" / "reactpy_django" / "index.js", + import_names=("DjangoForm"), + name="reactpy-django", ) @@ -63,7 +64,7 @@ def _django_form( ) # Validate and render the form - @hooks.use_effect(dependencies=[str(submitted_data)]) + @hooks.use_async_effect(dependencies=[str(submitted_data)]) async def render_form(): """Forms must be rendered in an async loop to allow database fields to execute.""" if submitted_data: @@ -113,7 +114,7 @@ async def _on_change(_event): }, DjangoForm({"onSubmitCallback": on_submit_callback, "formId": f"reactpy-{uuid}"}), *top_children, - utils.html_to_vdom( + utils.string_to_reactpy( rendered_form, convert_html_props_to_reactjs, convert_textarea_children_to_prop, diff --git a/src/reactpy_django/forms/transforms.py b/src/reactpy_django/forms/transforms.py index 1a757b77..141edc37 100644 --- a/src/reactpy_django/forms/transforms.py +++ b/src/reactpy_django/forms/transforms.py @@ -7,7 +7,7 @@ from reactpy.core.events import EventHandler, to_event_handler_function if TYPE_CHECKING: - from reactpy.core.types import VdomDict + from reactpy.types import VdomDict def convert_html_props_to_reactjs(vdom_tree: VdomDict) -> VdomDict: @@ -81,7 +81,8 @@ def infer_key_from_attributes(vdom_tree: VdomDict) -> VdomDict: key = attributes.get("name") if key: - vdom_tree["key"] = key + attributes["key"] = key + vdom_tree["attributes"] = attributes return vdom_tree diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index de18c190..e13599e1 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -17,7 +17,7 @@ from channels import DEFAULT_CHANNEL_LAYER from channels import auth as channels_auth from channels.layers import InMemoryChannelLayer, get_channel_layer -from reactpy import use_callback, use_effect, use_memo, use_ref, use_state +from reactpy import use_async_effect, use_callback, use_effect, use_memo, use_ref, use_state from reactpy import use_connection as _use_connection from reactpy import use_location as _use_location from reactpy import use_scope as _use_scope @@ -43,7 +43,7 @@ from channels_redis.core import RedisChannelLayer from django.contrib.auth.models import AbstractUser - from reactpy.backend.types import Location + from reactpy.types import Location _logger = logging.getLogger(__name__) @@ -378,7 +378,7 @@ def use_channel_layer( raise ValueError(msg) # Add/remove a group's channel during component mount/dismount respectively. - @use_effect(dependencies=[]) + @use_async_effect(dependencies=[]) async def group_manager(): if group: await channel_layer.group_add(group, channel_name) @@ -386,7 +386,7 @@ async def group_manager(): return None # Listen for messages on the channel using the provided `receiver` function. - @use_effect + @use_async_effect async def message_receiver(): if not receiver: return diff --git a/src/reactpy_django/html.py b/src/reactpy_django/html.py index d35daf43..551c1875 100644 --- a/src/reactpy_django/html.py +++ b/src/reactpy_django/html.py @@ -1,3 +1,3 @@ -from reactpy.core.vdom import make_vdom_constructor +from reactpy import Vdom -pyscript = make_vdom_constructor("py-script") +pyscript = Vdom("py-script") diff --git a/src/reactpy_django/pyscript/components.py b/src/reactpy_django/pyscript/components.py deleted file mode 100644 index 255b6354..00000000 --- a/src/reactpy_django/pyscript/components.py +++ /dev/null @@ -1,39 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING -from uuid import uuid4 - -from reactpy import component, hooks, html - -from reactpy_django.html import pyscript -from reactpy_django.pyscript.utils import render_pyscript_template -from reactpy_django.utils import reactpy_to_string - -if TYPE_CHECKING: - from reactpy.types import ComponentType, VdomDict - - -@component -def _pyscript_component( - *file_paths: str, - initial: str | VdomDict | ComponentType = "", - root: str = "root", -): - rendered, set_rendered = hooks.use_state(False) - uuid = hooks.use_ref(uuid4().hex.replace("-", "")).current - initial = reactpy_to_string(initial, uuid=uuid) - executor = render_pyscript_template(file_paths, uuid, root) - - if not rendered: - # FIXME: This is needed to properly re-render PyScript during a WebSocket - # disconnection / reconnection. There may be a better way to do this in the future. - set_rendered(True) - return None - - return html._( - html.div( - {"id": f"pyscript-{uuid}", "className": "pyscript", "data-uuid": uuid}, - initial, - ), - pyscript({"async": ""}, executor), - ) diff --git a/src/reactpy_django/router/converters.py b/src/reactpy_django/router/converters.py deleted file mode 100644 index 0b54efbc..00000000 --- a/src/reactpy_django/router/converters.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls.converters import get_converters -from reactpy_router.types import ConversionInfo - -CONVERTERS: dict[str, ConversionInfo] = { - name: {"regex": converter.regex, "func": converter.to_python} for name, converter in get_converters().items() -} -CONVERTERS["any"] = {"regex": r".*", "func": str} diff --git a/src/reactpy_django/router/resolvers.py b/src/reactpy_django/router/resolvers.py index 30bb3f46..3c405f05 100644 --- a/src/reactpy_django/router/resolvers.py +++ b/src/reactpy_django/router/resolvers.py @@ -1,26 +1,18 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from django.urls.converters import get_converters -from reactpy_router.resolvers import StarletteResolver +from typing import TYPE_CHECKING -from reactpy_django.router.converters import CONVERTERS +from reactpy_router.resolvers import ReactPyResolver if TYPE_CHECKING: - from reactpy_router.types import ConversionInfo, Route + from reactpy_router.types import Route -class DjangoResolver(StarletteResolver): +class DjangoResolver(ReactPyResolver): """A simple route resolver that uses regex to match paths""" - - def __init__( - self, - route: Route, - param_pattern=r"<(?P\w+:)?(?P\w+)>", - converters: dict[str, ConversionInfo] | None = None, - ) -> None: - super().__init__( - route=route, - param_pattern=param_pattern, - converters=converters or CONVERTERS, - ) + converters = ReactPyResolver.converters | { + name: {"regex": converter.regex, "func": converter.to_python} + for name, converter in get_converters().items() + } diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index f74b1ffa..e319a70c 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -27,7 +27,7 @@ if TYPE_CHECKING: from django.http import HttpRequest - from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict + from reactpy.types import Component, ComponentConstructor, VdomDict register = template.Library() @@ -187,7 +187,7 @@ def component( def pyscript_component( context: template.RequestContext, *file_paths: str, - initial: str | VdomDict | ComponentType = "", + initial: str | VdomDict | Component = "", root: str = "root", ): """ diff --git a/src/reactpy_django/types.py b/src/reactpy_django/types.py index 2703523d..e71f9c63 100644 --- a/src/reactpy_django/types.py +++ b/src/reactpy_django/types.py @@ -13,7 +13,7 @@ ) from django.http import HttpRequest -from reactpy.types import ComponentType, Connection, Key +from reactpy.types import Component, Connection, Key from typing_extensions import ParamSpec if TYPE_CHECKING: @@ -104,11 +104,11 @@ async def __call__(self, message: dict) -> None: ... class ViewToComponentConstructor(Protocol): def __call__( self, request: HttpRequest | None = None, *args: Any, key: Key | None = None, **kwargs: Any - ) -> ComponentType: ... + ) -> Component: ... class ViewToIframeConstructor(Protocol): - def __call__(self, *args: Any, key: Key | None = None, **kwargs: Any) -> ComponentType: ... + def __call__(self, *args: Any, key: Key | None = None, **kwargs: Any) -> Component: ... class UseAuthLogin(Protocol): diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 775ba0f9..ac0208df 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -26,10 +26,10 @@ from django.http import HttpRequest, HttpResponse from django.template import engines from django.utils.encoding import smart_str -from reactpy import vdom_to_html -from reactpy.backend.types import Connection, Location +from reactpy import reactpy_to_string as _reactpy_to_string from reactpy.core.hooks import ConnectionContext from reactpy.core.layout import Layout +from reactpy.types import Connection, Location from reactpy_django.exceptions import ( ComponentDoesNotExistError, @@ -359,7 +359,7 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): - SYNC_LAYOUT_THREAD.submit(self.loop.run_until_complete, self.__aexit__()).result() + SYNC_LAYOUT_THREAD.submit(self.loop.run_until_complete, self.__aexit__(exc_type, exc_val, exc_tb)).result() self.loop.close() def sync_render(self): @@ -405,21 +405,21 @@ def prerender_component( user_component(*args, **kwargs), value=Connection( scope=scope, - location=Location(pathname=request.path, search=f"?{search}" if search else ""), + location=Location(path=request.path, query_string=f"?{search}" if search else ""), carrier=request, ), ) ) as layout: vdom_tree = layout.sync_render()["model"] - return vdom_to_html(vdom_tree) # type: ignore + return _reactpy_to_string(vdom_tree) # type: ignore def reactpy_to_string(vdom_or_component: Any, request: HttpRequest | None = None, uuid: str | None = None) -> str: """Converts a VdomDict or component to an HTML string. If a string is provided instead, it will be automatically returned.""" if isinstance(vdom_or_component, dict): - return vdom_to_html(vdom_or_component) # type: ignore + return _reactpy_to_string(vdom_or_component) # type: ignore if hasattr(vdom_or_component, "render"): if not request: @@ -520,7 +520,7 @@ def cached_static_file(static_path: str) -> str: def del_html_head_body_transform(vdom: VdomDict) -> VdomDict: - """Transform intended for use with `html_to_vdom`. + """Transform intended for use with `string_to_reactpy `. Removes ``, ``, and `` while preserving their children. diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 47ccc717..d4834fe9 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -16,10 +16,10 @@ from channels.auth import login from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.utils import timezone -from reactpy.backend.types import Connection, Location from reactpy.core.hooks import ConnectionContext from reactpy.core.layout import Layout from reactpy.core.serve import serve_layout +from reactpy.types import Connection, Location from reactpy_django.tasks import clean from reactpy_django.utils import ensure_async @@ -161,7 +161,7 @@ async def run_dispatcher(self): self.recv_queue = asyncio.Queue() connection = Connection( # For `use_connection` scope=scope, - location=Location(pathname=http_pathname, search=http_search), + location=Location(path=http_pathname, query_string=http_search), carrier=self, ) now = timezone.now() diff --git a/tests/test_app/components.py b/tests/test_app/components.py index bcdf8276..b7017e9b 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -28,13 +28,13 @@ @component def hello_world(): - return html._(html.div({"id": "hello-world"}, "Hello World!")) + return html(html.div({"id": "hello-world"}, "Hello World!")) @component def button(): count, set_count = hooks.use_state(0) - return html._( + return html( html.div( "button:", html.button( @@ -49,7 +49,7 @@ def button(): @component def parameterized_component(x, y): total = x + y - return html._( + return html( html.div( {"id": "parametrized-component", "data-value": total}, f"parameterized_component: {total}", @@ -61,7 +61,7 @@ def parameterized_component(x, y): def object_in_templatetag(my_object: TestObject): success = bool(my_object and my_object.value) co_name = inspect.currentframe().f_code.co_name - return html._(html.div({"id": co_name, "data-success": success}, f"{co_name}: ", str(my_object))) + return html(html.div({"id": co_name, "data-success": success}, f"{co_name}: ", str(my_object))) SimpleButtonModule = web.module_from_file( @@ -75,7 +75,7 @@ def object_in_templatetag(my_object: TestObject): @component def button_from_js_module(): - return html._("button_from_js_module:", SimpleButton({"id": "button-from-js-module"})) + return html("button_from_js_module:", SimpleButton({"id": "button-from-js-module"})) @component @@ -125,7 +125,7 @@ def django_css(): @component def django_js(): success = False - return html._( + return html( html.div( {"id": "django-js", "data-success": success}, f"django_js: {success}", @@ -319,7 +319,7 @@ def on_change(event): elif items.data is None: rendered_items = html.h2("Loading...") else: - rendered_items = html._( + rendered_items = html( html.h3("Not Done"), _render_todo_items([i for i in items.data if not i.done], toggle_item), html.h3("Done"), @@ -390,7 +390,7 @@ async def on_change(event): elif items.data is None: rendered_items = html.h2("Loading...") else: - rendered_items = html._( + rendered_items = html( html.h3("Not Done"), _render_todo_items([i for i in items.data if not i.done], toggle_item), html.h3("Done"), @@ -503,7 +503,7 @@ def on_click(_): post_request.method = "POST" set_request(post_request) - return html._( + return html( html.button( { "id": f"{inspect.currentframe().f_code.co_name}_btn", @@ -522,7 +522,7 @@ def view_to_component_args(): def on_click(_): set_success("") - return html._( + return html( html.button( { "id": f"{inspect.currentframe().f_code.co_name}_btn", @@ -541,7 +541,7 @@ def view_to_component_kwargs(): def on_click(_): set_success("") - return html._( + return html( html.button( { "id": f"{inspect.currentframe().f_code.co_name}_btn", diff --git a/tests/test_app/pyscript/components/server_side.py b/tests/test_app/pyscript/components/server_side.py index 682411d5..fdffe58c 100644 --- a/tests/test_app/pyscript/components/server_side.py +++ b/tests/test_app/pyscript/components/server_side.py @@ -1,13 +1,13 @@ -from reactpy import component, html, use_state - -from reactpy_django.components import pyscript_component +from reactpy import component, html, use_state, pyscript_component @component def parent(): return html.div( {"id": "parent"}, - pyscript_component("./test_app/pyscript/components/child.py"), + pyscript_component( + "./test_app/pyscript/components/child.py", + ), ) @@ -30,5 +30,7 @@ def parent_toggle(): {"onClick": lambda _: set_state(not state)}, "Click to show/hide", ), - pyscript_component("./test_app/pyscript/components/child.py"), + pyscript_component( + "./test_app/pyscript/components/child.py", + ), ) diff --git a/tests/test_app/router/components.py b/tests/test_app/router/components.py index ea95c5f2..47eb425f 100644 --- a/tests/test_app/router/components.py +++ b/tests/test_app/router/components.py @@ -11,14 +11,14 @@ def display_params(string: str): search_params = use_search_params() url_params = use_params() - return html._( + return html( html.div({"id": "router-string"}, string), html.div( - {"id": "router-path", "data-path": location.pathname}, - f"path: {location.pathname}", + {"id": "router-path", "data-path": location.path}, + f"path: {location.path}", ), html.div(f"url_params: {url_params}"), - html.div(f"location.search: {location.search}"), + html.div(f"location.query_string: {location.query_string}"), html.div(f"search_params: {search_params}"), ) @@ -31,12 +31,12 @@ def show_route(path: str, *children: Route) -> Route: def main(): return django_router( show_route("/router/", show_route("subroute/")), - show_route("/router/unspecified//"), - show_route("/router/integer//"), - show_route("/router/path//"), - show_route("/router/slug//"), - show_route("/router/string//"), - show_route("/router/uuid//"), - show_route("/router/any/"), - show_route("/router/two///"), + show_route("/router/unspecified/{value}/"), + show_route("/router/integer/{value:int}/"), + show_route("/router/path/{value:path}/"), + show_route("/router/slug/{value:slug}/"), + show_route("/router/string/{value:str}/"), + show_route("/router/uuid/{value:uuid}/"), + show_route("/router/any/{name:any}"), + show_route("/router/two/{value:int}/{value2:str}/"), ) diff --git a/tests/test_app/templates/view_to_component_script.html b/tests/test_app/templates/view_to_component_script.html index c9c5d263..735ed71c 100644 --- a/tests/test_app/templates/view_to_component_script.html +++ b/tests/test_app/templates/view_to_component_script.html @@ -6,8 +6,8 @@ {% block bottom %}