diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-latest-docs.yml similarity index 96% rename from .github/workflows/publish-release-docs.yml rename to .github/workflows/publish-latest-docs.yml index 93df3e2a..bc7409f0 100644 --- a/.github/workflows/publish-release-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -1,4 +1,4 @@ -name: Publish Release Docs +name: Publish Latest Docs on: release: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5342e003..4719dd02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,24 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), @@ -36,7 +19,13 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] -- Nothing (yet)! +### Added + +- `settings.py:REACTPY_ASYNC_RENDERING` to enable asynchronous rendering of components. + +### Changed + +- Bumped the minimum ReactPy version to `1.1.0`. ## [5.0.0] - 2024-10-22 diff --git a/README.md b/README.md index d3d2a1a9..89d1fb11 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ - [Customizable reconnection behavior](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#stability-settings) - [Customizable disconnection behavior](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag) - [Multiple root components](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag/) -- [Cross-process communication/signaling (Channel Layers)](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-channel-layer) +- [Cross-process communication/signaling](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-channel-layer) - [Django view to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#view-to-component) - [Django static file access](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-css) - [Django database access](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#use-query) diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index 943b76c0..7c60ca68 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -160,7 +160,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. - Requires manual intervention to change HTTP methods to anything other than `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. - - Has no option to automatically intercept local anchor link (such as `#!html `) click events. + - Has no option to automatically intercept click events from hyperlinks (such as `#!html `). ??? question "How do I use this for Class Based Views?" diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md index 28b84351..be6093c6 100644 --- a/docs/src/reference/router.md +++ b/docs/src/reference/router.md @@ -50,6 +50,6 @@ URL router that enables the ability to conditionally render other components bas | --- | --- | | `#!python VdomDict | None` | The matched component/path after it has been fully rendered. | -??? question "How is this different from `#!python reactpy_router.simple.router`?" +??? question "How is this different from `#!python reactpy_router.browser_router`?" - This component utilizes `reactpy-router` under the hood, but provides a more Django-like URL routing syntax. + The `django_router` component utilizes the same internals as `browser_router`, but provides a more Django-like URL routing syntax. diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index 3f35ee4d..e65dd203 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -87,7 +87,7 @@ This is useful to continuously update `#!python last_login` timestamps and refre Multiprocessing-safe database used by ReactPy for database-backed hooks and features. -If configuring this value, it is mandatory to enable our database router like such: +If configuring this value, it is mandatory to configure Django to use the ReactPy database router: === "settings.py" @@ -123,6 +123,18 @@ This setting is incompatible with [`daphne`](https://github.com/django/daphne). --- +### `#!python REACTPY_ASYNC_RENDERING` + +**Default:** `#!python False` + +**Example Value(s):** `#!python True` + +Configures whether to use an async ReactPy rendering queue. When enabled, large renders will no longer block smaller renders from taking place. Additionally, prevents the rendering queue from being blocked on waiting for async effects to startup/shutdown (which is typically a relatively slow operation). + +This setting is currently experimental, and currently no effort is made to de-duplicate renders. For example, if parent and child components are scheduled to render at the same time, both renders will take place even though a single render of the parent component would have been sufficient. + +--- + ### `#!python REACTPY_DEFAULT_HOSTS` **Default:** `#!python None` diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index cec6a9e1..61182ef9 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,6 +1,6 @@ channels >=4.0.0 django >=4.2.0 -reactpy >=1.0.2, <1.1.0 +reactpy >=1.1.0, <1.2.0 reactpy-router >=1.0.0, <2.0.0 dill >=0.3.5 orjson >=3.6.0 diff --git a/setup.py b/setup.py index a3388b35..f0c2f22d 100644 --- a/setup.py +++ b/setup.py @@ -11,8 +11,6 @@ from setuptools.command.develop import develop from setuptools.command.sdist import sdist -log = getLogger(__name__) - # ----------------------------------------------------------------------------- # Basic Constants # ----------------------------------------------------------------------------- @@ -22,6 +20,7 @@ js_dir = src_dir / "js" package_dir = src_dir / name static_dir = package_dir / "static" / name +log = getLogger(__name__) # ----------------------------------------------------------------------------- @@ -60,7 +59,10 @@ "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Widget Sets", + "Topic :: Software Development :: User Interfaces", "Environment :: Web Environment", + "Typing :: Typed", ], } diff --git a/src/js/bun.lockb b/src/js/bun.lockb index 3807b571..142d0d83 100644 Binary files a/src/js/bun.lockb and b/src/js/bun.lockb differ diff --git a/src/js/package.json b/src/js/package.json index 7f6cc019..cca215e6 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -12,7 +12,7 @@ "prettier": "^3.3.3" }, "dependencies": { - "@pyscript/core": "^0.5", + "@pyscript/core": "^0.6", "@reactpy/client": "^0.3.1", "morphdom": "^2.7.4" } diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index a891cb5d..e74299e3 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -7,6 +7,7 @@ from django.core.cache import DEFAULT_CACHE_ALIAS from django.db import DEFAULT_DB_ALIAS from django.views import View +from reactpy.config import REACTPY_ASYNC_RENDERING as _REACTPY_ASYNC_RENDERING from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE from reactpy.core.types import ComponentConstructor @@ -17,13 +18,16 @@ from reactpy_django.utils import import_dotted_path # Non-configurable values -_REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) REACTPY_DEBUG_MODE = _REACTPY_DEBUG_MODE.current REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {} REACTPY_FAILED_COMPONENTS: set[str] = set() REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {} # Configurable through Django settings.py +_REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) +_REACTPY_ASYNC_RENDERING.set_current( + getattr(settings, "REACTPY_ASYNC_RENDERING", _REACTPY_ASYNC_RENDERING.current) +) REACTPY_URL_PREFIX: str = getattr( settings, "REACTPY_URL_PREFIX", diff --git a/src/reactpy_django/decorators.py b/src/reactpy_django/decorators.py index 39a028a4..d5278f7d 100644 --- a/src/reactpy_django/decorators.py +++ b/src/reactpy_django/decorators.py @@ -13,8 +13,6 @@ from django.contrib.auth.models import AbstractUser - - def user_passes_test( test_func: Callable[[AbstractUser], bool], /, @@ -59,7 +57,5 @@ def _user_passes_test(component_constructor, fallback, test_func, *args, **kwarg # Render the component. return user_component - # Render the fallback component. - # Returns an empty string if fallback is None, since ReactPy currently renders None as a string. - # TODO: Remove this fallback when ReactPy can render None properly. - return fallback(*args, **kwargs) if callable(fallback) else (fallback or "") + # Render the fallback content. + return fallback(*args, **kwargs) if callable(fallback) else (fallback or None) diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index e13655c2..c226e1ca 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -150,7 +150,7 @@ async def execute_query() -> None: try: # Run the query if asyncio.iscoroutinefunction(query): - new_data = await query(**kwargs) + new_data = await query(**kwargs) # type: ignore[call-arg] else: new_data = await database_sync_to_async( query, thread_sensitive=thread_sensitive @@ -219,7 +219,7 @@ def use_mutation( ) -> Mutation[FuncParams]: """This hook is used to modify data in the background, typically to create/update/delete \ data from the Django ORM. - + Mutation functions can `return False` to prevent executing your `refetch` function. All \ other returns are ignored. Mutation functions can be sync or async. @@ -318,7 +318,7 @@ def use_user_data( save_default_data: bool = False, ) -> UserData: """Get or set user data stored within the REACTPY_DATABASE. - + Kwargs: default_data: A dictionary containing `{key: default_value}` pairs. \ For computationally intensive defaults, your `default_value` \ diff --git a/src/reactpy_django/http/views.py b/src/reactpy_django/http/views.py index 522d3dcf..780ccc17 100644 --- a/src/reactpy_django/http/views.py +++ b/src/reactpy_django/http/views.py @@ -37,6 +37,8 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: await caches[REACTPY_CACHE].aset( cache_key, file_contents, timeout=604800, version=int(last_modified_time) ) + + # TODO: Convert this to a StreamingHttpResponse return HttpResponse(file_contents, content_type="text/javascript") diff --git a/src/reactpy_django/pyscript/layout_handler.py b/src/reactpy_django/pyscript/layout_handler.py index 479cb613..f5a72fa9 100644 --- a/src/reactpy_django/pyscript/layout_handler.py +++ b/src/reactpy_django/pyscript/layout_handler.py @@ -107,7 +107,7 @@ def delete_old_workspaces(): if value.startswith("user_workspace_") } - # Delete the workspace if it exists at the moment when we check + # Delete any workspaces that are not being used for uuid in python_uuids - dom_uuids: task_name = f"task_{uuid}" if task_name in globals(): diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 401cf724..b86cabdc 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -263,7 +263,7 @@ def generate_obj_name(obj: Any) -> str: """Makes a best effort to create a name for an object. Useful for JSON serialization of Python objects.""" - # First attempt: Dunder methods + # First attempt: Create a dotted path by inspecting dunder methods if hasattr(obj, "__module__"): if hasattr(obj, "__name__"): return f"{obj.__module__}.{obj.__name__}"