From 4dc81d0a21cff2c5a6225173c68f56e4360ac577 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Tue, 7 May 2024 15:34:45 -0700 Subject: [PATCH 01/35] Python 3.12 compatibility (#236) Add compatibility for Python 3.12 and some misc code cleanup --- .github/workflows/test-src.yml | 2 +- CHANGELOG.md | 6 ++-- .../python/configure-asgi-middleware.py | 6 +--- docs/examples/python/use-channel-layer.py | 28 ++++++++----------- .../user-passes-test-component-fallback.py | 4 +-- .../python/user-passes-test-vdom-fallback.py | 4 +-- docs/examples/python/user-passes-test.py | 4 +-- docs/src/reference/hooks.md | 12 ++++---- docs/src/reference/management-commands.md | 2 +- docs/src/reference/settings.md | 5 ++-- pyproject.toml | 6 ++-- setup.py | 1 + src/reactpy_django/hooks.py | 8 +++--- src/reactpy_django/http/urls.py | 4 +-- src/reactpy_django/models.py | 4 ++- src/reactpy_django/router/resolvers.py | 2 +- src/reactpy_django/templatetags/reactpy.py | 3 +- src/reactpy_django/utils.py | 16 +++++++++++ tests/test_app/tests/test_components.py | 4 +-- tests/test_app/tests/test_regex.py | 12 ++++---- 20 files changed, 73 insertions(+), 60 deletions(-) diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index c450cf9f..328bd1c3 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 - name: Use Python ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dda476a..3b42d5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,9 @@ Using the following categories, list your changes in this order: ## [Unreleased] -- Nothing (yet)! +### Added + +- Python 3.12 compatibility ## [3.8.0] - 2024-02-20 @@ -50,7 +52,7 @@ Using the following categories, list your changes in this order: ### Changed -- Simplified code for cascading deletion of UserData. +- Simplified code for cascading deletion of user data. ## [3.7.0] - 2024-01-30 diff --git a/docs/examples/python/configure-asgi-middleware.py b/docs/examples/python/configure-asgi-middleware.py index 1895d651..6df35a39 100644 --- a/docs/examples/python/configure-asgi-middleware.py +++ b/docs/examples/python/configure-asgi-middleware.py @@ -11,10 +11,6 @@ application = ProtocolTypeRouter( { "http": django_asgi_app, - "websocket": AuthMiddlewareStack( - URLRouter( - [REACTPY_WEBSOCKET_ROUTE], - ) - ), + "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), } ) diff --git a/docs/examples/python/use-channel-layer.py b/docs/examples/python/use-channel-layer.py index 36e3a40b..83a66f19 100644 --- a/docs/examples/python/use-channel-layer.py +++ b/docs/examples/python/use-channel-layer.py @@ -3,26 +3,20 @@ @component -def my_sender_component(): - sender = use_channel_layer("my-channel-name") +def my_component(): + async def receive_message(message): + set_message(message["text"]) - async def submit_event(event): + async def send_message(event): if event["key"] == "Enter": await sender({"text": event["target"]["value"]}) - return html.div( - "Message Sender: ", - html.input({"type": "text", "onKeyDown": submit_event}), - ) - - -@component -def my_receiver_component(): message, set_message = hooks.use_state("") + sender = use_channel_layer("my-channel-name", receiver=receive_message) - async def receive_event(message): - set_message(message["text"]) - - use_channel_layer("my-channel-name", receiver=receive_event) - - return html.div(f"Message Receiver: {message}") + return html.div( + f"Received: {message}", + html.br(), + "Send: ", + html.input({"type": "text", "onKeyDown": send_message}), + ) diff --git a/docs/examples/python/user-passes-test-component-fallback.py b/docs/examples/python/user-passes-test-component-fallback.py index e92035f4..9fb71ea7 100644 --- a/docs/examples/python/user-passes-test-component-fallback.py +++ b/docs/examples/python/user-passes-test-component-fallback.py @@ -7,11 +7,11 @@ def my_component_fallback(): return html.div("I am NOT logged in!") -def auth_check(user): +def is_authenticated(user): return user.is_authenticated -@user_passes_test(auth_check, fallback=my_component_fallback) +@user_passes_test(is_authenticated, fallback=my_component_fallback) @component def my_component(): return html.div("I am logged in!") diff --git a/docs/examples/python/user-passes-test-vdom-fallback.py b/docs/examples/python/user-passes-test-vdom-fallback.py index 337b86f7..5d5c54f4 100644 --- a/docs/examples/python/user-passes-test-vdom-fallback.py +++ b/docs/examples/python/user-passes-test-vdom-fallback.py @@ -2,11 +2,11 @@ from reactpy_django.decorators import user_passes_test -def auth_check(user): +def is_authenticated(user): return user.is_authenticated -@user_passes_test(auth_check, fallback=html.div("I am NOT logged in!")) +@user_passes_test(is_authenticated, fallback=html.div("I am NOT logged in!")) @component def my_component(): return html.div("I am logged in!") diff --git a/docs/examples/python/user-passes-test.py b/docs/examples/python/user-passes-test.py index c43e55c5..201ad831 100644 --- a/docs/examples/python/user-passes-test.py +++ b/docs/examples/python/user-passes-test.py @@ -2,11 +2,11 @@ from reactpy_django.decorators import user_passes_test -def auth_check(user): +def is_authenticated(user): return user.is_authenticated -@user_passes_test(auth_check) +@user_passes_test(is_authenticated) @component def my_component(): return html.div("I am logged in!") diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 6d387466..b3ec7d6e 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -536,15 +536,15 @@ This is useful when used in combination with [`#!python use_channel_layer`](#use ??? example "See Interface" - **Parameters** + **Parameters** - `#!python None` + `#!python None` - **Returns** + **Returns** - | Type | Description | - | --- | --- | - | `#!python str` | A string containing the root component's `#!python id`. | + | Type | Description | + | --- | --- | + | `#!python str` | A string containing the root component's `#!python id`. | --- diff --git a/docs/src/reference/management-commands.md b/docs/src/reference/management-commands.md index 13a94e30..6e09e5a1 100644 --- a/docs/src/reference/management-commands.md +++ b/docs/src/reference/management-commands.md @@ -12,7 +12,7 @@ ReactPy exposes Django management commands that can be used to perform various R Command used to manually clean ReactPy data. -When using this command without arguments, it will perform all cleaning operations. You can specify only performing specific cleaning operations through arguments such as `--sessions`. +When using this command without arguments, it will perform all cleaning operations. You can limit cleaning to specific operations through arguments such as `--sessions`. !!! example "Terminal" diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index bba80402..4751f7ba 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -131,7 +131,7 @@ This setting is incompatible with [`daphne`](https://github.com/django/daphne). The default host(s) that can render your ReactPy components. -ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. +ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. This is typically useful for self-hosted applications. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) to manually override this default. @@ -147,9 +147,10 @@ Configures whether to pre-render your components via HTTP, which enables SEO com During pre-rendering, there are some key differences in behavior: -1. Only the component's first render is pre-rendered. +1. Only the component's first paint is pre-rendered. 2. All [`connection` hooks](https://reactive-python.github.io/reactpy-django/latest/reference/hooks/#connection-hooks) will provide HTTP variants. 3. The component will be non-interactive until a WebSocket connection is formed. +4. The component is re-rendered once a WebSocket connection is formed. diff --git a/pyproject.toml b/pyproject.toml index 2c8cb227..274a352e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,10 +10,12 @@ warn_redundant_casts = true warn_unused_ignores = true check_untyped_defs = true -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = ["src", "tests"] -[tool.ruff] +[tool.ruff.lint] ignore = ["E501"] + +[tool.ruff] extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", ".nox/*", "build/*"] line-length = 120 diff --git a/setup.py b/setup.py index 5a8f9e2d..b99d550b 100644 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Operating System :: OS Independent", "Intended Audience :: Developers", "Intended Audience :: Science/Research", diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index d2574751..8015a4ab 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -15,7 +15,7 @@ ) from uuid import uuid4 -import orjson as pickle +import orjson from channels import DEFAULT_CHANNEL_LAYER from channels.db import database_sync_to_async from channels.layers import InMemoryChannelLayer, get_channel_layer @@ -351,7 +351,7 @@ async def _set_user_data(data: dict): pk = get_pk(user) model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk) - model.data = pickle.dumps(data) + model.data = orjson.dumps(data) await model.asave() query: Query[dict | None] = use_query( @@ -471,7 +471,7 @@ async def _get_user_data( pk = get_pk(user) model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk) - data = pickle.loads(model.data) if model.data else {} + data = orjson.loads(model.data) if model.data else {} if not isinstance(data, dict): raise TypeError(f"Expected dict while loading user data, got {type(data)}") @@ -489,7 +489,7 @@ async def _get_user_data( data[key] = new_value changed = True if changed: - model.data = pickle.dumps(data) + model.data = orjson.dumps(data) if save_default_data: await model.asave() diff --git a/src/reactpy_django/http/urls.py b/src/reactpy_django/http/urls.py index 23bf4a7e..def755e4 100644 --- a/src/reactpy_django/http/urls.py +++ b/src/reactpy_django/http/urls.py @@ -7,12 +7,12 @@ urlpatterns = [ path( "web_module/", - views.web_modules_file, # type: ignore[arg-type] + views.web_modules_file, name="web_modules", ), path( "iframe/", - views.view_to_iframe, # type: ignore[arg-type] + views.view_to_iframe, name="view_to_iframe", ), ] diff --git a/src/reactpy_django/models.py b/src/reactpy_django/models.py index 180b0b31..5256fba6 100644 --- a/src/reactpy_django/models.py +++ b/src/reactpy_django/models.py @@ -3,6 +3,8 @@ from django.db.models.signals import pre_delete from django.dispatch import receiver +from reactpy_django.utils import get_pk + class ComponentSession(models.Model): """A model for storing component sessions.""" @@ -41,6 +43,6 @@ class UserDataModel(models.Model): @receiver(pre_delete, sender=get_user_model(), dispatch_uid="reactpy_delete_user_data") def delete_user_data(sender, instance, **kwargs): """Delete ReactPy's `UserDataModel` when a Django `User` is deleted.""" - pk = getattr(instance, instance._meta.pk.name) + pk = get_pk(instance) UserDataModel.objects.filter(user_pk=pk).delete() diff --git a/src/reactpy_django/router/resolvers.py b/src/reactpy_django/router/resolvers.py index 9732ba37..7c095081 100644 --- a/src/reactpy_django/router/resolvers.py +++ b/src/reactpy_django/router/resolvers.py @@ -52,7 +52,7 @@ def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]: pattern += f"{re.escape(path[last_match_end:])}$" # Replace literal `*` with "match anything" regex pattern, if it's at the end of the path - if pattern.endswith("\*$"): + if pattern.endswith(r"\*$"): pattern = f"{pattern[:-3]}.*$" return re.compile(pattern), converters diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 396c850d..d6d5ad5d 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -1,6 +1,5 @@ from __future__ import annotations -from distutils.util import strtobool from logging import getLogger from uuid import uuid4 @@ -22,7 +21,7 @@ OfflineComponentMissing, ) from reactpy_django.types import ComponentParams -from reactpy_django.utils import SyncLayout, validate_component_args +from reactpy_django.utils import SyncLayout, strtobool, validate_component_args try: RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/") diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 3ed0e2de..86dd64b4 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -366,3 +366,19 @@ def render(self): def get_pk(model): """Returns the value of the primary key for a Django model.""" return getattr(model, model._meta.pk.name) + + +def strtobool(val): + """Convert a string representation of truth to true (1) or false (0). + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return 1 + elif val in ("n", "no", "f", "false", "off", "0"): + return 0 + else: + raise ValueError("invalid truth value %r" % (val,)) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index c7a0d2fd..d92867cd 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -2,7 +2,6 @@ import os import socket import sys -from distutils.util import strtobool from functools import partial from time import sleep @@ -14,6 +13,7 @@ from django.test.utils import modify_settings from playwright.sync_api import TimeoutError, sync_playwright from reactpy_django.models import ComponentSession +from reactpy_django.utils import strtobool GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False") CLICK_DELAY = 250 if strtobool(GITHUB_ACTIONS) else 25 # Delay in miliseconds. @@ -628,7 +628,7 @@ def test_url_router(self): path.get_attribute("data-path"), ) string = new_page.query_selector("#router-string") - self.assertEquals("Path 12", string.text_content()) + self.assertEqual("Path 12", string.text_content()) finally: new_page.close() diff --git a/tests/test_app/tests/test_regex.py b/tests/test_app/tests/test_regex.py index 07a0dbfd..bf567413 100644 --- a/tests/test_app/tests/test_regex.py +++ b/tests/test_app/tests/test_regex.py @@ -99,25 +99,25 @@ def test_comment_regex(self): self.assertNotRegex(r'{% component "my.component" %}', COMMENT_REGEX) # Components surrounded by comments - self.assertEquals( + self.assertEqual( COMMENT_REGEX.sub( "", r'{% component "my.component" %} ' ).strip(), '{% component "my.component" %}', ) - self.assertEquals( + self.assertEqual( COMMENT_REGEX.sub( "", r' {% component "my.component" %}' ).strip(), '{% component "my.component" %}', ) - self.assertEquals( + self.assertEqual( COMMENT_REGEX.sub( "", r' {% component "my.component" %} ' ).strip(), '{% component "my.component" %}', ) - self.assertEquals( + self.assertEqual( COMMENT_REGEX.sub( "", r"""'), "", ) - self.assertEquals( + self.assertEqual( COMMENT_REGEX.sub( "", r""" -By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the `django_query_postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. +By default, automatic recursive fetching of `#!python ManyToMany` or `#!python ForeignKey` fields is enabled within the `#!python django_query_postprocessor`. This is needed to prevent `#!python SynchronousOnlyOperation` exceptions when accessing these fields within your ReactPy components. diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index b3ec7d6e..6d7da90f 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -44,20 +44,21 @@ Query functions can be sync or async. | Name | Type | Description | Default | | --- | --- | --- | --- | - | `#!python options` | `#!python QueryOptions | None` | An optional `#!python QueryOptions` object that can modify how the query is executed. | `#!python None` | - | `#!python query` | `#!python Callable[_Params, _Result | None]` | A callable that returns a Django `#!python Model` or `#!python QuerySet`. | N/A | - | `#!python *args` | `#!python _Params.args` | Positional arguments to pass into `#!python query`. | N/A | - | `#!python **kwargs` | `#!python _Params.kwargs` | Keyword arguments to pass into `#!python query`. | N/A | + | `#!python query` | `#!python Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred]` | A function that executes a query and returns some data. | N/A | + | `#!python kwargs` | `#!python dict[str, Any] | None` | Keyword arguments to passed into the `#!python query` function. | `#!python None` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run your query function in thread sensitive mode. This mode only applies to sync query functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python postprocessor` | `#!python AsyncPostprocessor | SyncPostprocessor | None` | A callable that processes the query `#!python data` before it is returned. The first argument of postprocessor function must be the query `#!python data`. All proceeding arguments are optional `#!python postprocessor_kwargs`. This postprocessor function must return the modified `#!python data`. | `#!python None` | + | `#!python postprocessor_kwargs` | `#!python dict[str, Any] | None` | Keyworded arguments passed into the `#!python postprocessor` function. | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `#!python Query[_Result | None]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. | + | `#!python Query[Inferred]` | An object containing `#!python loading`/`#!python error` states, your `#!python data` (if the query has successfully executed), and a `#!python refetch` callable that can be used to re-run the query. | ??? question "How can I provide arguments to my query function?" - `#!python *args` and `#!python **kwargs` can be provided to your query function via `#!python use_query` parameters. + `#!python kwargs` can be provided to your query function via the `#!python kwargs=...` parameter. === "components.py" @@ -67,15 +68,15 @@ Query functions can be sync or async. ??? question "How can I customize this hook's behavior?" - This hook accepts a `#!python options: QueryOptions` parameter that can be used to customize behavior. + This hook has several parameters that can be used to customize behavior. - Below are the settings that can be modified via these `#!python QueryOptions`. + Below are examples of values that can be modified. --- **`#!python thread_sensitive`** - Whether to run your synchronous query function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. + Whether to run your synchronous query function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. This setting only applies to sync query functions, and will be ignored for async functions. @@ -96,7 +97,7 @@ Query functions can be sync or async. 1. Want to use this hook to defer IO intensive tasks to be computed in the background 2. Want to to utilize `#!python use_query` with a different ORM - ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python QueryOptions.postprocessor` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior. + ... then you can either set a custom `#!python postprocessor`, or disable all postprocessing behavior by modifying the `#!python postprocessor=...` parameter. In the example below, we will set the `#!python postprocessor` to `#!python None` to disable postprocessing behavior. === "components.py" @@ -104,11 +105,7 @@ Query functions can be sync or async. {% include "../../examples/python/use-query-postprocessor-disable.py" %} ``` - If you wish to create a custom `#!python postprocessor`, you will need to create a callable. - - The first argument of `#!python postprocessor` must be the query `#!python data`. All proceeding arguments - are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` must return - the modified `#!python data`. + If you wish to create a custom `#!python postprocessor`, you will need to create a function where the first must be the query `#!python data`. All proceeding arguments are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` function must return the modified `#!python data`. === "components.py" @@ -124,7 +121,7 @@ Query functions can be sync or async. However, if you have deep nested trees of relational data, this may not be a desirable behavior. In these scenarios, you may prefer to manually fetch these relational fields using a second `#!python use_query` hook. - You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python QueryOptions.postprocessor_kwargs` parameter. + You can disable the prefetching behavior of the default `#!python postprocessor` (located at `#!python reactpy_django.utils.django_query_postprocessor`) via the `#!python postprocessor_kwargs=...` parameter. === "components.py" @@ -140,7 +137,7 @@ Query functions can be sync or async. ??? question "Can I make a failed query try again?" - Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance. + Yes, `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` instance. For example, take a look at `#!python reset_event` below. @@ -190,14 +187,15 @@ Mutation functions can be sync or async. | Name | Type | Description | Default | | --- | --- | --- | --- | - | `#!python mutation` | `#!python Callable[_Params, bool | None]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | - | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A query function (the function you provide to your `#!python use_query` hook) or a sequence of query functions that need a `refetch` if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | `#!python None` | + | `#!python mutation` | `#!python Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | + | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This mode only applies to sync mutation functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A query function (the function you provide to your `#!python use_query` hook) or a sequence of query functions that need a `#!python refetch` if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | `#!python None` | **Returns** | Type | Description | | --- | --- | - | `#!python Mutation[_Params]` | An object containing `#!python loading`/`#!python error` states, a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults, and a `#!python execute` callable that will run the query. | + | `#!python Mutation[FuncParams]` | An object containing `#!python loading`/`#!python error` states, and a `#!python reset` callable that will set `#!python loading`/`#!python error` states to defaults. This object can be called to run the query. | ??? question "How can I provide arguments to my mutation function?" @@ -211,15 +209,15 @@ Mutation functions can be sync or async. ??? question "How can I customize this hook's behavior?" - This hook accepts a `#!python options: MutationOptions` parameter that can be used to customize behavior. + This hook has several parameters that can be used to customize behavior. - Below are the settings that can be modified via these `#!python MutationOptions`. + Below are examples of values that can be modified. --- **`#!python thread_sensitive`** - Whether to run your synchronous mutation function in thread-sensitive mode. Thread-sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. + Whether to run your synchronous mutation function in thread sensitive mode. Thread sensitive mode is turned on by default due to Django ORM limitations. See Django's [`sync_to_async` docs](https://docs.djangoproject.com/en/dev/topics/async/#sync-to-async) docs for more information. This setting only applies to sync query functions, and will be ignored for async functions. @@ -235,7 +233,7 @@ Mutation functions can be sync or async. ??? question "Can I make a failed mutation try again?" - Yes, a `#!python use_mutation` can be re-performed by calling `#!python reset()` on your `#!python use_mutation` instance. + Yes, `#!python use_mutation` can be re-executed by calling `#!python reset()` on your `#!python use_mutation` instance. For example, take a look at `#!python reset_event` below. @@ -257,7 +255,7 @@ Mutation functions can be sync or async. The example below is a merge of the `#!python use_query` and `#!python use_mutation` examples above with the addition of a `#!python use_mutation(refetch=...)` argument. - Please note that `refetch` will cause all `#!python use_query` hooks that use `#!python get_items` in the current component tree will be refetched. + Please note that `#!python refetch` will cause all `#!python use_query` hooks that use `#!python get_items` in the current component tree will be refetched. === "components.py" diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index 8015a4ab..e13655c2 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -11,7 +11,6 @@ Sequence, Union, cast, - overload, ) from uuid import uuid4 @@ -29,16 +28,16 @@ from reactpy_django.types import ( AsyncMessageReceiver, AsyncMessageSender, + AsyncPostprocessor, ConnectionType, FuncParams, Inferred, Mutation, - MutationOptions, Query, - QueryOptions, + SyncPostprocessor, UserData, ) -from reactpy_django.utils import generate_obj_name, get_pk +from reactpy_django.utils import django_query_postprocessor, generate_obj_name, get_pk if TYPE_CHECKING: from channels_redis.core import RedisChannelLayer @@ -51,7 +50,6 @@ ) -# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.* def use_location() -> Location: """Get the current route as a `Location` object""" return _use_location() @@ -85,7 +83,6 @@ def use_origin() -> str | None: return None -# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.* def use_scope() -> dict[str, Any]: """Get the current ASGI scope dictionary""" scope = _use_scope() @@ -96,55 +93,55 @@ def use_scope() -> dict[str, Any]: raise TypeError(f"Expected scope to be a dict, got {type(scope)}") -# TODO: Deprecate this once the equivalent hook gets moved to reactpy.hooks.* def use_connection() -> ConnectionType: """Get the current `Connection` object""" return _use_connection() -@overload def use_query( - options: QueryOptions, - /, query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred], - *args: FuncParams.args, - **kwargs: FuncParams.kwargs, -) -> Query[Inferred]: ... - - -@overload -def use_query( - query: Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred], - *args: FuncParams.args, - **kwargs: FuncParams.kwargs, -) -> Query[Inferred]: ... - - -def use_query(*args, **kwargs) -> Query[Inferred]: + kwargs: dict[str, Any] | None = None, + *, + thread_sensitive: bool = True, + postprocessor: ( + AsyncPostprocessor | SyncPostprocessor | None + ) = django_query_postprocessor, + postprocessor_kwargs: dict[str, Any] | None = None, +) -> Query[Inferred]: """This hook is used to execute functions in the background and return the result, \ typically to read data the Django ORM. Args: - options: An optional `QueryOptions` object that can modify how the query is executed. - query: A callable that returns a Django `Model` or `QuerySet`. - *args: Positional arguments to pass into `query`. + query: A function that executes a query and returns some data. - Keyword Args: - **kwargs: Keyword arguments to pass into `query`.""" + Kwargs: + kwargs: Keyword arguments to passed into the `query` function. + thread_sensitive: Whether to run the query in thread sensitive mode. \ + This mode only applies to sync query functions, and is turned on by default \ + due to Django ORM limitations. + postprocessor: A callable that processes the query `data` before it is returned. \ + The first argument of postprocessor function must be the query `data`. All \ + proceeding arguments are optional `postprocessor_kwargs`. This postprocessor \ + function must return the modified `data`. \ + \ + If unset, `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` is used. By default, this \ + is used to prevent Django's lazy query execution and supports `many_to_many` \ + and `many_to_one` as `postprocessor_kwargs`. + postprocessor_kwargs: Keyworded arguments passed into the `postprocessor` function. + + Returns: + An object containing `loading`/`#!python error` states, your `data` (if the query \ + has successfully executed), and a `refetch` callable that can be used to re-run the query. + """ should_execute, set_should_execute = use_state(True) data, set_data = use_state(cast(Inferred, None)) loading, set_loading = use_state(True) error, set_error = use_state(cast(Union[Exception, None], None)) - if isinstance(args[0], QueryOptions): - query_options, query, query_args, query_kwargs = _use_query_args_1( - *args, **kwargs - ) - else: - query_options, query, query_args, query_kwargs = _use_query_args_2( - *args, **kwargs - ) query_ref = use_ref(query) + kwargs = kwargs or {} + postprocessor_kwargs = postprocessor_kwargs or {} + if query_ref.current is not query: raise ValueError(f"Query function changed from {query_ref.current} to {query}.") @@ -153,31 +150,27 @@ async def execute_query() -> None: try: # Run the query if asyncio.iscoroutinefunction(query): - new_data = await query(*query_args, **query_kwargs) + new_data = await query(**kwargs) else: new_data = await database_sync_to_async( - query, - thread_sensitive=query_options.thread_sensitive, - )(*query_args, **query_kwargs) + query, thread_sensitive=thread_sensitive + )(**kwargs) # Run the postprocessor - if query_options.postprocessor: - if asyncio.iscoroutinefunction(query_options.postprocessor): - new_data = await query_options.postprocessor( - new_data, **query_options.postprocessor_kwargs - ) + if postprocessor: + if asyncio.iscoroutinefunction(postprocessor): + new_data = await postprocessor(new_data, **postprocessor_kwargs) else: new_data = await database_sync_to_async( - query_options.postprocessor, - thread_sensitive=query_options.thread_sensitive, - )(new_data, **query_options.postprocessor_kwargs) + postprocessor, thread_sensitive=thread_sensitive + )(new_data, **postprocessor_kwargs) # Log any errors and set the error state except Exception as e: set_data(cast(Inferred, None)) set_loading(False) set_error(e) - _logger.exception(f"Failed to execute query: {generate_obj_name(query)}") + _logger.exception("Failed to execute query: %s", generate_obj_name(query)) return # Query was successful @@ -212,30 +205,18 @@ def register_refetch_callback() -> Callable[[], None]: _REFETCH_CALLBACKS[query].add(refetch) return lambda: _REFETCH_CALLBACKS[query].remove(refetch) - # The query's user API + # Return Query user API return Query(data, loading, error, refetch) -@overload -def use_mutation( - options: MutationOptions, - mutation: ( - Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]] - ), - refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, -) -> Mutation[FuncParams]: ... - - -@overload def use_mutation( mutation: ( Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]] ), + *, + thread_sensitive: bool = True, refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, -) -> Mutation[FuncParams]: ... - - -def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]: +) -> Mutation[FuncParams]: """This hook is used to modify data in the background, typically to create/update/delete \ data from the Django ORM. @@ -246,18 +227,24 @@ def use_mutation(*args: Any, **kwargs: Any) -> Mutation[FuncParams]: mutation: A callable that performs Django ORM create, update, or delete \ functionality. If this function returns `False`, then your `refetch` \ function will not be used. + + Kwargs: + thread_sensitive: Whether to run the mutation in thread sensitive mode. \ + This mode only applies to sync mutation functions, and is turned on by default \ + due to Django ORM limitations. refetch: A query function (the function you provide to your `use_query` \ hook) or a sequence of query functions that need a `refetch` if the \ mutation succeeds. This is useful for refreshing data after a mutation \ has been performed. + + Returns: + An object containing `#!python loading`/`#!python error` states, and a \ + `#!python reset` callable that will set `#!python loading`/`#!python error` \ + states to defaults. This object can be called to run the query. """ loading, set_loading = use_state(False) error, set_error = use_state(cast(Union[Exception, None], None)) - if isinstance(args[0], MutationOptions): - mutation_options, mutation, refetch = _use_mutation_args_1(*args, **kwargs) - else: - mutation_options, mutation, refetch = _use_mutation_args_2(*args, **kwargs) # The main "running" function for `use_mutation` async def execute_mutation(exec_args, exec_kwargs) -> None: @@ -267,7 +254,7 @@ async def execute_mutation(exec_args, exec_kwargs) -> None: should_refetch = await mutation(*exec_args, **exec_kwargs) else: should_refetch = await database_sync_to_async( - mutation, thread_sensitive=mutation_options.thread_sensitive + mutation, thread_sensitive=thread_sensitive )(*exec_args, **exec_kwargs) # Log any errors and set the error state @@ -275,7 +262,7 @@ async def execute_mutation(exec_args, exec_kwargs) -> None: set_loading(False) set_error(e) _logger.exception( - f"Failed to execute mutation: {generate_obj_name(mutation)}" + "Failed to execute mutation: %s", generate_obj_name(mutation) ) # Mutation was successful @@ -311,7 +298,7 @@ def reset() -> None: set_loading(False) set_error(None) - # The mutation's user API + # Return mutation user API return Mutation(schedule_mutation, loading, error, reset) @@ -355,11 +342,13 @@ async def _set_user_data(data: dict): await model.asave() query: Query[dict | None] = use_query( - QueryOptions(postprocessor=None), _get_user_data, - user=user, - default_data=default_data, - save_default_data=save_default_data, + kwargs={ + "user": user, + "default_data": default_data, + "save_default_data": save_default_data, + }, + postprocessor=None, ) mutation = use_mutation(_set_user_data, refetch=_get_user_data) @@ -444,22 +433,6 @@ def use_root_id() -> str: return scope["reactpy"]["id"] -def _use_query_args_1(options: QueryOptions, /, query: Query, *args, **kwargs): - return options, query, args, kwargs - - -def _use_query_args_2(query: Query, *args, **kwargs): - return QueryOptions(), query, args, kwargs - - -def _use_mutation_args_1(options: MutationOptions, mutation: Mutation, refetch=None): - return options, mutation, refetch - - -def _use_mutation_args_2(mutation, refetch=None): - return MutationOptions(), mutation, refetch - - async def _get_user_data( user: AbstractUser, default_data: None | dict, save_default_data: bool ) -> dict | None: diff --git a/src/reactpy_django/types.py b/src/reactpy_django/types.py index 8efda72f..91ffc319 100644 --- a/src/reactpy_django/types.py +++ b/src/reactpy_django/types.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import ( TYPE_CHECKING, Any, @@ -52,49 +52,11 @@ def __call__(self, *args: FuncParams.args, **kwargs: FuncParams.kwargs) -> None: class AsyncPostprocessor(Protocol): - async def __call__(self, data: Any) -> Any: - ... + async def __call__(self, data: Any) -> Any: ... class SyncPostprocessor(Protocol): - def __call__(self, data: Any) -> Any: - ... - - -@dataclass -class QueryOptions: - """Configuration options that can be provided to `use_query`.""" - - from reactpy_django.config import REACTPY_DEFAULT_QUERY_POSTPROCESSOR - - postprocessor: AsyncPostprocessor | SyncPostprocessor | None = ( - REACTPY_DEFAULT_QUERY_POSTPROCESSOR - ) - """A callable that can modify the query `data` after the query has been executed. - - The first argument of postprocessor must be the query `data`. All proceeding arguments - are optional `postprocessor_kwargs` (see below). This postprocessor function must return - the modified `data`. - - If unset, REACTPY_DEFAULT_QUERY_POSTPROCESSOR is used. - - ReactPy's default django_query_postprocessor prevents Django's lazy query execution, and - additionally can be configured via `postprocessor_kwargs` to recursively fetch - `many_to_many` and `many_to_one` fields.""" - - postprocessor_kwargs: MutableMapping[str, Any] = field(default_factory=lambda: {}) - """Keyworded arguments directly passed into the `postprocessor` for configuration.""" - - thread_sensitive: bool = True - """Whether to run the query in thread-sensitive mode. This setting only applies to sync query functions.""" - - -@dataclass -class MutationOptions: - """Configuration options that can be provided to `use_mutation`.""" - - thread_sensitive: bool = True - """Whether to run the mutation in thread-sensitive mode. This setting only applies to sync mutation functions.""" + def __call__(self, data: Any) -> Any: ... @dataclass @@ -112,10 +74,8 @@ class UserData(NamedTuple): class AsyncMessageReceiver(Protocol): - async def __call__(self, message: dict) -> None: - ... + async def __call__(self, message: dict) -> None: ... class AsyncMessageSender(Protocol): - async def __call__(self, message: dict) -> None: - ... + async def __call__(self, message: dict) -> None: ... diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 457caf47..73538ad7 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -29,19 +29,19 @@ ) _logger = logging.getLogger(__name__) -_component_tag = r"(?Pcomponent)" -_component_path = r"""(?P"[^"'\s]+"|'[^"'\s]+')""" -_component_offline_kwarg = ( - rf"""(\s*offline\s*=\s*{_component_path.replace(r"", r"")})""" +_TAG_PATTERN = r"(?Pcomponent)" +_PATH_PATTERN = r"""(?P"[^"'\s]+"|'[^"'\s]+')""" +_OFFLINE_KWARG_PATTERN = ( + rf"""(\s*offline\s*=\s*{_PATH_PATTERN.replace(r"", r"")})""" ) -_component_generic_kwarg = r"""(\s*.*?)""" +_GENERIC_KWARG_PATTERN = r"""(\s*.*?)""" COMMENT_REGEX = re.compile(r"") COMPONENT_REGEX = re.compile( r"{%\s*" - + _component_tag + + _TAG_PATTERN + r"\s*" - + _component_path - + rf"({_component_offline_kwarg}|{_component_generic_kwarg})*?" + + _PATH_PATTERN + + rf"({_OFFLINE_KWARG_PATTERN}|{_GENERIC_KWARG_PATTERN})*?" + r"\s*%}" ) @@ -262,7 +262,7 @@ def django_query_postprocessor( ) -> QuerySet | Model: """Recursively fetch all fields within a `Model` or `QuerySet` to ensure they are not performed lazily. - Behaviors can be modified through `QueryOptions` within your `use_query` hook. + Behavior can be modified through `postprocessor_kwargs` within your `use_query` hook. Args: data: The `Model` or `QuerySet` to recursively fetch fields from. @@ -275,8 +275,7 @@ def django_query_postprocessor( The `Model` or `QuerySet` with all fields fetched. """ - # `QuerySet`, which is an iterable of `Model`/`QuerySet` instances - # https://github.com/typeddjango/django-stubs/issues/704 + # `QuerySet`, which is an iterable containing `Model`/`QuerySet` objects. if isinstance(data, QuerySet): for model in data: django_query_postprocessor( @@ -314,7 +313,7 @@ def django_query_postprocessor( "One of the following may have occurred:\n" " - You are using a non-Django ORM.\n" " - You are attempting to use `use_query` to fetch non-ORM data.\n\n" - "If these situations seem correct, you may want to consider disabling the postprocessor via `QueryOptions`." + "If these situations seem correct, you may want to consider disabling the postprocessor." ) return data @@ -381,4 +380,4 @@ def strtobool(val): elif val in ("n", "no", "f", "false", "off", "0"): return 0 else: - raise ValueError("invalid truth value %r" % (val,)) + raise ValueError(f"invalid truth value {val}") diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 0d4c62d1..345f399e 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -29,6 +29,8 @@ if TYPE_CHECKING: from django.contrib.auth.models import AbstractUser + from reactpy_django import models + _logger = logging.getLogger(__name__) BACKHAUL_LOOP = asyncio.new_event_loop() @@ -47,9 +49,18 @@ def start_backhaul_loop(): class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): """Communicates with the browser to perform actions on-demand.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # New WebsocketConsumer attributes created by ReactPy + self.dispatcher: Future | asyncio.Task + self.threaded: bool + self.recv_queue: asyncio.Queue + self.dotted_path: str + self.component_session: "models.ComponentSession" | None = None + async def connect(self) -> None: """The browser has connected.""" - from reactpy_django import models from reactpy_django.config import ( REACTPY_AUTH_BACKEND, REACTPY_AUTO_RELOGIN, @@ -79,9 +90,7 @@ async def connect(self) -> None: ) # Start the component dispatcher - self.dispatcher: Future | asyncio.Task self.threaded = REACTPY_BACKHAUL_THREAD - self.component_session: models.ComponentSession | None = None if self.threaded: if not BACKHAUL_THREAD.is_alive(): await asyncio.to_thread( @@ -149,14 +158,14 @@ async def run_dispatcher(self): ) scope = self.scope - self.dotted_path = dotted_path = scope["url_route"]["kwargs"]["dotted_path"] + self.dotted_path = scope["url_route"]["kwargs"]["dotted_path"] uuid = scope["url_route"]["kwargs"].get("uuid") has_args = scope["url_route"]["kwargs"].get("has_args") scope["reactpy"] = {"id": str(uuid)} query_string = parse_qs(scope["query_string"].decode(), strict_parsing=True) http_pathname = query_string.get("http_pathname", [""])[0] http_search = query_string.get("http_search", [""])[0] - self.recv_queue: asyncio.Queue = asyncio.Queue() + self.recv_queue = asyncio.Queue() connection = Connection( # For `use_connection` scope=scope, location=Location(pathname=http_pathname, search=http_search), @@ -168,11 +177,11 @@ async def run_dispatcher(self): # Verify the component has already been registered try: - root_component_constructor = REACTPY_REGISTERED_COMPONENTS[dotted_path] + root_component_constructor = REACTPY_REGISTERED_COMPONENTS[self.dotted_path] except KeyError: await asyncio.to_thread( _logger.warning, - f"Attempt to access invalid ReactPy component: {dotted_path!r}", + f"Attempt to access invalid ReactPy component: {self.dotted_path!r}", ) return @@ -194,7 +203,7 @@ async def run_dispatcher(self): except models.ComponentSession.DoesNotExist: await asyncio.to_thread( _logger.warning, - f"Component session for '{dotted_path}:{uuid}' not found. The " + f"Component session for '{self.dotted_path}:{uuid}' not found. The " "session may have already expired beyond REACTPY_SESSION_MAX_AGE. " "If you are using a custom `host`, you may have forgotten to provide " "args/kwargs.", diff --git a/tests/test_app/components.py b/tests/test_app/components.py index dbe9bd8f..fe6df2f0 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -10,7 +10,6 @@ from django.shortcuts import render from reactpy import component, hooks, html, web from reactpy_django.components import view_to_component, view_to_iframe -from reactpy_django.types import QueryOptions from test_app.models import ( AsyncForiegnChild, @@ -659,7 +658,7 @@ def custom_host(number=0): @component def broken_postprocessor_query(): relational_parent = reactpy_django.hooks.use_query( - QueryOptions(postprocessor=None), get_relational_parent_query + get_relational_parent_query, postprocessor=None ) if not relational_parent.data: @@ -720,9 +719,9 @@ async def on_submit(event): "data-fetch-error": bool(user_data_query.error), "data-mutation-error": bool(user_data_mutation.error), "data-loading": user_data_query.loading or user_data_mutation.loading, - "data-username": "AnonymousUser" - if current_user.is_anonymous - else current_user.username, + "data-username": ( + "AnonymousUser" if current_user.is_anonymous else current_user.username + ), }, html.div("use_user_data"), html.button({"class": "login-1", "on_click": login_user1}, "Login 1"), @@ -788,9 +787,9 @@ async def on_submit(event): "data-fetch-error": bool(user_data_query.error), "data-mutation-error": bool(user_data_mutation.error), "data-loading": user_data_query.loading or user_data_mutation.loading, - "data-username": "AnonymousUser" - if current_user.is_anonymous - else current_user.username, + "data-username": ( + "AnonymousUser" if current_user.is_anonymous else current_user.username + ), }, html.div("use_user_data_with_default"), html.button({"class": "login-3", "on_click": login_user3}, "Login 3"), From fb6c25837c272413464991aad41ed30e8d48fcc8 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sat, 22 Jun 2024 04:42:56 -0700 Subject: [PATCH 06/35] Client-side Python components using PyScript (#243) - Client-side Python components can now be rendered via the new `{% pyscript_component %}` template tag - You must first call the `{% pyscript_setup %}` template tag to load PyScript dependencies - Client-side components can be embedded into existing server-side components via `reactpy_django.components.pyscript_component`. - Tired of writing JavaScript? You can now write PyScript code that runs directly within client browser via the `reactpy_django.html.pyscript` element. - This is a viable substitution for most JavaScript code. --- .gitignore | 4 +- CHANGELOG.md | 18 +- README.md | 5 +- docs/examples/html/pyscript-component.html | 14 + .../html/pyscript-initial-object.html | 3 + .../html/pyscript-initial-string.html | 3 + docs/examples/html/pyscript-js-module.html | 14 + .../html/pyscript-multiple-files.html | 15 + docs/examples/html/pyscript-root.html | 3 + .../html/pyscript-setup-config-object.html | 4 + .../html/pyscript-setup-config-string.html | 4 + .../html/pyscript-setup-dependencies.html | 4 + .../html/pyscript-setup-extra-js-object.html | 14 + .../html/pyscript-setup-extra-js-string.html | 14 + docs/examples/html/pyscript-setup.html | 6 + docs/examples/html/pyscript-ssr-parent.html | 14 + docs/examples/html/pyscript-tag.html | 14 + docs/examples/python/example/views.py | 2 +- .../pyscript-component-initial-object.py | 12 + .../pyscript-component-initial-string.py | 12 + .../pyscript-component-multiple-files-root.py | 12 + .../python/pyscript-component-root.py | 12 + docs/examples/python/pyscript-hello-world.py | 6 + .../python/pyscript-initial-object.py | 10 + docs/examples/python/pyscript-js-execution.py | 11 + docs/examples/python/pyscript-js-module.py | 12 + .../python/pyscript-multiple-files-child.py | 6 + .../python/pyscript-multiple-files-root.py | 11 + docs/examples/python/pyscript-root.py | 6 + .../python/pyscript-setup-config-object.py | 9 + .../python/pyscript-setup-extra-js-object.py | 10 + docs/examples/python/pyscript-ssr-child.py | 6 + docs/examples/python/pyscript-ssr-parent.py | 10 + docs/examples/python/pyscript-tag.py | 15 + docs/examples/python/template-tag-bad-view.py | 7 +- docs/mkdocs.yml | 1 + docs/src/assets/css/admonition.css | 137 +- docs/src/dictionary.txt | 2 + .../learn/add-reactpy-to-a-django-project.md | 2 +- docs/src/learn/your-first-component.md | 6 +- docs/src/reference/components.md | 108 + docs/src/reference/html.md | 31 + docs/src/reference/router.md | 7 + docs/src/reference/template-tag.md | 250 +- docs/src/reference/utils.md | 2 +- setup.py | 24 +- src/js/package-lock.json | 140 +- src/js/package.json | 4 +- src/reactpy_django/__init__.py | 12 +- src/reactpy_django/components.py | 67 +- src/reactpy_django/config.py | 5 +- src/reactpy_django/html.py | 3 + .../management/commands/clean_reactpy.py | 4 +- src/reactpy_django/pyscript/__init__.py | 0 .../pyscript/component_template.py | 26 + src/reactpy_django/pyscript/layout_handler.py | 138 + .../static/reactpy_django/pyscript-custom.css | 3 + .../reactpy_django/pyscript-hide-debug.css | 3 + .../templates/reactpy/pyscript_component.html | 2 + .../templates/reactpy/pyscript_setup.html | 8 + src/reactpy_django/templatetags/reactpy.py | 151 +- src/reactpy_django/utils.py | 181 +- tests/test_app/__init__.py | 31 + tests/test_app/components.py | 12 +- tests/test_app/pyscript/__init__.py | 0 .../test_app/pyscript/components/__init__.py | 0 tests/test_app/pyscript/components/child.py | 25 + tests/test_app/pyscript/components/counter.py | 24 + .../pyscript/components/custom_root.py | 6 + .../pyscript/components/hello_world.py | 6 + .../pyscript/components/multifile_child.py | 6 + .../pyscript/components/multifile_parent.py | 11 + .../pyscript/components/remote_js_module.py | 14 + .../pyscript/components/server_side.py | 33 + tests/test_app/pyscript/urls.py | 7 + tests/test_app/pyscript/views.py | 5 + tests/test_app/static/moment.js | 5680 +++++++++++++++++ tests/test_app/templates/pyscript.html | 33 + tests/test_app/tests/test_components.py | 44 + tests/test_app/urls.py | 2 + 80 files changed, 7395 insertions(+), 183 deletions(-) create mode 100644 docs/examples/html/pyscript-component.html create mode 100644 docs/examples/html/pyscript-initial-object.html create mode 100644 docs/examples/html/pyscript-initial-string.html create mode 100644 docs/examples/html/pyscript-js-module.html create mode 100644 docs/examples/html/pyscript-multiple-files.html create mode 100644 docs/examples/html/pyscript-root.html create mode 100644 docs/examples/html/pyscript-setup-config-object.html create mode 100644 docs/examples/html/pyscript-setup-config-string.html create mode 100644 docs/examples/html/pyscript-setup-dependencies.html create mode 100644 docs/examples/html/pyscript-setup-extra-js-object.html create mode 100644 docs/examples/html/pyscript-setup-extra-js-string.html create mode 100644 docs/examples/html/pyscript-setup.html create mode 100644 docs/examples/html/pyscript-ssr-parent.html create mode 100644 docs/examples/html/pyscript-tag.html create mode 100644 docs/examples/python/pyscript-component-initial-object.py create mode 100644 docs/examples/python/pyscript-component-initial-string.py create mode 100644 docs/examples/python/pyscript-component-multiple-files-root.py create mode 100644 docs/examples/python/pyscript-component-root.py create mode 100644 docs/examples/python/pyscript-hello-world.py create mode 100644 docs/examples/python/pyscript-initial-object.py create mode 100644 docs/examples/python/pyscript-js-execution.py create mode 100644 docs/examples/python/pyscript-js-module.py create mode 100644 docs/examples/python/pyscript-multiple-files-child.py create mode 100644 docs/examples/python/pyscript-multiple-files-root.py create mode 100644 docs/examples/python/pyscript-root.py create mode 100644 docs/examples/python/pyscript-setup-config-object.py create mode 100644 docs/examples/python/pyscript-setup-extra-js-object.py create mode 100644 docs/examples/python/pyscript-ssr-child.py create mode 100644 docs/examples/python/pyscript-ssr-parent.py create mode 100644 docs/examples/python/pyscript-tag.py create mode 100644 docs/src/reference/html.md create mode 100644 src/reactpy_django/html.py create mode 100644 src/reactpy_django/pyscript/__init__.py create mode 100644 src/reactpy_django/pyscript/component_template.py create mode 100644 src/reactpy_django/pyscript/layout_handler.py create mode 100644 src/reactpy_django/static/reactpy_django/pyscript-custom.css create mode 100644 src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css create mode 100644 src/reactpy_django/templates/reactpy/pyscript_component.html create mode 100644 src/reactpy_django/templates/reactpy/pyscript_setup.html create mode 100644 tests/test_app/pyscript/__init__.py create mode 100644 tests/test_app/pyscript/components/__init__.py create mode 100644 tests/test_app/pyscript/components/child.py create mode 100644 tests/test_app/pyscript/components/counter.py create mode 100644 tests/test_app/pyscript/components/custom_root.py create mode 100644 tests/test_app/pyscript/components/hello_world.py create mode 100644 tests/test_app/pyscript/components/multifile_child.py create mode 100644 tests/test_app/pyscript/components/multifile_parent.py create mode 100644 tests/test_app/pyscript/components/remote_js_module.py create mode 100644 tests/test_app/pyscript/components/server_side.py create mode 100644 tests/test_app/pyscript/urls.py create mode 100644 tests/test_app/pyscript/views.py create mode 100644 tests/test_app/static/moment.js create mode 100644 tests/test_app/templates/pyscript.html diff --git a/.gitignore b/.gitignore index 07d4c0cd..ffabb7fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # ReactPy-Django Build Artifacts -src/reactpy_django/static/* +src/reactpy_django/static/reactpy_django/client.js +src/reactpy_django/static/reactpy_django/pyscript +src/reactpy_django/static/reactpy_django/morphdom # Django # logs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9b047f..1e1e5b29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,13 +34,21 @@ Using the following categories, list your changes in this order: ## [Unreleased] +### Added + +- Client-side Python components can now be rendered via the new `{% pyscript_component %}` template tag + - You must first call the `{% pyscript_setup %}` template tag to load PyScript dependencies +- Client-side components can be embedded into existing server-side components via `reactpy_django.components.pyscript_component`. +- Tired of writing JavaScript? You can now write PyScript code that runs directly within client browser via the `reactpy_django.html.pyscript` element. + - This is a viable substitution for most JavaScript code. + ### Changed - New syntax for `use_query` and `use_mutation` hooks. Here's a quick comparison of the changes: ```python - query = use_query(QueryOptions(thread_sensitive=True), get_items, value=123456, foo="bar") # Old - query = use_query(get_items, {"value":12356, "foo":"bar"}, thread_sensitive=True) # New + query = use_query(QueryOptions(thread_sensitive=True), get_items, foo="bar") # Old + query = use_query(get_items, {"foo":"bar"}, thread_sensitive=True) # New mutation = use_mutation(MutationOptions(thread_sensitive=True), remove_item) # Old mutation = use_mutation(remove_item, thread_sensitive=True) # New @@ -48,7 +56,11 @@ Using the following categories, list your changes in this order: ### Removed -- `QueryOptions` and `MutationOptions` have been removed. Their values are now passed direct into the hook. +- `QueryOptions` and `MutationOptions` have been removed. The value contained within these objects are now passed directly into the hook. + +### Fixed + +- Resolved a bug where Django-ReactPy would not properly detect `settings.py:DEBUG`. ## [3.8.1] - 2024-05-07 diff --git a/README.md b/README.md index ce11472f..817e684b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ReactPy Django +# ReactPy-Django

@@ -21,6 +21,7 @@ [ReactPy-Django](https://github.com/reactive-python/reactpy-django) is used to add [ReactPy](https://reactpy.dev/) support to an existing **Django project**. This package also turbocharges ReactPy with features such as... - [SEO compatible rendering](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#reactpy_prerender) +- [Client-Side Python components](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag/#pyscript-component) - [Single page application (SPA) capabilities](https://reactive-python.github.io/reactpy-django/latest/reference/router/#django-router) - [Distributed computing](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#reactpy_default_hosts) - [Performance enhancements](https://reactive-python.github.io/reactpy-django/latest/reference/settings/#performance-settings) @@ -82,7 +83,7 @@ def hello_world(recipient: str): -## [`my_app/templates/my-template.html`](https://docs.djangoproject.com/en/dev/topics/templates/) +## [`my_app/templates/my_template.html`](https://docs.djangoproject.com/en/dev/topics/templates/) diff --git a/docs/examples/html/pyscript-component.html b/docs/examples/html/pyscript-component.html new file mode 100644 index 00000000..3f21e3fa --- /dev/null +++ b/docs/examples/html/pyscript-component.html @@ -0,0 +1,14 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup %} + + + + {% pyscript_component "./example_project/my_app/components/hello_world.py" %} + + + diff --git a/docs/examples/html/pyscript-initial-object.html b/docs/examples/html/pyscript-initial-object.html new file mode 100644 index 00000000..0e0a35c3 --- /dev/null +++ b/docs/examples/html/pyscript-initial-object.html @@ -0,0 +1,3 @@ + + {% pyscript_component "./example_project/my_app/components/root.py" initial=my_initial_object %} + diff --git a/docs/examples/html/pyscript-initial-string.html b/docs/examples/html/pyscript-initial-string.html new file mode 100644 index 00000000..8e062d6a --- /dev/null +++ b/docs/examples/html/pyscript-initial-string.html @@ -0,0 +1,3 @@ + + {% pyscript_component "./example_project/my_app/components/root.py" initial="

" %} + diff --git a/docs/examples/html/pyscript-js-module.html b/docs/examples/html/pyscript-js-module.html new file mode 100644 index 00000000..2d0130fb --- /dev/null +++ b/docs/examples/html/pyscript-js-module.html @@ -0,0 +1,14 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup extra_js='{"/static/moment.js":"moment"}' %} + + + + {% component "example_project.my_app.components.root.py" %} + + + diff --git a/docs/examples/html/pyscript-multiple-files.html b/docs/examples/html/pyscript-multiple-files.html new file mode 100644 index 00000000..1f9267a8 --- /dev/null +++ b/docs/examples/html/pyscript-multiple-files.html @@ -0,0 +1,15 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup %} + + + + {% pyscript_component "./example_project/my_app/components/root.py" + "./example_project/my_app/components/child.py" %} + + + diff --git a/docs/examples/html/pyscript-root.html b/docs/examples/html/pyscript-root.html new file mode 100644 index 00000000..e89a5369 --- /dev/null +++ b/docs/examples/html/pyscript-root.html @@ -0,0 +1,3 @@ + + {% pyscript_component "./example_project/my_app/components/main.py" root="main" %} + diff --git a/docs/examples/html/pyscript-setup-config-object.html b/docs/examples/html/pyscript-setup-config-object.html new file mode 100644 index 00000000..70b408b1 --- /dev/null +++ b/docs/examples/html/pyscript-setup-config-object.html @@ -0,0 +1,4 @@ + + ReactPy + {% pyscript_setup config=my_config_object %} + diff --git a/docs/examples/html/pyscript-setup-config-string.html b/docs/examples/html/pyscript-setup-config-string.html new file mode 100644 index 00000000..842bb769 --- /dev/null +++ b/docs/examples/html/pyscript-setup-config-string.html @@ -0,0 +1,4 @@ + + ReactPy + {% pyscript_setup config='{"experimental_create_proxy":"auto"}' %} + diff --git a/docs/examples/html/pyscript-setup-dependencies.html b/docs/examples/html/pyscript-setup-dependencies.html new file mode 100644 index 00000000..f982b8fb --- /dev/null +++ b/docs/examples/html/pyscript-setup-dependencies.html @@ -0,0 +1,4 @@ + + ReactPy + {% pyscript_setup "dill==0.3.5" "markdown<=3.6.0" "nest_asyncio" "titlecase" %} + diff --git a/docs/examples/html/pyscript-setup-extra-js-object.html b/docs/examples/html/pyscript-setup-extra-js-object.html new file mode 100644 index 00000000..815cb040 --- /dev/null +++ b/docs/examples/html/pyscript-setup-extra-js-object.html @@ -0,0 +1,14 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup extra_js=my_extra_js_object %} + + + + {% component "example_project.my_app.components.root.py" %} + + + diff --git a/docs/examples/html/pyscript-setup-extra-js-string.html b/docs/examples/html/pyscript-setup-extra-js-string.html new file mode 100644 index 00000000..2d0130fb --- /dev/null +++ b/docs/examples/html/pyscript-setup-extra-js-string.html @@ -0,0 +1,14 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup extra_js='{"/static/moment.js":"moment"}' %} + + + + {% component "example_project.my_app.components.root.py" %} + + + diff --git a/docs/examples/html/pyscript-setup.html b/docs/examples/html/pyscript-setup.html new file mode 100644 index 00000000..20bb2b09 --- /dev/null +++ b/docs/examples/html/pyscript-setup.html @@ -0,0 +1,6 @@ +{% load reactpy %} + + + ReactPy + {% pyscript_setup %} + diff --git a/docs/examples/html/pyscript-ssr-parent.html b/docs/examples/html/pyscript-ssr-parent.html new file mode 100644 index 00000000..bf0f47ae --- /dev/null +++ b/docs/examples/html/pyscript-ssr-parent.html @@ -0,0 +1,14 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup %} + + + + {% component "example_project.my_app.components.server_side_component" %} + + + diff --git a/docs/examples/html/pyscript-tag.html b/docs/examples/html/pyscript-tag.html new file mode 100644 index 00000000..6ca71085 --- /dev/null +++ b/docs/examples/html/pyscript-tag.html @@ -0,0 +1,14 @@ +{% load reactpy %} + + + + + ReactPy + {% pyscript_setup %} + + + + {% component "example_project.my_app.components.server_side_component.py" %} + + + diff --git a/docs/examples/python/example/views.py b/docs/examples/python/example/views.py index a8ed7fdb..23e21130 100644 --- a/docs/examples/python/example/views.py +++ b/docs/examples/python/example/views.py @@ -2,4 +2,4 @@ def index(request): - return render(request, "my-template.html") + return render(request, "my_template.html") diff --git a/docs/examples/python/pyscript-component-initial-object.py b/docs/examples/python/pyscript-component-initial-object.py new file mode 100644 index 00000000..222a568b --- /dev/null +++ b/docs/examples/python/pyscript-component-initial-object.py @@ -0,0 +1,12 @@ +from reactpy import component, html +from reactpy_django.components import pyscript_component + + +@component +def server_side_component(): + return html.div( + pyscript_component( + "./example_project/my_app/components/root.py", + initial=html.div("Loading ..."), + ), + ) diff --git a/docs/examples/python/pyscript-component-initial-string.py b/docs/examples/python/pyscript-component-initial-string.py new file mode 100644 index 00000000..664b9f9b --- /dev/null +++ b/docs/examples/python/pyscript-component-initial-string.py @@ -0,0 +1,12 @@ +from reactpy import component, html +from reactpy_django.components import pyscript_component + + +@component +def server_side_component(): + return html.div( + pyscript_component( + "./example_project/my_app/components/root.py", + initial="
Loading ...
", + ), + ) diff --git a/docs/examples/python/pyscript-component-multiple-files-root.py b/docs/examples/python/pyscript-component-multiple-files-root.py new file mode 100644 index 00000000..776b26b2 --- /dev/null +++ b/docs/examples/python/pyscript-component-multiple-files-root.py @@ -0,0 +1,12 @@ +from reactpy import component, html +from reactpy_django.components import pyscript_component + + +@component +def server_side_component(): + return html.div( + pyscript_component( + "./example_project/my_app/components/root.py", + "./example_project/my_app/components/child.py", + ), + ) diff --git a/docs/examples/python/pyscript-component-root.py b/docs/examples/python/pyscript-component-root.py new file mode 100644 index 00000000..9880b740 --- /dev/null +++ b/docs/examples/python/pyscript-component-root.py @@ -0,0 +1,12 @@ +from reactpy import component, html +from reactpy_django.components import pyscript_component + + +@component +def server_side_component(): + return html.div( + pyscript_component( + "./example_project/my_app/components/main.py", + root="main", + ), + ) diff --git a/docs/examples/python/pyscript-hello-world.py b/docs/examples/python/pyscript-hello-world.py new file mode 100644 index 00000000..d5737421 --- /dev/null +++ b/docs/examples/python/pyscript-hello-world.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def root(): + return html.div("Hello, World!") diff --git a/docs/examples/python/pyscript-initial-object.py b/docs/examples/python/pyscript-initial-object.py new file mode 100644 index 00000000..1742ff87 --- /dev/null +++ b/docs/examples/python/pyscript-initial-object.py @@ -0,0 +1,10 @@ +from django.shortcuts import render +from reactpy import html + + +def index(request): + return render( + request, + "my_template.html", + context={"my_initial_object": html.div("Loading ...")}, + ) diff --git a/docs/examples/python/pyscript-js-execution.py b/docs/examples/python/pyscript-js-execution.py new file mode 100644 index 00000000..a96ef65b --- /dev/null +++ b/docs/examples/python/pyscript-js-execution.py @@ -0,0 +1,11 @@ +import js +from reactpy import component, html + + +@component +def root(): + + def onClick(event): + js.document.title = "New window title" + + return html.button({"onClick": onClick}, "Click Me!") diff --git a/docs/examples/python/pyscript-js-module.py b/docs/examples/python/pyscript-js-module.py new file mode 100644 index 00000000..221b5bae --- /dev/null +++ b/docs/examples/python/pyscript-js-module.py @@ -0,0 +1,12 @@ +from reactpy import component, html + + +@component +def root(): + from pyscript.js_modules import moment + + return html.div( + {"id": "moment"}, + "Using the JavaScript package 'moment' to calculate time: ", + moment.default().format("YYYY-MM-DD HH:mm:ss"), + ) diff --git a/docs/examples/python/pyscript-multiple-files-child.py b/docs/examples/python/pyscript-multiple-files-child.py new file mode 100644 index 00000000..73dbb189 --- /dev/null +++ b/docs/examples/python/pyscript-multiple-files-child.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def child_component(): + return html.div("This is a child component from a different file.") diff --git a/docs/examples/python/pyscript-multiple-files-root.py b/docs/examples/python/pyscript-multiple-files-root.py new file mode 100644 index 00000000..dc17e7ad --- /dev/null +++ b/docs/examples/python/pyscript-multiple-files-root.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from reactpy import component, html + +if TYPE_CHECKING: + from .child import child_component + + +@component +def root(): + return html.div("This text is from the root component.", child_component()) diff --git a/docs/examples/python/pyscript-root.py b/docs/examples/python/pyscript-root.py new file mode 100644 index 00000000..f39fd01e --- /dev/null +++ b/docs/examples/python/pyscript-root.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def main(): + return html.div("Hello, World!") diff --git a/docs/examples/python/pyscript-setup-config-object.py b/docs/examples/python/pyscript-setup-config-object.py new file mode 100644 index 00000000..85db2751 --- /dev/null +++ b/docs/examples/python/pyscript-setup-config-object.py @@ -0,0 +1,9 @@ +from django.shortcuts import render + + +def index(request): + return render( + request, + "my_template.html", + context={"my_config_object": {"experimental_create_proxy": "auto"}}, + ) diff --git a/docs/examples/python/pyscript-setup-extra-js-object.py b/docs/examples/python/pyscript-setup-extra-js-object.py new file mode 100644 index 00000000..805365cf --- /dev/null +++ b/docs/examples/python/pyscript-setup-extra-js-object.py @@ -0,0 +1,10 @@ +from django.shortcuts import render +from django.templatetags.static import static + + +def index(request): + return render( + request, + "my_template.html", + context={"my_extra_js_object": {static("moment.js"): "moment"}}, + ) diff --git a/docs/examples/python/pyscript-ssr-child.py b/docs/examples/python/pyscript-ssr-child.py new file mode 100644 index 00000000..d2566c88 --- /dev/null +++ b/docs/examples/python/pyscript-ssr-child.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def root(): + return html.div("This text is from my client-side component") diff --git a/docs/examples/python/pyscript-ssr-parent.py b/docs/examples/python/pyscript-ssr-parent.py new file mode 100644 index 00000000..b51aa110 --- /dev/null +++ b/docs/examples/python/pyscript-ssr-parent.py @@ -0,0 +1,10 @@ +from reactpy import component, html +from reactpy_django.components import 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"), + ) diff --git a/docs/examples/python/pyscript-tag.py b/docs/examples/python/pyscript-tag.py new file mode 100644 index 00000000..6455e9da --- /dev/null +++ b/docs/examples/python/pyscript-tag.py @@ -0,0 +1,15 @@ +from reactpy import component, html +from reactpy_django.html import pyscript + +example_source_code = """ +import js + +js.console.log("Hello, World!") +""" + + +@component +def server_side_component(): + return html.div( + pyscript(example_source_code.strip()), + ) diff --git a/docs/examples/python/template-tag-bad-view.py b/docs/examples/python/template-tag-bad-view.py index 00d0d9f7..ef16c845 100644 --- a/docs/examples/python/template-tag-bad-view.py +++ b/docs/examples/python/template-tag-bad-view.py @@ -2,5 +2,8 @@ def example_view(request): - context_vars = {"my_variable": "example_project.my_app.components.hello_world"} - return render(request, "my-template.html", context_vars) + return render( + request, + "my_template.html", + context={"my_variable": "example_project.my_app.components.hello_world"}, + ) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index bee85cc1..e4159640 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -7,6 +7,7 @@ nav: - Reference: - Components: reference/components.md - Hooks: reference/hooks.md + - HTML: reference/html.md - URL Router: reference/router.md - Decorators: reference/decorators.md - Utilities: reference/utils.md diff --git a/docs/src/assets/css/admonition.css b/docs/src/assets/css/admonition.css index 8b3f06ef..c93892a8 100644 --- a/docs/src/assets/css/admonition.css +++ b/docs/src/assets/css/admonition.css @@ -1,45 +1,45 @@ [data-md-color-scheme="slate"] { - --admonition-border-color: transparent; - --admonition-expanded-border-color: rgba(255, 255, 255, 0.1); - --note-bg-color: rgba(43, 110, 98, 0.2); - --terminal-bg-color: #0c0c0c; - --terminal-title-bg-color: #000; - --deep-dive-bg-color: rgba(43, 52, 145, 0.2); - --you-will-learn-bg-color: #353a45; - --pitfall-bg-color: rgba(182, 87, 0, 0.2); + --admonition-border-color: transparent; + --admonition-expanded-border-color: rgba(255, 255, 255, 0.1); + --note-bg-color: rgba(43, 110, 98, 0.2); + --terminal-bg-color: #0c0c0c; + --terminal-title-bg-color: #000; + --deep-dive-bg-color: rgba(43, 52, 145, 0.2); + --you-will-learn-bg-color: #353a45; + --pitfall-bg-color: rgba(182, 87, 0, 0.2); } [data-md-color-scheme="default"] { - --admonition-border-color: rgba(0, 0, 0, 0.08); - --admonition-expanded-border-color: var(--admonition-border-color); - --note-bg-color: rgb(244, 251, 249); - --terminal-bg-color: rgb(64, 71, 86); - --terminal-title-bg-color: rgb(35, 39, 47); - --deep-dive-bg-color: rgb(243, 244, 253); - --you-will-learn-bg-color: rgb(246, 247, 249); - --pitfall-bg-color: rgb(254, 245, 231); + --admonition-border-color: rgba(0, 0, 0, 0.08); + --admonition-expanded-border-color: var(--admonition-border-color); + --note-bg-color: rgb(244, 251, 249); + --terminal-bg-color: rgb(64, 71, 86); + --terminal-title-bg-color: rgb(35, 39, 47); + --deep-dive-bg-color: rgb(243, 244, 253); + --you-will-learn-bg-color: rgb(246, 247, 249); + --pitfall-bg-color: rgb(254, 245, 231); } .md-typeset details, .md-typeset .admonition { - border-color: var(--admonition-border-color) !important; - box-shadow: none; + border-color: var(--admonition-border-color) !important; + box-shadow: none; } .md-typeset :is(.admonition, details) { - margin: 0.55em 0; + margin: 0 0; } .md-typeset .admonition { - font-size: 0.7rem; + font-size: 0.7rem; } .md-typeset .admonition:focus-within, .md-typeset details:focus-within { - box-shadow: none !important; + box-shadow: none !important; } .md-typeset details[open] { - border-color: var(--admonition-expanded-border-color) !important; + border-color: var(--admonition-expanded-border-color) !important; } /* @@ -47,24 +47,24 @@ Admonition: "summary" React Name: "You will learn" */ .md-typeset .admonition.summary { - background: var(--you-will-learn-bg-color); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; + background: var(--you-will-learn-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; } .md-typeset .summary .admonition-title { - font-size: 1rem; - background: transparent; - padding-left: 0.6rem; - padding-bottom: 0; + font-size: 1rem; + background: transparent; + padding-left: 0.6rem; + padding-bottom: 0; } .md-typeset .summary .admonition-title:before { - display: none; + display: none; } .md-typeset .admonition.summary { - border-color: #ffffff17 !important; + border-color: #ffffff17 !important; } /* @@ -72,21 +72,21 @@ Admonition: "abstract" React Name: "Note" */ .md-typeset .admonition.abstract { - background: var(--note-bg-color); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; + background: var(--note-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; } .md-typeset .abstract .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(68, 172, 153); + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(68, 172, 153); } .md-typeset .abstract .admonition-title:before { - font-size: 1.1rem; - background: rgb(68, 172, 153); + font-size: 1.1rem; + background: rgb(68, 172, 153); } /* @@ -94,21 +94,21 @@ Admonition: "warning" React Name: "Pitfall" */ .md-typeset .admonition.warning { - background: var(--pitfall-bg-color); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; + background: var(--pitfall-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; } .md-typeset .warning .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(219, 125, 39); + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(219, 125, 39); } .md-typeset .warning .admonition-title:before { - font-size: 1.1rem; - background: rgb(219, 125, 39); + font-size: 1.1rem; + background: rgb(219, 125, 39); } /* @@ -116,21 +116,21 @@ Admonition: "info" React Name: "Deep Dive" */ .md-typeset .admonition.info { - background: var(--deep-dive-bg-color); - padding: 0.8rem 1.4rem; - border-radius: 0.8rem; + background: var(--deep-dive-bg-color); + padding: 0.8rem 1.4rem; + border-radius: 0.8rem; } .md-typeset .info .admonition-title { - font-size: 1rem; - background: transparent; - padding-bottom: 0; - color: rgb(136, 145, 236); + font-size: 1rem; + background: transparent; + padding-bottom: 0; + color: rgb(136, 145, 236); } .md-typeset .info .admonition-title:before { - font-size: 1.1rem; - background: rgb(136, 145, 236); + font-size: 1.1rem; + background: rgb(136, 145, 236); } /* @@ -138,23 +138,24 @@ Admonition: "example" React Name: "Terminal" */ .md-typeset .admonition.example { - background: var(--terminal-bg-color); - border-radius: 0.4rem; - overflow: hidden; - border: none; + background: var(--terminal-bg-color); + border-radius: 0.4rem; + overflow: hidden; + border: none; + margin: 0.5rem 0; } .md-typeset .example .admonition-title { - background: var(--terminal-title-bg-color); - color: rgb(246, 247, 249); + background: var(--terminal-title-bg-color); + color: rgb(246, 247, 249); } .md-typeset .example .admonition-title:before { - background: rgb(246, 247, 249); + background: rgb(246, 247, 249); } .md-typeset .admonition.example code { - background: transparent; - color: #fff; - box-shadow: none; + background: transparent; + color: #fff; + box-shadow: none; } diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index dee45011..66265e78 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -39,3 +39,5 @@ misconfigurations backhaul sublicense broadcasted +hello_world +my_template diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md index a0cca013..dd258737 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -125,6 +125,6 @@ Prefer a quick summary? Read the **At a Glance** section below. --- - **`my_app/templates/my-template.html`** + **`my_app/templates/my_template.html`** {% include-markdown "../../../README.md" start="" end="" %} diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md index b0749c41..08df6a57 100644 --- a/docs/src/learn/your-first-component.md +++ b/docs/src/learn/your-first-component.md @@ -43,7 +43,7 @@ Within this file, you can define your component functions using ReactPy's `#!pyt We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - Ultimately, components are referenced by Python dotted path in `my-template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. + Ultimately, components are referenced by Python dotted path in `my_template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. ??? question "What does the decorator actually do?" @@ -62,7 +62,7 @@ In your **Django app**'s HTML template, you can now embed your ReactPy component Additionally, you can pass in `#!python args` and `#!python kwargs` into your component function. After reading the code below, pay attention to how the function definition for `#!python hello_world` ([_from the previous step_](#defining-a-component)) accepts a `#!python recipient` argument. -=== "my-template.html" +=== "my_template.html" {% include-markdown "../../../README.md" start="" end="" %} @@ -76,7 +76,7 @@ Additionally, you can pass in `#!python args` and `#!python kwargs` into your co ## Setting up a Django view -Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my-template.html` ([_from the previous step_](#embedding-in-a-template)). +Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my_template.html` ([_from the previous step_](#embedding-in-a-template)). === "views.py" diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index aa0e75d4..aaeabba7 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -8,6 +8,114 @@ We supply some pre-designed that components can be used to help simplify develop --- +## PyScript Component + +This allows you to embedded any number of client-side PyScript components within traditional ReactPy components. + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +=== "components.py" + + ```python + {% include "../../examples/python/pyscript-ssr-parent.py" %} + ``` + +=== "root.py" + + ```python + {% include "../../examples/python/pyscript-ssr-child.py" %} + ``` + +=== "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-ssr-parent.html" %} + ``` + +??? example "See Interface" + + **Parameters** + + | 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 root` | `#!python str` | The name of the root component function. | `#!python "root"` | + + + +??? warning "You must call `pyscript_setup` in your Django template before using this tag!" + + This requires using of the [`#!jinja {% pyscript_setup %}` template tag](./template-tag.md#pyscript-setup) to initialize PyScript on the client. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup.html" %} + ``` + + + +{% include-markdown "../reference/template-tag.md" start="" end="" %} + +{% include-markdown "../reference/template-tag.md" start="" end="" trailing-newlines=false preserve-includer-indent=false %} + + === "components.py" + + ```python + {% include "../../examples/python/pyscript-component-multiple-files-root.py" %} + ``` + + === "root.py" + + ```python + {% include "../../examples/python/pyscript-multiple-files-root.py" %} + ``` + + === "child.py" + + ```python + {% include "../../examples/python/pyscript-multiple-files-child.py" %} + ``` + +??? question "How do I display something while the component is loading?" + + You can configure the `#!python initial` keyword to display HTML while your PyScript component is loading. + + The value for `#!python initial` is most commonly be a `#!python reactpy.html` snippet or a non-interactive `#!python @component`. + + === "components.py" + + ```python + {% include "../../examples/python/pyscript-component-initial-object.py" %} + ``` + + However, you can also use a string containing raw HTML. + + === "components.py" + + ```python + {% include "../../examples/python/pyscript-component-initial-string.py" %} + ``` + +??? question "Can I use a different name for my root component?" + + Yes, you can use the `#!python root` keyword to specify a different name for your root function. + + === "components.py" + + ```python + {% include "../../examples/python/pyscript-component-root.py" %} + ``` + + === "main.py" + + ```python + {% include "../../examples/python/pyscript-root.py" %} + ``` + +--- + ## View To Component Automatically convert a Django view into a component. diff --git a/docs/src/reference/html.md b/docs/src/reference/html.md new file mode 100644 index 00000000..fd63c033 --- /dev/null +++ b/docs/src/reference/html.md @@ -0,0 +1,31 @@ +## Overview + +

+ +We supply some pre-generated that HTML nodes can be used to help simplify development. + +

+ +--- + +## PyScript + +Primitive HTML tag that is leveraged by [`reactpy_django.components.pyscript_component`](./components.md#pyscript-component). + +This can be used as an alternative to the `#!python reactpy.html.script` tag to execute JavaScript and run client-side Python code. + +Additionally, this tag functions identically to any other tag contained within `#!python reactpy.html`, and can be used in the same way. + +=== "components.py" + + ```python + {% include "../../examples/python/pyscript-tag.py" %} + ``` + +=== "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-tag.html" %} + ``` + +{% include-markdown "../reference/components.md" start="" end="" %} diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md index af5353e8..66ad0f9e 100644 --- a/docs/src/reference/router.md +++ b/docs/src/reference/router.md @@ -18,6 +18,13 @@ A Single Page Application URL router, which is a variant of [`reactpy-router`](h URL router that enables the ability to conditionally render other components based on the client's current URL `#!python path`. +!!! warning "Pitfall" + + All pages where `django_router` is used must have identical, or more permissive URL exposure within Django's [URL patterns](https://docs.djangoproject.com/en/5.0/topics/http/urls/#example). You can think of the router component as a secondary, client-side router. Django still handles the primary server-side routes. + + We recommend creating a route with a wildcard `.*` to forward routes to ReactPy. For example... + `#!python re_path(r"^/router/.*$", my_reactpy_view)` + === "components.py" ```python diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 759aa8cf..434c81d0 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -14,7 +14,7 @@ This template tag can be used to insert any number of ReactPy components onto yo Each component loaded via this template tag will receive a dedicated WebSocket connection to the server. -=== "my-template.html" +=== "my_template.html" {% include-markdown "../../../README.md" start="" end="" %} @@ -33,23 +33,17 @@ Each component loaded via this template tag will receive a dedicated WebSocket c | `#!python offline` | `#!python str` | The dotted path to a component that will be displayed if your root component loses connection to the server. Keep in mind, this `offline` component will be non-interactive (hooks won't operate). | `#!python ""` | | `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A | - **Returns** - - | Type | Description | - | --- | --- | - | `#!python Component` | A ReactPy component. | - ??? warning "Do not use context variables for the component path" The ReactPy component finder requires that your component path is a string. - **Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior, such as components that will not render. + **Do not** use Django template/context variables for the component path. Failure to follow this warning will result in render failures. For example, **do not** do the following: - === "my-template.html" + === "my_template.html" ```jinja @@ -75,7 +69,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c You can add as many components to a webpage as needed by using the template tag multiple times. Retrofitting legacy sites to use ReactPy will typically involve many components on one page. - === "my-template.html" + === "my_template.html" ```jinja {% load reactpy %} @@ -99,7 +93,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c You can use any combination of `#!python *args`/`#!python **kwargs` in your template tag. - === "my-template.html" + === "my_template.html" ```jinja {% component "example_project.my_app.components.frog_greeter" 123 "Mr. Froggles" species="Grey Treefrog" %} @@ -115,7 +109,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c Yes! This is most commonly done through [`settings.py:REACTPY_HOSTS`](../reference/settings.md#reactpy_default_hosts). However, you can use the `#!python host` keyword to render components on a specific ASGI server. - === "my-template.html" + === "my_template.html" ```jinja ... @@ -135,7 +129,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c This is most commonly done through [`settings.py:REACTPY_PRERENDER`](../reference/settings.md#reactpy_prerender). However, you can use the `#!python prerender` keyword to pre-render a specific component. - === "my-template.html" + === "my_template.html" ```jinja ... @@ -143,11 +137,11 @@ Each component loaded via this template tag will receive a dedicated WebSocket c ... ``` -??? question "How do I show something when the client disconnects?" +??? question "How do I display something when the client disconnects?" You can use the `#!python offline` keyword to display a specific component when the client disconnects from the server. - === "my-template.html" + === "my_template.html" ```jinja ... @@ -156,3 +150,229 @@ Each component loaded via this template tag will receive a dedicated WebSocket c ``` _Note: The `#!python offline` component will be non-interactive (hooks won't operate)._ + +## PyScript Component + +This template tag can be used to insert any number of **client-side** ReactPy components onto your page. + + + +By default, the only dependencies available are the Python standard library, `pyscript`, `pyodide`, `reactpy` core. + +Your PyScript component file requires a `#!python def root()` component to function as the entry point. + + + +!!! warning "Pitfall" + + Your provided Python file is loaded directly into the client (web browser) **as raw text**, and ran using a PyScript interpreter. Be cautious about what you include in your Python file. + + As a result of running client-side, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components. + +=== "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-component.html" %} + ``` + +=== "hello_world.py" + + ```python + {% include "../../examples/python/pyscript-hello-world.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | 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 root` | `#!python str` | The name of the root component function. | `#!python "root"` | + + + +??? question "How do I execute JavaScript within PyScript components?" + + PyScript components have the ability to directly execute standard library JavaScript using the [`pyodide` `js` module](https://pyodide.org/en/stable/usage/type-conversions.html#importing-javascript-objects-into-python) or [`pyscript` foreign function interface](https://docs.pyscript.net/2024.6.1/user-guide/dom/). + + The `#!python js` module has access to everything within the browser's JavaScript environment. Therefore, any global JavaScript functions loaded within your HTML `#!html ` can be called as well. However, be mindful of JavaScript load order! + + === "root.py" + + ```python + {% include "../../examples/python/pyscript-js-execution.py" %} + ``` + + To import JavaScript modules in a fashion similar to `#!javascript import {moment} from 'static/moment.js'`, you will need to configure your `#!jinja {% pyscript_setup %}` block to make the module available to PyScript. This module will be accessed within `#!python pyscript.js_modules.*`. For more information, see the [PyScript JS modules docs](https://docs.pyscript.net/2024.6.2/user-guide/configuration/#javascript-modules). + + === "root.py" + + ```python + {% include "../../examples/python/pyscript-js-module.py" %} + ``` + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-js-module.html" %} + ``` + + + + + +??? question "Does my entire component need to be contained in one file?" + + Splitting a large file into multiple files is a common practice in software development. + + However, PyScript components are run on the client browser. As such, they do not have access to your local development environment, and thus cannot `#!python import` any local Python files. + + If your PyScript component file gets too large, you can declare multiple file paths instead. These files will automatically combined by ReactPy. + + Here is how we recommend splitting your component into multiple files while avoiding local imports but retaining type hints. + + + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-multiple-files.html" %} + ``` + + === "root.py" + + ```python + {% include "../../examples/python/pyscript-multiple-files-root.py" %} + ``` + + === "child.py" + + ```python + {% include "../../examples/python/pyscript-multiple-files-child.py" %} + ``` + +??? question "How do I display something while the component is loading?" + + You can configure the `#!python initial` keyword to display HTML while your PyScript component is loading. + + The value for `#!python initial` is most commonly be a string containing raw HTML. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-initial-string.html" %} + ``` + + However, you can also insert a `#!python reactpy.html` snippet or a non-interactive `#!python @component` via template context. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-initial-object.html" %} + ``` + + === "views.py" + + ```python + {% include "../../examples/python/pyscript-initial-object.py" %} + ``` + +??? question "Can I use a different name for my root component?" + + Yes, you can use the `#!python root` keyword to specify a different name for your root function. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-root.html" %} + ``` + + === "main.py" + + ```python + {% include "../../examples/python/pyscript-root.py" %} + ``` + +## PyScript Setup + +This template tag configures the current page to be able to run `pyscript`. + +You can optionally use this tag to configure the current PyScript environment. For example, you can include a list of Python packages to automatically install within the PyScript environment. + +=== "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup.html" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python *extra_py` | `#!python str` | Dependencies that need to be loaded on the page for your PyScript components. Each dependency must be contained within it's own string and written in Python requirements file syntax. | N/A | + | `#!python extra_js` | `#!python str | dict` | A JSON string or Python dictionary containing a vanilla JavaScript module URL and the `#!python name: str` to access it within `#!python pyscript.js_modules.*`. | `#!python ""` | + | `#!python config` | `#!python str | dict` | A JSON string or Python dictionary containing PyScript configuration values. | `#!python ""` | + +??? question "How do I install additional Python dependencies?" + + Dependencies must be available on [`pypi`](https://pypi.org/) and declared in your `#!jinja {% pyscript_setup %}` block using Python requirements file syntax. + + These dependencies are automatically downloaded and installed into the PyScript client-side environment when the page is loaded. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup-dependencies.html" %} + ``` + +??? question "How do I install additional Javascript dependencies?" + + You can use the `#!python extra_js` keyword to load additional JavaScript modules into your PyScript environment. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup-extra-js-object.html" %} + ``` + + === "views.py" + + ```python + {% include "../../examples/python/pyscript-setup-extra-js-object.py" %} + ``` + + The value for `#!python extra_js` is most commonly a Python dictionary, but JSON strings are also supported. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup-extra-js-string.html" %} + ``` + +??? question "How do I modify the `pyscript` default configuration?" + + You can modify the default [PyScript configuration](https://docs.pyscript.net/2024.6.2/user-guide/configuration/) by providing a value to the `#!python config` keyword. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup-config-string.html" %} + ``` + + While this value is most commonly a JSON string, Python dictionary objects are also supported. + + === "my_template.html" + + ```jinja + {% include "../../examples/html/pyscript-setup-config-object.html" %} + ``` + + === "views.py" + + ```python + {% include "../../examples/python/pyscript-setup-config-object.py" %} + ``` diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index 461d9df5..6590012c 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -76,7 +76,7 @@ Typically, this function is automatically called on all components contained wit For security reasons, ReactPy requires all root components to be registered. However, all components contained within Django templates are automatically registered. - This function is needed when you have configured your [`host`](../reference/template-tag.md#component) to a dedicated Django rendering application that doesn't have templates. + This function is commonly needed when you have configured your [`host`](../reference/template-tag.md#component) to a dedicated Django rendering application that doesn't have templates. --- diff --git a/setup.py b/setup.py index b99d550b..76a91edf 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ from __future__ import annotations, print_function +import shutil import sys import traceback from distutils import log @@ -16,7 +17,9 @@ name = "reactpy_django" root_dir = Path(__file__).parent src_dir = root_dir / "src" +js_dir = src_dir / "js" package_dir = src_dir / name +static_dir = package_dir / "static" / name # ----------------------------------------------------------------------------- @@ -97,22 +100,37 @@ def build_javascript_first(build_cls: type): class Command(build_cls): def run(self): - js_dir = str(src_dir / "js") log.info("Installing Javascript...") - result = npm.call(["install"], cwd=js_dir) + result = npm.call(["install"], cwd=str(js_dir)) if result != 0: log.error(traceback.format_exc()) log.error("Failed to install Javascript") raise RuntimeError("Failed to install Javascript") log.info("Building Javascript...") - result = npm.call(["run", "build"], cwd=js_dir) + result = npm.call(["run", "build"], cwd=str(js_dir)) if result != 0: log.error(traceback.format_exc()) log.error("Failed to build Javascript") raise RuntimeError("Failed to build Javascript") + log.info("Copying @pyscript/core distribution") + pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist" + pyscript_static_dir = static_dir / "pyscript" + if not pyscript_static_dir.exists(): + pyscript_static_dir.mkdir() + for file in pyscript_dist.iterdir(): + shutil.copy(file, pyscript_static_dir / file.name) + + log.info("Copying Morphdom distribution") + morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist" + morphdom_static_dir = static_dir / "morphdom" + if not morphdom_static_dir.exists(): + morphdom_static_dir.mkdir() + for file in morphdom_dist.iterdir(): + shutil.copy(file, morphdom_static_dir / file.name) + log.info("Successfully built Javascript") super().run() diff --git a/src/js/package-lock.json b/src/js/package-lock.json index b0390203..d4cb1c0b 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -5,8 +5,10 @@ "packages": { "": { "dependencies": { + "@pyscript/core": "^0.4.48", "@reactpy/client": "^0.3.1", "@rollup/plugin-typescript": "^11.1.6", + "morphdom": "^2.7.3", "tslib": "^2.6.2" }, "devDependencies": { @@ -205,6 +207,19 @@ "node": ">= 8" } }, + "node_modules/@pyscript/core": { + "version": "0.4.48", + "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.48.tgz", + "integrity": "sha512-cVZ//1WDkWhjZ1tOjUB1YJ5mKxDf3kMpzS/pw7Oe9/BMrB/NM3TxxCQ9Oyvq7Fkfv1F+srIcsi1xZ5gQeP+5Tg==", + "dependencies": { + "@ungap/with-resolvers": "^0.1.0", + "basic-devtools": "^0.1.6", + "polyscript": "^0.13.5", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1", + "type-checked-collections": "^0.1.7" + } + }, "node_modules/@reactpy/client": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz", @@ -550,8 +565,22 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@ungap/with-resolvers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", + "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" + }, + "node_modules/@webreflection/fetch": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", + "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" + }, + "node_modules/@webreflection/idb-map": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.1.tgz", + "integrity": "sha512-lRCanqwR7tHHFohJHAMSMEZnoNPvgjcKr0f5e4y+lTJA+fctT61EZ+f5pT5/+8+wlSsMAvXjzfKRLT6o9aqxbA==" }, "node_modules/acorn": { "version": "8.11.3", @@ -749,6 +778,11 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/basic-devtools": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", + "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -809,6 +843,28 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/codedent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", + "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", + "dependencies": { + "plain-tag": "^0.1.3" + } + }, + "node_modules/coincident": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", + "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "gc-hook": "^0.3.1", + "proxy-target": "^3.0.2" + }, + "optionalDependencies": { + "ws": "^8.16.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1463,6 +1519,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gc-hook": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", + "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -1653,6 +1714,11 @@ "node": ">= 0.4" } }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -2212,6 +2278,11 @@ "node": ">=10" } }, + "node_modules/morphdom": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.3.tgz", + "integrity": "sha512-rvGK92GxSuPEZLY8D/JH07cG3BxyA+/F0Bxg32OoGAEFFhGWA3OqVpqPZlOgZTCR52clXrmz+z2pYSJ6gOig1w==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -2440,6 +2511,30 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/plain-tag": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", + "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" + }, + "node_modules/polyscript": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.13.5.tgz", + "integrity": "sha512-PwXWnhLbOMtvZWFIN271JhaN7KnxESaMtv9Rcdrq1TKTCMnkz9idvYb3Od1iumBJlr49lLlwyUKeGb423rFR4w==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "@webreflection/fetch": "^0.1.5", + "@webreflection/idb-map": "^0.3.1", + "basic-devtools": "^0.1.6", + "codedent": "^0.1.2", + "coincident": "^1.2.3", + "gc-hook": "^0.3.1", + "html-escaper": "^3.0.3", + "proxy-target": "^3.0.2", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2475,6 +2570,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-target": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", + "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2840,6 +2940,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sticky-module": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", + "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -2958,6 +3063,11 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/to-json-callback": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", + "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -2975,6 +3085,11 @@ "node": ">= 0.8.0" } }, + "node_modules/type-checked-collections": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", + "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -3185,6 +3300,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "optional": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/js/package.json b/src/js/package.json index 8d2d9ff5..949b6cf9 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -13,15 +13,17 @@ "@rollup/plugin-replace": "^5.0.5", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", - "prettier": "^3.2.3", "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", + "prettier": "^3.2.3", "rollup": "^4.9.5", "typescript": "^5.3.3" }, "dependencies": { + "@pyscript/core": "^0.4.48", "@reactpy/client": "^0.3.1", "@rollup/plugin-typescript": "^11.1.6", + "morphdom": "^2.7.3", "tslib": "^2.6.2" } } diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index 8598ed0c..0bbff9d1 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -2,7 +2,16 @@ import nest_asyncio -from reactpy_django import checks, components, decorators, hooks, router, types, utils +from reactpy_django import ( + checks, + components, + decorators, + hooks, + html, + router, + types, + utils, +) from reactpy_django.websocket.paths import ( REACTPY_WEBSOCKET_PATH, REACTPY_WEBSOCKET_ROUTE, @@ -12,6 +21,7 @@ __all__ = [ "REACTPY_WEBSOCKET_PATH", "REACTPY_WEBSOCKET_ROUTE", + "html", "hooks", "components", "decorators", diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 75b0c321..579c73e3 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -4,6 +4,7 @@ import os from typing import Any, Callable, Sequence, Union, cast, overload from urllib.parse import urlencode +from uuid import uuid4 from warnings import warn from django.contrib.staticfiles.finders import find @@ -12,10 +13,17 @@ from django.urls import reverse from django.views import View from reactpy import component, hooks, html, utils -from reactpy.types import Key, VdomDict +from reactpy.types import ComponentType, Key, VdomDict from reactpy_django.exceptions import ViewNotRegisteredError -from reactpy_django.utils import generate_obj_name, import_module, render_view +from reactpy_django.html import pyscript +from reactpy_django.utils import ( + generate_obj_name, + import_module, + render_pyscript_template, + render_view, + vdom_or_component_to_string, +) # Type hints for: @@ -27,8 +35,7 @@ def view_to_component( compatibility: bool = False, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Any: - ... +) -> Any: ... # Type hints for: @@ -39,8 +46,7 @@ def view_to_component( compatibility: bool = False, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Callable[[Callable], Any]: - ... +) -> Callable[[Callable], Any]: ... def view_to_component( @@ -148,6 +154,29 @@ def django_js(static_path: str, key: Key | None = None): return _django_js(static_path=static_path, key=key) +def pyscript_component( + *file_paths: str, + initial: str | VdomDict | ComponentType = "", + root: str = "root", +): + """ + 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, @@ -284,3 +313,29 @@ def _cached_static_contents(static_path: str) -> str: ) return file_contents + + +@component +def _pyscript_component( + *file_paths: str, + initial: str | VdomDict | ComponentType = "", + root: str = "root", +): + rendered, set_rendered = hooks.use_state(False) + uuid = uuid4().hex.replace("-", "") + initial = vdom_or_component_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/config.py b/src/reactpy_django/config.py index cabb61a4..21a30a32 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -7,7 +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_DEBUG_MODE +from reactpy.config import REACTPY_DEBUG_MODE as _REACTPY_DEBUG_MODE from reactpy.core.types import ComponentConstructor from reactpy_django.types import ( @@ -17,7 +17,8 @@ from reactpy_django.utils import import_dotted_path # Non-configurable values -REACTPY_DEBUG_MODE.set_current(getattr(settings, "DEBUG")) +_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] = {} diff --git a/src/reactpy_django/html.py b/src/reactpy_django/html.py new file mode 100644 index 00000000..d35daf43 --- /dev/null +++ b/src/reactpy_django/html.py @@ -0,0 +1,3 @@ +from reactpy.core.vdom import make_vdom_constructor + +pyscript = make_vdom_constructor("py-script") diff --git a/src/reactpy_django/management/commands/clean_reactpy.py b/src/reactpy_django/management/commands/clean_reactpy.py index bfde6f2f..0c5dc308 100644 --- a/src/reactpy_django/management/commands/clean_reactpy.py +++ b/src/reactpy_django/management/commands/clean_reactpy.py @@ -28,10 +28,10 @@ def add_arguments(self, parser): parser.add_argument( "--sessions", action="store_true", - help="Configure this clean to only clean session data (and other configured cleaning options).", + help="Clean session data. This value can be combined with other cleaning options.", ) parser.add_argument( "--user-data", action="store_true", - help="Configure this clean to only clean user data (and other configured cleaning options).", + help="Clean user data. This value can be combined with other cleaning options.", ) diff --git a/src/reactpy_django/pyscript/__init__.py b/src/reactpy_django/pyscript/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/reactpy_django/pyscript/component_template.py b/src/reactpy_django/pyscript/component_template.py new file mode 100644 index 00000000..59442571 --- /dev/null +++ b/src/reactpy_django/pyscript/component_template.py @@ -0,0 +1,26 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import asyncio + + from reactpy_django.pyscript.layout_handler import ReactPyLayoutHandler + + +# User component is inserted below by regex replacement +def user_workspace_UUID(): + """Encapsulate the user's code with a completely unique function (workspace) + to prevent overlapping imports and variable names between different components. + + This code is designed to be run directly by PyScript, and is not intended to be run + in a normal Python environment. + + ReactPy-Django performs string substitutions to turn this file into valid PyScript. + """ + + def root(): ... + + return root() + + +# Create a task to run the user's component workspace +task_UUID = asyncio.create_task(ReactPyLayoutHandler("UUID").run(user_workspace_UUID)) diff --git a/src/reactpy_django/pyscript/layout_handler.py b/src/reactpy_django/pyscript/layout_handler.py new file mode 100644 index 00000000..da5bfb1b --- /dev/null +++ b/src/reactpy_django/pyscript/layout_handler.py @@ -0,0 +1,138 @@ +# mypy: disable-error-code=attr-defined +import asyncio + + +class ReactPyLayoutHandler: + """Encapsulate the entire layout handler with a class to prevent overlapping + variable names between user code. + + This code is designed to be run directly by PyScript, and is not intended to be run + in a normal Python environment. + """ + + def __init__(self, uuid): + self.uuid = uuid + + @staticmethod + def apply_update(update, root_model): + """Apply an update ReactPy's internal DOM model.""" + from jsonpointer import set_pointer + + if update["path"]: + set_pointer(root_model, update["path"], update["model"]) + else: + root_model.update(update["model"]) + + def render(self, layout, model): + """Submit ReactPy's internal DOM model into the HTML DOM.""" + import js + from pyscript.js_modules import morphdom + + # Create a new container to render the layout into + container = js.document.getElementById(f"pyscript-{self.uuid}") + temp_container = container.cloneNode(False) + self.build_element_tree(layout, temp_container, model) + + # Use morphdom to update the DOM + morphdom.default(container, temp_container) + + # Remove the cloned container to prevent memory leaks + temp_container.remove() + + def build_element_tree(self, layout, parent, model): + """Recursively build an element tree, starting from the root component.""" + import js + + if isinstance(model, str): + parent.appendChild(js.document.createTextNode(model)) + elif isinstance(model, dict): + if not model["tagName"]: + for child in model.get("children", []): + self.build_element_tree(layout, parent, child) + return + tag = model["tagName"] + attributes = model.get("attributes", {}) + children = model.get("children", []) + element = js.document.createElement(tag) + for key, value in attributes.items(): + if key == "style": + for style_key, style_value in value.items(): + setattr(element.style, style_key, style_value) + elif key == "className": + element.className = value + else: + element.setAttribute(key, value) + for event_name, event_handler_model in model.get( + "eventHandlers", {} + ).items(): + self.create_event_handler( + layout, element, event_name, event_handler_model + ) + for child in children: + self.build_element_tree(layout, element, child) + parent.appendChild(element) + else: + raise ValueError(f"Unknown model type: {type(model)}") + + @staticmethod + def create_event_handler(layout, element, event_name, event_handler_model): + """Create an event handler for an element. This function is used as an + adapter between ReactPy and browser events.""" + from pyodide.ffi.wrappers import add_event_listener + + target = event_handler_model["target"] + + def event_handler(*args): + asyncio.create_task( + layout.deliver({"type": "layout-event", "target": target, "data": args}) + ) + + event_name = event_name.lstrip("on_").lower().replace("_", "") + add_event_listener(element, event_name, event_handler) + + @staticmethod + def delete_old_workspaces(): + """To prevent memory leaks, we must delete all user generated Python code when + it is no longer in use (removed from the page). To do this, we compare what + UUIDs exist on the DOM, versus what UUIDs exist within the PyScript global + interpreter.""" + import js + + dom_workspaces = js.document.querySelectorAll(".pyscript") + dom_uuids = {element.dataset.uuid for element in dom_workspaces} + python_uuids = { + value.split("_")[-1] + for value in globals() + if value.startswith("user_workspace_") + } + + # Delete the workspace if it exists at the moment when we check + for uuid in python_uuids - dom_uuids: + task_name = f"task_{uuid}" + if task_name in globals(): + task: asyncio.Task = globals()[task_name] + task.cancel() + del globals()[task_name] + else: + print(f"Warning: Could not auto delete PyScript task {task_name}") + + workspace_name = f"user_workspace_{uuid}" + if workspace_name in globals(): + del globals()[workspace_name] + else: + print( + f"Warning: Could not auto delete PyScript workspace {workspace_name}" + ) + + async def run(self, workspace_function): + """Run the layout handler. This function is main executor for all user generated code.""" + from reactpy.core.layout import Layout + + self.delete_old_workspaces() + root_model: dict = {} + + async with Layout(workspace_function()) as layout: + while True: + update = await layout.render() + self.apply_update(update, root_model) + self.render(layout, root_model) diff --git a/src/reactpy_django/static/reactpy_django/pyscript-custom.css b/src/reactpy_django/static/reactpy_django/pyscript-custom.css new file mode 100644 index 00000000..5793fd52 --- /dev/null +++ b/src/reactpy_django/static/reactpy_django/pyscript-custom.css @@ -0,0 +1,3 @@ +py-script { + display: none; +} diff --git a/src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css b/src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css new file mode 100644 index 00000000..9cd8541e --- /dev/null +++ b/src/reactpy_django/static/reactpy_django/pyscript-hide-debug.css @@ -0,0 +1,3 @@ +.py-error { + display: none; +} diff --git a/src/reactpy_django/templates/reactpy/pyscript_component.html b/src/reactpy_django/templates/reactpy/pyscript_component.html new file mode 100644 index 00000000..a4767040 --- /dev/null +++ b/src/reactpy_django/templates/reactpy/pyscript_component.html @@ -0,0 +1,2 @@ +
{{pyscript_initial_html}}
+{{pyscript_executor}} diff --git a/src/reactpy_django/templates/reactpy/pyscript_setup.html b/src/reactpy_django/templates/reactpy/pyscript_setup.html new file mode 100644 index 00000000..e258cf08 --- /dev/null +++ b/src/reactpy_django/templates/reactpy/pyscript_setup.html @@ -0,0 +1,8 @@ +{% load static %} + + +{% if not reactpy_debug_mode %} + +{% endif %} + +{{pyscript_layout_handler}} diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index e33d8387..1fdfa6af 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -3,16 +3,12 @@ from logging import getLogger from uuid import uuid4 -import dill as pickle from django import template from django.http import HttpRequest from django.urls import NoReverseMatch, reverse -from reactpy.backend.hooks import ConnectionContext -from reactpy.backend.types import Connection, Location -from reactpy.core.types import ComponentConstructor -from reactpy.utils import vdom_to_html +from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict -from reactpy_django import config, models +from reactpy_django import config as reactpy_config from reactpy_django.exceptions import ( ComponentCarrierError, ComponentDoesNotExistError, @@ -20,8 +16,17 @@ InvalidHostError, OfflineComponentMissing, ) -from reactpy_django.types import ComponentParams -from reactpy_django.utils import SyncLayout, strtobool, validate_component_args +from reactpy_django.utils import ( + PYSCRIPT_LAYOUT_HANDLER, + extend_pyscript_config, + prerender_component, + render_pyscript_template, + save_component_params, + strtobool, + validate_component_args, + validate_host, + vdom_or_component_to_string, +) try: RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/") @@ -37,7 +42,7 @@ def component( dotted_path: str, *args, host: str | None = None, - prerender: str = str(config.REACTPY_PRERENDER), + prerender: str = str(reactpy_config.REACTPY_PRERENDER), offline: str = "", **kwargs, ): @@ -73,7 +78,11 @@ def component( perceived_host = (request.get_host() if request else "").strip("/") host = ( host - or (next(config.REACTPY_DEFAULT_HOSTS) if config.REACTPY_DEFAULT_HOSTS else "") + or ( + next(reactpy_config.REACTPY_DEFAULT_HOSTS) + if reactpy_config.REACTPY_DEFAULT_HOSTS + else "" + ) ).strip("/") is_local = not host or host.startswith(perceived_host) uuid = str(uuid4()) @@ -84,7 +93,7 @@ def component( _offline_html = "" # Validate the host - if host and config.REACTPY_DEBUG_MODE: + if host and reactpy_config.REACTPY_DEBUG_MODE: try: validate_host(host) except InvalidHostError as e: @@ -92,14 +101,14 @@ def component( # Fetch the component if is_local: - user_component = config.REACTPY_REGISTERED_COMPONENTS.get(dotted_path) + user_component = reactpy_config.REACTPY_REGISTERED_COMPONENTS.get(dotted_path) if not user_component: msg = f"Component '{dotted_path}' is not registered as a root component. " _logger.error(msg) return failure_context(dotted_path, ComponentDoesNotExistError(msg)) # Validate the component args & kwargs - if is_local and config.REACTPY_DEBUG_MODE: + if is_local and reactpy_config.REACTPY_DEBUG_MODE: try: validate_component_args(user_component, *args, **kwargs) except ComponentParamError as e: @@ -140,7 +149,7 @@ def component( # Fetch the offline component's HTML, if requested if offline: - offline_component = config.REACTPY_REGISTERED_COMPONENTS.get(offline) + offline_component = reactpy_config.REACTPY_REGISTERED_COMPONENTS.get(offline) if not offline_component: msg = f"Cannot render offline component '{offline}'. It is not registered as a component." _logger.error(msg) @@ -159,63 +168,83 @@ def component( "reactpy_class": class_, "reactpy_uuid": uuid, "reactpy_host": host or perceived_host, - "reactpy_url_prefix": config.REACTPY_URL_PREFIX, + "reactpy_url_prefix": reactpy_config.REACTPY_URL_PREFIX, "reactpy_component_path": f"{dotted_path}/{uuid}/{int(has_args)}/", "reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH, - "reactpy_reconnect_interval": config.REACTPY_RECONNECT_INTERVAL, - "reactpy_reconnect_max_interval": config.REACTPY_RECONNECT_MAX_INTERVAL, - "reactpy_reconnect_backoff_multiplier": config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, - "reactpy_reconnect_max_retries": config.REACTPY_RECONNECT_MAX_RETRIES, + "reactpy_reconnect_interval": reactpy_config.REACTPY_RECONNECT_INTERVAL, + "reactpy_reconnect_max_interval": reactpy_config.REACTPY_RECONNECT_MAX_INTERVAL, + "reactpy_reconnect_backoff_multiplier": reactpy_config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, + "reactpy_reconnect_max_retries": reactpy_config.REACTPY_RECONNECT_MAX_RETRIES, "reactpy_prerender_html": _prerender_html, "reactpy_offline_html": _offline_html, } -def failure_context(dotted_path: str, error: Exception): - return { - "reactpy_failure": True, - "reactpy_debug_mode": config.REACTPY_DEBUG_MODE, - "reactpy_dotted_path": dotted_path, - "reactpy_error": type(error).__name__, - } - - -def save_component_params(args, kwargs, uuid): - params = ComponentParams(args, kwargs) - model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params)) - model.full_clean() - model.save() +@register.inclusion_tag("reactpy/pyscript_component.html", takes_context=True) +def pyscript_component( + context: template.RequestContext, + *file_paths: str, + initial: str | VdomDict | ComponentType = "", + root: str = "root", +): + """ + 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. + """ + if not file_paths: + raise ValueError( + "At least one file path must be provided to the 'pyscript_component' tag." + ) + uuid = uuid4().hex + request: HttpRequest | None = context.get("request") + initial = vdom_or_component_to_string(initial, request=request, uuid=uuid) + executor = render_pyscript_template(file_paths, uuid, root) -def validate_host(host: str): - if "://" in host: - protocol = host.split("://")[0] - msg = ( - f"Invalid host provided to component. Contains a protocol '{protocol}://'." - ) - _logger.error(msg) - raise InvalidHostError(msg) + return { + "pyscript_executor": executor, + "pyscript_uuid": uuid, + "pyscript_initial_html": initial, + } -def prerender_component( - user_component: ComponentConstructor, args, kwargs, uuid, request: HttpRequest +@register.inclusion_tag("reactpy/pyscript_setup.html") +def pyscript_setup( + *extra_py: str, + extra_js: str | dict = "", + config: str | dict = "", ): - search = request.GET.urlencode() - scope = getattr(request, "scope", {}) - scope["reactpy"] = {"id": str(uuid)} - - with SyncLayout( - ConnectionContext( - user_component(*args, **kwargs), - value=Connection( - scope=scope, - location=Location( - pathname=request.path, search=f"?{search}" if search else "" - ), - carrier=request, - ), - ) - ) as layout: - vdom_tree = layout.render()["model"] + """ + Args: + extra_py: Dependencies that need to be loaded on the page for \ + your PyScript components. Each dependency must be contained \ + within it's own string and written in Python requirements file syntax. + + Kwargs: + extra_js: A JSON string or Python dictionary containing a vanilla \ + JavaScript module URL and the `name: str` to access it within \ + `pyscript.js_modules.*`. + config: A JSON string or Python dictionary containing PyScript \ + configuration values. + """ + return { + "pyscript_config": extend_pyscript_config(extra_py, extra_js, config), + "pyscript_layout_handler": PYSCRIPT_LAYOUT_HANDLER, + "reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE, + } + - return vdom_to_html(vdom_tree) +def failure_context(dotted_path: str, error: Exception): + return { + "reactpy_failure": True, + "reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE, + "reactpy_dotted_path": dotted_path, + "reactpy_error": type(error).__name__, + } diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 73538ad7..48559e84 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -2,14 +2,23 @@ import contextlib import inspect +import json import logging import os import re +import textwrap from asyncio import iscoroutinefunction +from copy import deepcopy from fnmatch import fnmatch from importlib import import_module -from typing import Any, Callable, Sequence - +from pathlib import Path +from typing import Any, Callable, Mapping, Sequence +from uuid import UUID, uuid4 + +import dill +import jsonpointer +import orjson +import reactpy from asgiref.sync import async_to_sync from channels.db import database_sync_to_async from django.db.models import ManyToManyField, ManyToOneRel, prefetch_related_objects @@ -17,14 +26,19 @@ from django.db.models.query import QuerySet from django.http import HttpRequest, HttpResponse from django.template import engines +from django.templatetags.static import static from django.utils.encoding import smart_str from django.views import View +from reactpy import vdom_to_html +from reactpy.backend.hooks import ConnectionContext +from reactpy.backend.types import Connection, Location from reactpy.core.layout import Layout from reactpy.types import ComponentConstructor from reactpy_django.exceptions import ( ComponentDoesNotExistError, ComponentParamError, + InvalidHostError, ViewDoesNotExistError, ) @@ -44,6 +58,13 @@ + rf"({_OFFLINE_KWARG_PATTERN}|{_GENERIC_KWARG_PATTERN})*?" + r"\s*%}" ) +PYSCRIPT_COMPONENT_TEMPLATE = ( + Path(__file__).parent / "pyscript" / "component_template.py" +).read_text(encoding="utf-8") +PYSCRIPT_LAYOUT_HANDLER = ( + Path(__file__).parent / "pyscript" / "layout_handler.py" +).read_text(encoding="utf-8") +PYSCRIPT_DEFAULT_CONFIG: dict[str, Any] = {} async def render_view( @@ -381,3 +402,159 @@ def strtobool(val): return 0 else: raise ValueError(f"invalid truth value {val}") + + +def prerender_component( + user_component: ComponentConstructor, + args: Sequence, + kwargs: Mapping, + uuid: str | UUID, + request: HttpRequest, +) -> str: + """Prerenders a ReactPy component and returns the HTML string.""" + search = request.GET.urlencode() + scope = getattr(request, "scope", {}) + scope["reactpy"] = {"id": str(uuid)} + + with SyncLayout( + ConnectionContext( + user_component(*args, **kwargs), + value=Connection( + scope=scope, + location=Location( + pathname=request.path, search=f"?{search}" if search else "" + ), + carrier=request, + ), + ) + ) as layout: + vdom_tree = layout.render()["model"] + + return vdom_to_html(vdom_tree) + + +def vdom_or_component_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 + + if hasattr(vdom_or_component, "render"): + if not request: + request = HttpRequest() + request.method = "GET" + if not uuid: + uuid = uuid4().hex + return prerender_component(vdom_or_component, [], {}, uuid, request) + + if isinstance(vdom_or_component, str): + return vdom_or_component + + raise ValueError( + f"Invalid type for vdom_or_component: {type(vdom_or_component)}. " + "Expected a VdomDict, component, or string." + ) + + +def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str): + """Inserts the user's code into the PyScript template using pattern matching.""" + from django.core.cache import caches + + from reactpy_django.config import REACTPY_CACHE + + # Create a valid PyScript executor by replacing the template values + executor = PYSCRIPT_COMPONENT_TEMPLATE.replace("UUID", uuid) + executor = executor.replace("return root()", f"return {root}()") + + # Fetch the user's PyScript code + all_file_contents: list[str] = [] + for file_path in file_paths: + # Try to get user code from cache + cache_key = create_cache_key("pyscript", file_path) + last_modified_time = os.stat(file_path).st_mtime + file_contents: str = caches[REACTPY_CACHE].get( + cache_key, version=int(last_modified_time) + ) + if file_contents: + all_file_contents.append(file_contents) + + # If not cached, read from file system + else: + file_contents = Path(file_path).read_text(encoding="utf-8").strip() + all_file_contents.append(file_contents) + caches[REACTPY_CACHE].set( + cache_key, file_contents, version=int(last_modified_time) + ) + + # Prepare the PyScript code block + user_code = "\n".join(all_file_contents) # Combine all user code + user_code = user_code.replace("\t", " ") # Normalize the text + user_code = textwrap.indent(user_code, " ") # Add indentation to match template + + # Insert the user code into the PyScript template + return executor.replace(" def root(): ...", user_code) + + +def extend_pyscript_config( + extra_py: Sequence, extra_js: dict | str, config: dict | str +) -> str: + """Extends ReactPy's default PyScript config with user provided values.""" + # Lazily set up the initial config in to wait for Django's static file system + if not PYSCRIPT_DEFAULT_CONFIG: + PYSCRIPT_DEFAULT_CONFIG.update( + { + "packages": [ + f"reactpy=={reactpy.__version__}", + f"jsonpointer=={jsonpointer.__version__}", + "ssl", + ], + "js_modules": { + "main": { + static("reactpy_django/morphdom/morphdom-esm.js"): "morphdom" + } + }, + } + ) + + # Extend the Python dependency list + pyscript_config = deepcopy(PYSCRIPT_DEFAULT_CONFIG) + pyscript_config["packages"].extend(extra_py) + + # Extend the JavaScript dependency list + if extra_js and isinstance(extra_js, str): + pyscript_config["js_modules"]["main"].update(json.loads(extra_js)) + elif extra_js and isinstance(extra_js, dict): + pyscript_config["js_modules"]["main"].update(extra_py) + + # Update the config + if config and isinstance(config, str): + pyscript_config.update(json.loads(config)) + elif config and isinstance(config, dict): + pyscript_config.update(config) + return orjson.dumps(pyscript_config).decode("utf-8") + + +def save_component_params(args, kwargs, uuid) -> None: + """Saves the component parameters to the database. + This is used within our template tag in order to propogate + the parameters between the HTTP and WebSocket stack.""" + from reactpy_django import models + from reactpy_django.types import ComponentParams + + params = ComponentParams(args, kwargs) + model = models.ComponentSession(uuid=uuid, params=dill.dumps(params)) + model.full_clean() + model.save() + + +def validate_host(host: str) -> None: + """Validates the host string to ensure it does not contain a protocol.""" + if "://" in host: + protocol = host.split("://")[0] + msg = ( + f"Invalid host provided to component. Contains a protocol '{protocol}://'." + ) + _logger.error(msg) + raise InvalidHostError(msg) diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py index 86dbd107..392e6066 100644 --- a/tests/test_app/__init__.py +++ b/tests/test_app/__init__.py @@ -1,3 +1,4 @@ +import shutil from pathlib import Path from nodejs import npm @@ -6,3 +7,33 @@ js_dir = Path(__file__).parent.parent.parent / "src" / "js" assert npm.call(["install"], cwd=str(js_dir)) == 0 assert npm.call(["run", "build"], cwd=str(js_dir)) == 0 + +# Make sure the current PyScript distribution is always available +pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist" +pyscript_static_dir = ( + Path(__file__).parent.parent.parent + / "src" + / "reactpy_django" + / "static" + / "reactpy_django" + / "pyscript" +) +if not pyscript_static_dir.exists(): + pyscript_static_dir.mkdir() +for file in pyscript_dist.iterdir(): + shutil.copy(file, pyscript_static_dir / file.name) + +# Make sure the current Morphdom distrubiton is always available +morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist" +morphdom_static_dir = ( + Path(__file__).parent.parent.parent + / "src" + / "reactpy_django" + / "static" + / "reactpy_django" + / "morphdom" +) +if not morphdom_static_dir.exists(): + morphdom_static_dir.mkdir() +for file in morphdom_dist.iterdir(): + shutil.copy(file, morphdom_static_dir / file.name) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index fe6df2f0..69b1541c 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -724,10 +724,10 @@ async def on_submit(event): ), }, html.div("use_user_data"), - html.button({"class": "login-1", "on_click": login_user1}, "Login 1"), - html.button({"class": "login-2", "on_click": login_user2}, "Login 2"), - html.button({"class": "logout", "on_click": logout_user}, "Logout"), - html.button({"class": "clear", "on_click": clear_data}, "Clear Data"), + html.button({"className": "login-1", "on_click": login_user1}, "Login 1"), + html.button({"className": "login-2", "on_click": login_user2}, "Login 2"), + html.button({"className": "logout", "on_click": logout_user}, "Logout"), + html.button({"className": "clear", "on_click": clear_data}, "Clear Data"), html.div(f"User: {current_user}"), html.div(f"Data: {user_data_query.data}"), html.div( @@ -792,8 +792,8 @@ async def on_submit(event): ), }, html.div("use_user_data_with_default"), - html.button({"class": "login-3", "on_click": login_user3}, "Login 3"), - html.button({"class": "clear", "on_click": clear_data}, "Clear Data"), + html.button({"className": "login-3", "on_click": login_user3}, "Login 3"), + html.button({"className": "clear", "on_click": clear_data}, "Clear Data"), html.div(f"User: {current_user}"), html.div(f"Data: {user_data_query.data}"), html.div( diff --git a/tests/test_app/pyscript/__init__.py b/tests/test_app/pyscript/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/pyscript/components/__init__.py b/tests/test_app/pyscript/components/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_app/pyscript/components/child.py b/tests/test_app/pyscript/components/child.py new file mode 100644 index 00000000..1f4a7824 --- /dev/null +++ b/tests/test_app/pyscript/components/child.py @@ -0,0 +1,25 @@ +from reactpy import component, html, use_state + + +@component +def root(): + value, set_value = use_state(0) + return html.article( + {"id": "child"}, + "This was embedded via a server-side component.", + html.div( + {"className": "grid"}, + html.button( + {"className": "plus", "on_click": lambda event: set_value(value + 1)}, + "+", + ), + html.button( + {"className": "minus", "on_click": lambda event: set_value(value - 1)}, + "-", + ), + ), + "Current value", + html.pre( + {"style": {"font-style": "bold"}, "data-value": str(value)}, str(value) + ), + ) diff --git a/tests/test_app/pyscript/components/counter.py b/tests/test_app/pyscript/components/counter.py new file mode 100644 index 00000000..31df55a1 --- /dev/null +++ b/tests/test_app/pyscript/components/counter.py @@ -0,0 +1,24 @@ +from reactpy import component, html, use_state + + +@component +def root(): + value, set_value = use_state(0) + return html.article( + {"id": "counter"}, + html.div( + {"className": "grid"}, + html.button( + {"className": "plus", "on_click": lambda event: set_value(value + 1)}, + "+", + ), + html.button( + {"className": "minus", "on_click": lambda event: set_value(value - 1)}, + "-", + ), + ), + "Current value", + html.pre( + {"style": {"font-style": "bold"}, "data-value": str(value)}, str(value) + ), + ) diff --git a/tests/test_app/pyscript/components/custom_root.py b/tests/test_app/pyscript/components/custom_root.py new file mode 100644 index 00000000..ee44fde4 --- /dev/null +++ b/tests/test_app/pyscript/components/custom_root.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def main(): + return html.div({"id": "custom-root"}, "Component with a custom root name.") diff --git a/tests/test_app/pyscript/components/hello_world.py b/tests/test_app/pyscript/components/hello_world.py new file mode 100644 index 00000000..d8c36ee8 --- /dev/null +++ b/tests/test_app/pyscript/components/hello_world.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def root(): + return html.div({"id": "hello-world"}, "hello world") diff --git a/tests/test_app/pyscript/components/multifile_child.py b/tests/test_app/pyscript/components/multifile_child.py new file mode 100644 index 00000000..4658e8a2 --- /dev/null +++ b/tests/test_app/pyscript/components/multifile_child.py @@ -0,0 +1,6 @@ +from reactpy import component, html + + +@component +def child(): + return html.div({"id": "multifile-child"}, "Multifile child") diff --git a/tests/test_app/pyscript/components/multifile_parent.py b/tests/test_app/pyscript/components/multifile_parent.py new file mode 100644 index 00000000..48a1b1d8 --- /dev/null +++ b/tests/test_app/pyscript/components/multifile_parent.py @@ -0,0 +1,11 @@ +from typing import TYPE_CHECKING + +from reactpy import component, html + +if TYPE_CHECKING: + from .multifile_child import child + + +@component +def root(): + return html.div({"id": "multifile-parent"}, "Multifile root", child()) diff --git a/tests/test_app/pyscript/components/remote_js_module.py b/tests/test_app/pyscript/components/remote_js_module.py new file mode 100644 index 00000000..26eccf03 --- /dev/null +++ b/tests/test_app/pyscript/components/remote_js_module.py @@ -0,0 +1,14 @@ +from reactpy import component, html + + +@component +def root(): + from pyscript.js_modules import moment + + time: str = moment.default().format("YYYY-MM-DD HH:mm:ss") + + return html.div( + {"id": "moment", "data-success": bool(time)}, + "Using the JavaScript package 'moment' to calculate time: ", + time, + ) diff --git a/tests/test_app/pyscript/components/server_side.py b/tests/test_app/pyscript/components/server_side.py new file mode 100644 index 00000000..fe31d527 --- /dev/null +++ b/tests/test_app/pyscript/components/server_side.py @@ -0,0 +1,33 @@ +from reactpy import component, html, use_state +from reactpy_django.components import pyscript_component + + +@component +def parent(): + return html.div( + {"id": "parent"}, + pyscript_component("./test_app/pyscript/components/child.py"), + ) + + +@component +def parent_toggle(): + state, set_state = use_state(False) + + if not state: + return html.div( + {"id": "parent-toggle"}, + html.button( + {"onClick": lambda x: set_state(not state)}, + "Click to show/hide", + ), + ) + + return html.div( + {"id": "parent-toggle"}, + html.button( + {"onClick": lambda x: set_state(not state)}, + "Click to show/hide", + ), + pyscript_component("./test_app/pyscript/components/child.py"), + ) diff --git a/tests/test_app/pyscript/urls.py b/tests/test_app/pyscript/urls.py new file mode 100644 index 00000000..e71c18e5 --- /dev/null +++ b/tests/test_app/pyscript/urls.py @@ -0,0 +1,7 @@ +from django.urls import re_path + +from test_app.pyscript.views import pyscript + +urlpatterns = [ + re_path(r"^pyscript/(?P.*)/?$", pyscript), +] diff --git a/tests/test_app/pyscript/views.py b/tests/test_app/pyscript/views.py new file mode 100644 index 00000000..f4891be1 --- /dev/null +++ b/tests/test_app/pyscript/views.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + + +def pyscript(request, path=None): + return render(request, "pyscript.html", {}) diff --git a/tests/test_app/static/moment.js b/tests/test_app/static/moment.js new file mode 100644 index 00000000..956eeb3d --- /dev/null +++ b/tests/test_app/static/moment.js @@ -0,0 +1,5680 @@ +//! moment.js +//! version : 2.30.1 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com + +var hookCallback; + +function hooks() { + return hookCallback.apply(null, arguments); +} + +// This is done to register the method called with moment() +// without creating circular dependencies. +function setHookCallback(callback) { + hookCallback = callback; +} + +function isArray(input) { + return ( + input instanceof Array || + Object.prototype.toString.call(input) === '[object Array]' + ); +} + +function isObject(input) { + // IE8 will treat undefined and null as object if it wasn't for + // input != null + return ( + input != null && + Object.prototype.toString.call(input) === '[object Object]' + ); +} + +function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); +} + +function isObjectEmpty(obj) { + if (Object.getOwnPropertyNames) { + return Object.getOwnPropertyNames(obj).length === 0; + } else { + var k; + for (k in obj) { + if (hasOwnProp(obj, k)) { + return false; + } + } + return true; + } +} + +function isUndefined(input) { + return input === void 0; +} + +function isNumber(input) { + return ( + typeof input === 'number' || + Object.prototype.toString.call(input) === '[object Number]' + ); +} + +function isDate(input) { + return ( + input instanceof Date || + Object.prototype.toString.call(input) === '[object Date]' + ); +} + +function map(arr, fn) { + var res = [], + i, + arrLen = arr.length; + for (i = 0; i < arrLen; ++i) { + res.push(fn(arr[i], i)); + } + return res; +} + +function extend(a, b) { + for (var i in b) { + if (hasOwnProp(b, i)) { + a[i] = b[i]; + } + } + + if (hasOwnProp(b, 'toString')) { + a.toString = b.toString; + } + + if (hasOwnProp(b, 'valueOf')) { + a.valueOf = b.valueOf; + } + + return a; +} + +function createUTC(input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); +} + +function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty: false, + unusedTokens: [], + unusedInput: [], + overflow: -2, + charsLeftOver: 0, + nullInput: false, + invalidEra: null, + invalidMonth: null, + invalidFormat: false, + userInvalidated: false, + iso: false, + parsedDateParts: [], + era: null, + meridiem: null, + rfc2822: false, + weekdayMismatch: false, + }; +} + +function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; +} + +var some; +if (Array.prototype.some) { + some = Array.prototype.some; +} else { + some = function (fun) { + var t = Object(this), + len = t.length >>> 0, + i; + + for (i = 0; i < len; i++) { + if (i in t && fun.call(this, t[i], i, t)) { + return true; + } + } + + return false; + }; +} + +function isValid(m) { + var flags = null, + parsedParts = false, + isNowValid = m._d && !isNaN(m._d.getTime()); + if (isNowValid) { + flags = getParsingFlags(m); + parsedParts = some.call(flags.parsedDateParts, function (i) { + return i != null; + }); + isNowValid = + flags.overflow < 0 && + !flags.empty && + !flags.invalidEra && + !flags.invalidMonth && + !flags.invalidWeekday && + !flags.weekdayMismatch && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated && + (!flags.meridiem || (flags.meridiem && parsedParts)); + if (m._strict) { + isNowValid = + isNowValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + } + if (Object.isFrozen == null || !Object.isFrozen(m)) { + m._isValid = isNowValid; + } else { + return isNowValid; + } + return m._isValid; +} + +function createInvalid(flags) { + var m = createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } else { + getParsingFlags(m).userInvalidated = true; + } + + return m; +} + +// Plugins that add properties should also add the key here (null value), +// so we can properly clone ourselves. +var momentProperties = (hooks.momentProperties = []), + updateInProgress = false; + +function copyConfig(to, from) { + var i, + prop, + val, + momentPropertiesLen = momentProperties.length; + + if (!isUndefined(from._isAMomentObject)) { + to._isAMomentObject = from._isAMomentObject; + } + if (!isUndefined(from._i)) { + to._i = from._i; + } + if (!isUndefined(from._f)) { + to._f = from._f; + } + if (!isUndefined(from._l)) { + to._l = from._l; + } + if (!isUndefined(from._strict)) { + to._strict = from._strict; + } + if (!isUndefined(from._tzm)) { + to._tzm = from._tzm; + } + if (!isUndefined(from._isUTC)) { + to._isUTC = from._isUTC; + } + if (!isUndefined(from._offset)) { + to._offset = from._offset; + } + if (!isUndefined(from._pf)) { + to._pf = getParsingFlags(from); + } + if (!isUndefined(from._locale)) { + to._locale = from._locale; + } + + if (momentPropertiesLen > 0) { + for (i = 0; i < momentPropertiesLen; i++) { + prop = momentProperties[i]; + val = from[prop]; + if (!isUndefined(val)) { + to[prop] = val; + } + } + } + + return to; +} + +// Moment prototype object +function Moment(config) { + copyConfig(this, config); + this._d = new Date(config._d != null ? config._d.getTime() : NaN); + if (!this.isValid()) { + this._d = new Date(NaN); + } + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + hooks.updateOffset(this); + updateInProgress = false; + } +} + +function isMoment(obj) { + return ( + obj instanceof Moment || (obj != null && obj._isAMomentObject != null) + ); +} + +function warn(msg) { + if ( + hooks.suppressDeprecationWarnings === false && + typeof console !== 'undefined' && + console.warn + ) { + console.warn('Deprecation warning: ' + msg); + } +} + +function deprecate(msg, fn) { + var firstTime = true; + + return extend(function () { + if (hooks.deprecationHandler != null) { + hooks.deprecationHandler(null, msg); + } + if (firstTime) { + var args = [], + arg, + i, + key, + argLen = arguments.length; + for (i = 0; i < argLen; i++) { + arg = ''; + if (typeof arguments[i] === 'object') { + arg += '\n[' + i + '] '; + for (key in arguments[0]) { + if (hasOwnProp(arguments[0], key)) { + arg += key + ': ' + arguments[0][key] + ', '; + } + } + arg = arg.slice(0, -2); // Remove trailing comma and space + } else { + arg = arguments[i]; + } + args.push(arg); + } + warn( + msg + + '\nArguments: ' + + Array.prototype.slice.call(args).join('') + + '\n' + + new Error().stack + ); + firstTime = false; + } + return fn.apply(this, arguments); + }, fn); +} + +var deprecations = {}; + +function deprecateSimple(name, msg) { + if (hooks.deprecationHandler != null) { + hooks.deprecationHandler(name, msg); + } + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; + } +} + +hooks.suppressDeprecationWarnings = false; +hooks.deprecationHandler = null; + +function isFunction(input) { + return ( + (typeof Function !== 'undefined' && input instanceof Function) || + Object.prototype.toString.call(input) === '[object Function]' + ); +} + +function set(config) { + var prop, i; + for (i in config) { + if (hasOwnProp(config, i)) { + prop = config[i]; + if (isFunction(prop)) { + this[i] = prop; + } else { + this['_' + i] = prop; + } + } + } + this._config = config; + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _dayOfMonthOrdinalParse. + // TODO: Remove "ordinalParse" fallback in next major release. + this._dayOfMonthOrdinalParseLenient = new RegExp( + (this._dayOfMonthOrdinalParse.source || this._ordinalParse.source) + + '|' + + /\d{1,2}/.source + ); +} + +function mergeConfigs(parentConfig, childConfig) { + var res = extend({}, parentConfig), + prop; + for (prop in childConfig) { + if (hasOwnProp(childConfig, prop)) { + if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { + res[prop] = {}; + extend(res[prop], parentConfig[prop]); + extend(res[prop], childConfig[prop]); + } else if (childConfig[prop] != null) { + res[prop] = childConfig[prop]; + } else { + delete res[prop]; + } + } + } + for (prop in parentConfig) { + if ( + hasOwnProp(parentConfig, prop) && + !hasOwnProp(childConfig, prop) && + isObject(parentConfig[prop]) + ) { + // make sure changes to properties don't modify parent config + res[prop] = extend({}, res[prop]); + } + } + return res; +} + +function Locale(config) { + if (config != null) { + this.set(config); + } +} + +var keys; + +if (Object.keys) { + keys = Object.keys; +} else { + keys = function (obj) { + var i, + res = []; + for (i in obj) { + if (hasOwnProp(obj, i)) { + res.push(i); + } + } + return res; + }; +} + +var defaultCalendar = { + sameDay: '[Today at] LT', + nextDay: '[Tomorrow at] LT', + nextWeek: 'dddd [at] LT', + lastDay: '[Yesterday at] LT', + lastWeek: '[Last] dddd [at] LT', + sameElse: 'L', +}; + +function calendar(key, mom, now) { + var output = this._calendar[key] || this._calendar['sameElse']; + return isFunction(output) ? output.call(mom, now) : output; +} + +function zeroFill(number, targetLength, forceSign) { + var absNumber = '' + Math.abs(number), + zerosToFill = targetLength - absNumber.length, + sign = number >= 0; + return ( + (sign ? (forceSign ? '+' : '') : '-') + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + + absNumber + ); +} + +var formattingTokens = + /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g, + localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, + formatFunctions = {}, + formatTokenFunctions = {}; + +// token: 'M' +// padded: ['MM', 2] +// ordinal: 'Mo' +// callback: function () { this.month() + 1 } +function addFormatToken(token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal( + func.apply(this, arguments), + token + ); + }; + } +} + +function removeFormattingTokens(input) { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); +} + +function makeFormatFunction(format) { + var array = format.match(formattingTokens), + i, + length; + + for (i = 0, length = array.length; i < length; i++) { + if (formatTokenFunctions[array[i]]) { + array[i] = formatTokenFunctions[array[i]]; + } else { + array[i] = removeFormattingTokens(array[i]); + } + } + + return function (mom) { + var output = '', + i; + for (i = 0; i < length; i++) { + output += isFunction(array[i]) + ? array[i].call(mom, format) + : array[i]; + } + return output; + }; +} + +// format date using native date object +function formatMoment(m, format) { + if (!m.isValid()) { + return m.localeData().invalidDate(); + } + + format = expandFormat(format, m.localeData()); + formatFunctions[format] = + formatFunctions[format] || makeFormatFunction(format); + + return formatFunctions[format](m); +} + +function expandFormat(format, locale) { + var i = 5; + + function replaceLongDateFormatTokens(input) { + return locale.longDateFormat(input) || input; + } + + localFormattingTokens.lastIndex = 0; + while (i >= 0 && localFormattingTokens.test(format)) { + format = format.replace( + localFormattingTokens, + replaceLongDateFormatTokens + ); + localFormattingTokens.lastIndex = 0; + i -= 1; + } + + return format; +} + +var defaultLongDateFormat = { + LTS: 'h:mm:ss A', + LT: 'h:mm A', + L: 'MM/DD/YYYY', + LL: 'MMMM D, YYYY', + LLL: 'MMMM D, YYYY h:mm A', + LLLL: 'dddd, MMMM D, YYYY h:mm A', +}; + +function longDateFormat(key) { + var format = this._longDateFormat[key], + formatUpper = this._longDateFormat[key.toUpperCase()]; + + if (format || !formatUpper) { + return format; + } + + this._longDateFormat[key] = formatUpper + .match(formattingTokens) + .map(function (tok) { + if ( + tok === 'MMMM' || + tok === 'MM' || + tok === 'DD' || + tok === 'dddd' + ) { + return tok.slice(1); + } + return tok; + }) + .join(''); + + return this._longDateFormat[key]; +} + +var defaultInvalidDate = 'Invalid date'; + +function invalidDate() { + return this._invalidDate; +} + +var defaultOrdinal = '%d', + defaultDayOfMonthOrdinalParse = /\d{1,2}/; + +function ordinal(number) { + return this._ordinal.replace('%d', number); +} + +var defaultRelativeTime = { + future: 'in %s', + past: '%s ago', + s: 'a few seconds', + ss: '%d seconds', + m: 'a minute', + mm: '%d minutes', + h: 'an hour', + hh: '%d hours', + d: 'a day', + dd: '%d days', + w: 'a week', + ww: '%d weeks', + M: 'a month', + MM: '%d months', + y: 'a year', + yy: '%d years', +}; + +function relativeTime(number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return isFunction(output) + ? output(number, withoutSuffix, string, isFuture) + : output.replace(/%d/i, number); +} + +function pastFuture(diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return isFunction(format) ? format(output) : format.replace(/%s/i, output); +} + +var aliases = { + D: 'date', + dates: 'date', + date: 'date', + d: 'day', + days: 'day', + day: 'day', + e: 'weekday', + weekdays: 'weekday', + weekday: 'weekday', + E: 'isoWeekday', + isoweekdays: 'isoWeekday', + isoweekday: 'isoWeekday', + DDD: 'dayOfYear', + dayofyears: 'dayOfYear', + dayofyear: 'dayOfYear', + h: 'hour', + hours: 'hour', + hour: 'hour', + ms: 'millisecond', + milliseconds: 'millisecond', + millisecond: 'millisecond', + m: 'minute', + minutes: 'minute', + minute: 'minute', + M: 'month', + months: 'month', + month: 'month', + Q: 'quarter', + quarters: 'quarter', + quarter: 'quarter', + s: 'second', + seconds: 'second', + second: 'second', + gg: 'weekYear', + weekyears: 'weekYear', + weekyear: 'weekYear', + GG: 'isoWeekYear', + isoweekyears: 'isoWeekYear', + isoweekyear: 'isoWeekYear', + w: 'week', + weeks: 'week', + week: 'week', + W: 'isoWeek', + isoweeks: 'isoWeek', + isoweek: 'isoWeek', + y: 'year', + years: 'year', + year: 'year', +}; + +function normalizeUnits(units) { + return typeof units === 'string' + ? aliases[units] || aliases[units.toLowerCase()] + : undefined; +} + +function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; +} + +var priorities = { + date: 9, + day: 11, + weekday: 11, + isoWeekday: 11, + dayOfYear: 4, + hour: 13, + millisecond: 16, + minute: 14, + month: 8, + quarter: 7, + second: 15, + weekYear: 1, + isoWeekYear: 1, + week: 5, + isoWeek: 5, + year: 1, +}; + +function getPrioritizedUnits(unitsObj) { + var units = [], + u; + for (u in unitsObj) { + if (hasOwnProp(unitsObj, u)) { + units.push({ unit: u, priority: priorities[u] }); + } + } + units.sort(function (a, b) { + return a.priority - b.priority; + }); + return units; +} + +var match1 = /\d/, // 0 - 9 + match2 = /\d\d/, // 00 - 99 + match3 = /\d{3}/, // 000 - 999 + match4 = /\d{4}/, // 0000 - 9999 + match6 = /[+-]?\d{6}/, // -999999 - 999999 + match1to2 = /\d\d?/, // 0 - 99 + match3to4 = /\d\d\d\d?/, // 999 - 9999 + match5to6 = /\d\d\d\d\d\d?/, // 99999 - 999999 + match1to3 = /\d{1,3}/, // 0 - 999 + match1to4 = /\d{1,4}/, // 0 - 9999 + match1to6 = /[+-]?\d{1,6}/, // -999999 - 999999 + matchUnsigned = /\d+/, // 0 - inf + matchSigned = /[+-]?\d+/, // -inf - inf + matchOffset = /Z|[+-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z + matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi, // +00 -00 +00:00 -00:00 +0000 -0000 or Z + matchTimestamp = /[+-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 + // any word (or two) characters or numbers including two/three word month in arabic. + // includes scottish gaelic two word and hyphenated months + matchWord = + /[0-9]{0,256}['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFF07\uFF10-\uFFEF]{1,256}|[\u0600-\u06FF\/]{1,256}(\s*?[\u0600-\u06FF]{1,256}){1,2}/i, + match1to2NoLeadingZero = /^[1-9]\d?/, // 1-99 + match1to2HasZero = /^([1-9]\d|\d)/, // 0-99 + regexes; + +regexes = {}; + +function addRegexToken(token, regex, strictRegex) { + regexes[token] = isFunction(regex) + ? regex + : function (isStrict, localeData) { + return isStrict && strictRegex ? strictRegex : regex; + }; +} + +function getParseRegexForToken(token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } + + return regexes[token](config._strict, config._locale); +} + +// Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript +function unescapeFormat(s) { + return regexEscape( + s + .replace('\\', '') + .replace( + /\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, + function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + } + ) + ); +} + +function regexEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +function absFloor(number) { + if (number < 0) { + // -0 -> 0 + return Math.ceil(number) || 0; + } else { + return Math.floor(number); + } +} + +function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + value = absFloor(coercedNumber); + } + + return value; +} + +var tokens = {}; + +function addParseToken(token, callback) { + var i, + func = callback, + tokenLen; + if (typeof token === 'string') { + token = [token]; + } + if (isNumber(callback)) { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + tokenLen = token.length; + for (i = 0; i < tokenLen; i++) { + tokens[token[i]] = func; + } +} + +function addWeekParseToken(token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); +} + +function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } +} + +function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +var YEAR = 0, + MONTH = 1, + DATE = 2, + HOUR = 3, + MINUTE = 4, + SECOND = 5, + MILLISECOND = 6, + WEEK = 7, + WEEKDAY = 8; + +// FORMATTING + +addFormatToken('Y', 0, 0, function () { + var y = this.year(); + return y <= 9999 ? zeroFill(y, 4) : '+' + y; +}); + +addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; +}); + +addFormatToken(0, ['YYYY', 4], 0, 'year'); +addFormatToken(0, ['YYYYY', 5], 0, 'year'); +addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + +// PARSING + +addRegexToken('Y', matchSigned); +addRegexToken('YY', match1to2, match2); +addRegexToken('YYYY', match1to4, match4); +addRegexToken('YYYYY', match1to6, match6); +addRegexToken('YYYYYY', match1to6, match6); + +addParseToken(['YYYYY', 'YYYYYY'], YEAR); +addParseToken('YYYY', function (input, array) { + array[YEAR] = + input.length === 2 ? hooks.parseTwoDigitYear(input) : toInt(input); +}); +addParseToken('YY', function (input, array) { + array[YEAR] = hooks.parseTwoDigitYear(input); +}); +addParseToken('Y', function (input, array) { + array[YEAR] = parseInt(input, 10); +}); + +// HELPERS + +function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; +} + +// HOOKS + +hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); +}; + +// MOMENTS + +var getSetYear = makeGetSet('FullYear', true); + +function getIsLeapYear() { + return isLeapYear(this.year()); +} + +function makeGetSet(unit, keepTime) { + return function (value) { + if (value != null) { + set$1(this, unit, value); + hooks.updateOffset(this, keepTime); + return this; + } else { + return get(this, unit); + } + }; +} + +function get(mom, unit) { + if (!mom.isValid()) { + return NaN; + } + + var d = mom._d, + isUTC = mom._isUTC; + + switch (unit) { + case 'Milliseconds': + return isUTC ? d.getUTCMilliseconds() : d.getMilliseconds(); + case 'Seconds': + return isUTC ? d.getUTCSeconds() : d.getSeconds(); + case 'Minutes': + return isUTC ? d.getUTCMinutes() : d.getMinutes(); + case 'Hours': + return isUTC ? d.getUTCHours() : d.getHours(); + case 'Date': + return isUTC ? d.getUTCDate() : d.getDate(); + case 'Day': + return isUTC ? d.getUTCDay() : d.getDay(); + case 'Month': + return isUTC ? d.getUTCMonth() : d.getMonth(); + case 'FullYear': + return isUTC ? d.getUTCFullYear() : d.getFullYear(); + default: + return NaN; // Just in case + } +} + +function set$1(mom, unit, value) { + var d, isUTC, year, month, date; + + if (!mom.isValid() || isNaN(value)) { + return; + } + + d = mom._d; + isUTC = mom._isUTC; + + switch (unit) { + case 'Milliseconds': + return void (isUTC + ? d.setUTCMilliseconds(value) + : d.setMilliseconds(value)); + case 'Seconds': + return void (isUTC ? d.setUTCSeconds(value) : d.setSeconds(value)); + case 'Minutes': + return void (isUTC ? d.setUTCMinutes(value) : d.setMinutes(value)); + case 'Hours': + return void (isUTC ? d.setUTCHours(value) : d.setHours(value)); + case 'Date': + return void (isUTC ? d.setUTCDate(value) : d.setDate(value)); + // case 'Day': // Not real + // return void (isUTC ? d.setUTCDay(value) : d.setDay(value)); + // case 'Month': // Not used because we need to pass two variables + // return void (isUTC ? d.setUTCMonth(value) : d.setMonth(value)); + case 'FullYear': + break; // See below ... + default: + return; // Just in case + } + + year = value; + month = mom.month(); + date = mom.date(); + date = date === 29 && month === 1 && !isLeapYear(year) ? 28 : date; + void (isUTC + ? d.setUTCFullYear(year, month, date) + : d.setFullYear(year, month, date)); +} + +// MOMENTS + +function stringGet(units) { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](); + } + return this; +} + +function stringSet(units, value) { + if (typeof units === 'object') { + units = normalizeObjectUnits(units); + var prioritized = getPrioritizedUnits(units), + i, + prioritizedLen = prioritized.length; + for (i = 0; i < prioritizedLen; i++) { + this[prioritized[i].unit](units[prioritized[i].unit]); + } + } else { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](value); + } + } + return this; +} + +function mod(n, x) { + return ((n % x) + x) % x; +} + +var indexOf; + +if (Array.prototype.indexOf) { + indexOf = Array.prototype.indexOf; +} else { + indexOf = function (o) { + // I know + var i; + for (i = 0; i < this.length; ++i) { + if (this[i] === o) { + return i; + } + } + return -1; + }; +} + +function daysInMonth(year, month) { + if (isNaN(year) || isNaN(month)) { + return NaN; + } + var modMonth = mod(month, 12); + year += (month - modMonth) / 12; + return modMonth === 1 + ? isLeapYear(year) + ? 29 + : 28 + : 31 - ((modMonth % 7) % 2); +} + +// FORMATTING + +addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; +}); + +addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); +}); + +addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); +}); + +// PARSING + +addRegexToken('M', match1to2, match1to2NoLeadingZero); +addRegexToken('MM', match1to2, match2); +addRegexToken('MMM', function (isStrict, locale) { + return locale.monthsShortRegex(isStrict); +}); +addRegexToken('MMMM', function (isStrict, locale) { + return locale.monthsRegex(isStrict); +}); + +addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; +}); + +addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } +}); + +// LOCALES + +var defaultLocaleMonths = + 'January_February_March_April_May_June_July_August_September_October_November_December'.split( + '_' + ), + defaultLocaleMonthsShort = + 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/, + defaultMonthsShortRegex = matchWord, + defaultMonthsRegex = matchWord; + +function localeMonths(m, format) { + if (!m) { + return isArray(this._months) + ? this._months + : this._months['standalone']; + } + return isArray(this._months) + ? this._months[m.month()] + : this._months[ + (this._months.isFormat || MONTHS_IN_FORMAT).test(format) + ? 'format' + : 'standalone' + ][m.month()]; +} + +function localeMonthsShort(m, format) { + if (!m) { + return isArray(this._monthsShort) + ? this._monthsShort + : this._monthsShort['standalone']; + } + return isArray(this._monthsShort) + ? this._monthsShort[m.month()] + : this._monthsShort[ + MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone' + ][m.month()]; +} + +function handleStrictParse(monthName, format, strict) { + var i, + ii, + mom, + llc = monthName.toLocaleLowerCase(); + if (!this._monthsParse) { + // this is not used + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + for (i = 0; i < 12; ++i) { + mom = createUTC([2000, i]); + this._shortMonthsParse[i] = this.monthsShort( + mom, + '' + ).toLocaleLowerCase(); + this._longMonthsParse[i] = this.months(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'MMM') { + ii = indexOf.call(this._shortMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._longMonthsParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._longMonthsParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortMonthsParse, llc); + return ii !== -1 ? ii : null; + } + } +} + +function localeMonthsParse(monthName, format, strict) { + var i, mom, regex; + + if (this._monthsParseExact) { + return handleStrictParse.call(this, monthName, format, strict); + } + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + // TODO: add sorting + // Sorting makes sure if one month (or abbr) is a prefix of another + // see sorting in computeMonthsParse + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp( + '^' + this.months(mom, '').replace('.', '') + '$', + 'i' + ); + this._shortMonthsParse[i] = new RegExp( + '^' + this.monthsShort(mom, '').replace('.', '') + '$', + 'i' + ); + } + if (!strict && !this._monthsParse[i]) { + regex = + '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if ( + strict && + format === 'MMMM' && + this._longMonthsParse[i].test(monthName) + ) { + return i; + } else if ( + strict && + format === 'MMM' && + this._shortMonthsParse[i].test(monthName) + ) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; + } + } +} + +// MOMENTS + +function setMonth(mom, value) { + if (!mom.isValid()) { + // No op + return mom; + } + + if (typeof value === 'string') { + if (/^\d+$/.test(value)) { + value = toInt(value); + } else { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (!isNumber(value)) { + return mom; + } + } + } + + var month = value, + date = mom.date(); + + date = date < 29 ? date : Math.min(date, daysInMonth(mom.year(), month)); + void (mom._isUTC + ? mom._d.setUTCMonth(month, date) + : mom._d.setMonth(month, date)); + return mom; +} + +function getSetMonth(value) { + if (value != null) { + setMonth(this, value); + hooks.updateOffset(this, true); + return this; + } else { + return get(this, 'Month'); + } +} + +function getDaysInMonth() { + return daysInMonth(this.year(), this.month()); +} + +function monthsShortRegex(isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsShortStrictRegex; + } else { + return this._monthsShortRegex; + } + } else { + if (!hasOwnProp(this, '_monthsShortRegex')) { + this._monthsShortRegex = defaultMonthsShortRegex; + } + return this._monthsShortStrictRegex && isStrict + ? this._monthsShortStrictRegex + : this._monthsShortRegex; + } +} + +function monthsRegex(isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsStrictRegex; + } else { + return this._monthsRegex; + } + } else { + if (!hasOwnProp(this, '_monthsRegex')) { + this._monthsRegex = defaultMonthsRegex; + } + return this._monthsStrictRegex && isStrict + ? this._monthsStrictRegex + : this._monthsRegex; + } +} + +function computeMonthsParse() { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var shortPieces = [], + longPieces = [], + mixedPieces = [], + i, + mom, + shortP, + longP; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, i]); + shortP = regexEscape(this.monthsShort(mom, '')); + longP = regexEscape(this.months(mom, '')); + shortPieces.push(shortP); + longPieces.push(longP); + mixedPieces.push(longP); + mixedPieces.push(shortP); + } + // Sorting makes sure if one month (or abbr) is a prefix of another it + // will match the longer piece. + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + + this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._monthsShortRegex = this._monthsRegex; + this._monthsStrictRegex = new RegExp( + '^(' + longPieces.join('|') + ')', + 'i' + ); + this._monthsShortStrictRegex = new RegExp( + '^(' + shortPieces.join('|') + ')', + 'i' + ); +} + +function createDate(y, m, d, h, M, s, ms) { + // can't just apply() to create a date: + // https://stackoverflow.com/q/181348 + var date; + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + date = new Date(y + 400, m, d, h, M, s, ms); + if (isFinite(date.getFullYear())) { + date.setFullYear(y); + } + } else { + date = new Date(y, m, d, h, M, s, ms); + } + + return date; +} + +function createUTCDate(y) { + var date, args; + // the Date.UTC function remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + args = Array.prototype.slice.call(arguments); + // preserve leap years using a full 400 year cycle, then reset + args[0] = y + 400; + date = new Date(Date.UTC.apply(null, args)); + if (isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + } else { + date = new Date(Date.UTC.apply(null, arguments)); + } + + return date; +} + +// start-of-first-week - start-of-year +function firstWeekOffset(year, dow, doy) { + var // first-week day -- which january is always in the first week (4 for iso, 1 for other) + fwd = 7 + dow - doy, + // first-week day local weekday -- which local weekday is fwd + fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; + + return -fwdlw + fwd - 1; +} + +// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday +function dayOfYearFromWeeks(year, week, weekday, dow, doy) { + var localWeekday = (7 + weekday - dow) % 7, + weekOffset = firstWeekOffset(year, dow, doy), + dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, + resYear, + resDayOfYear; + + if (dayOfYear <= 0) { + resYear = year - 1; + resDayOfYear = daysInYear(resYear) + dayOfYear; + } else if (dayOfYear > daysInYear(year)) { + resYear = year + 1; + resDayOfYear = dayOfYear - daysInYear(year); + } else { + resYear = year; + resDayOfYear = dayOfYear; + } + + return { + year: resYear, + dayOfYear: resDayOfYear, + }; +} + +function weekOfYear(mom, dow, doy) { + var weekOffset = firstWeekOffset(mom.year(), dow, doy), + week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, + resWeek, + resYear; + + if (week < 1) { + resYear = mom.year() - 1; + resWeek = week + weeksInYear(resYear, dow, doy); + } else if (week > weeksInYear(mom.year(), dow, doy)) { + resWeek = week - weeksInYear(mom.year(), dow, doy); + resYear = mom.year() + 1; + } else { + resYear = mom.year(); + resWeek = week; + } + + return { + week: resWeek, + year: resYear, + }; +} + +function weeksInYear(year, dow, doy) { + var weekOffset = firstWeekOffset(year, dow, doy), + weekOffsetNext = firstWeekOffset(year + 1, dow, doy); + return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; +} + +// FORMATTING + +addFormatToken('w', ['ww', 2], 'wo', 'week'); +addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + +// PARSING + +addRegexToken('w', match1to2, match1to2NoLeadingZero); +addRegexToken('ww', match1to2, match2); +addRegexToken('W', match1to2, match1to2NoLeadingZero); +addRegexToken('WW', match1to2, match2); + +addWeekParseToken( + ['w', 'ww', 'W', 'WW'], + function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); + } +); + +// HELPERS + +// LOCALES + +function localeWeek(mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; +} + +var defaultLocaleWeek = { + dow: 0, // Sunday is the first day of the week. + doy: 6, // The week that contains Jan 6th is the first week of the year. +}; + +function localeFirstDayOfWeek() { + return this._week.dow; +} + +function localeFirstDayOfYear() { + return this._week.doy; +} + +// MOMENTS + +function getSetWeek(input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); +} + +function getSetISOWeek(input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); +} + +// FORMATTING + +addFormatToken('d', 0, 'do', 'day'); + +addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); +}); + +addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); +}); + +addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); +}); + +addFormatToken('e', 0, 0, 'weekday'); +addFormatToken('E', 0, 0, 'isoWeekday'); + +// PARSING + +addRegexToken('d', match1to2); +addRegexToken('e', match1to2); +addRegexToken('E', match1to2); +addRegexToken('dd', function (isStrict, locale) { + return locale.weekdaysMinRegex(isStrict); +}); +addRegexToken('ddd', function (isStrict, locale) { + return locale.weekdaysShortRegex(isStrict); +}); +addRegexToken('dddd', function (isStrict, locale) { + return locale.weekdaysRegex(isStrict); +}); + +addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { + var weekday = config._locale.weekdaysParse(input, token, config._strict); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } +}); + +addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); +}); + +// HELPERS + +function parseWeekday(input, locale) { + if (typeof input !== 'string') { + return input; + } + + if (!isNaN(input)) { + return parseInt(input, 10); + } + + input = locale.weekdaysParse(input); + if (typeof input === 'number') { + return input; + } + + return null; +} + +function parseIsoWeekday(input, locale) { + if (typeof input === 'string') { + return locale.weekdaysParse(input) % 7 || 7; + } + return isNaN(input) ? null : input; +} + +// LOCALES +function shiftWeekdays(ws, n) { + return ws.slice(n, 7).concat(ws.slice(0, n)); +} + +var defaultLocaleWeekdays = + 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + defaultWeekdaysRegex = matchWord, + defaultWeekdaysShortRegex = matchWord, + defaultWeekdaysMinRegex = matchWord; + +function localeWeekdays(m, format) { + var weekdays = isArray(this._weekdays) + ? this._weekdays + : this._weekdays[ + m && m !== true && this._weekdays.isFormat.test(format) + ? 'format' + : 'standalone' + ]; + return m === true + ? shiftWeekdays(weekdays, this._week.dow) + : m + ? weekdays[m.day()] + : weekdays; +} + +function localeWeekdaysShort(m) { + return m === true + ? shiftWeekdays(this._weekdaysShort, this._week.dow) + : m + ? this._weekdaysShort[m.day()] + : this._weekdaysShort; +} + +function localeWeekdaysMin(m) { + return m === true + ? shiftWeekdays(this._weekdaysMin, this._week.dow) + : m + ? this._weekdaysMin[m.day()] + : this._weekdaysMin; +} + +function handleStrictParse$1(weekdayName, format, strict) { + var i, + ii, + mom, + llc = weekdayName.toLocaleLowerCase(); + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._shortWeekdaysParse = []; + this._minWeekdaysParse = []; + + for (i = 0; i < 7; ++i) { + mom = createUTC([2000, 1]).day(i); + this._minWeekdaysParse[i] = this.weekdaysMin( + mom, + '' + ).toLocaleLowerCase(); + this._shortWeekdaysParse[i] = this.weekdaysShort( + mom, + '' + ).toLocaleLowerCase(); + this._weekdaysParse[i] = this.weekdays(mom, '').toLocaleLowerCase(); + } + } + + if (strict) { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } else { + if (format === 'dddd') { + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else if (format === 'ddd') { + ii = indexOf.call(this._shortWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._minWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } else { + ii = indexOf.call(this._minWeekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._weekdaysParse, llc); + if (ii !== -1) { + return ii; + } + ii = indexOf.call(this._shortWeekdaysParse, llc); + return ii !== -1 ? ii : null; + } + } +} + +function localeWeekdaysParse(weekdayName, format, strict) { + var i, mom, regex; + + if (this._weekdaysParseExact) { + return handleStrictParse$1.call(this, weekdayName, format, strict); + } + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._minWeekdaysParse = []; + this._shortWeekdaysParse = []; + this._fullWeekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + + mom = createUTC([2000, 1]).day(i); + if (strict && !this._fullWeekdaysParse[i]) { + this._fullWeekdaysParse[i] = new RegExp( + '^' + this.weekdays(mom, '').replace('.', '\\.?') + '$', + 'i' + ); + this._shortWeekdaysParse[i] = new RegExp( + '^' + this.weekdaysShort(mom, '').replace('.', '\\.?') + '$', + 'i' + ); + this._minWeekdaysParse[i] = new RegExp( + '^' + this.weekdaysMin(mom, '').replace('.', '\\.?') + '$', + 'i' + ); + } + if (!this._weekdaysParse[i]) { + regex = + '^' + + this.weekdays(mom, '') + + '|^' + + this.weekdaysShort(mom, '') + + '|^' + + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if ( + strict && + format === 'dddd' && + this._fullWeekdaysParse[i].test(weekdayName) + ) { + return i; + } else if ( + strict && + format === 'ddd' && + this._shortWeekdaysParse[i].test(weekdayName) + ) { + return i; + } else if ( + strict && + format === 'dd' && + this._minWeekdaysParse[i].test(weekdayName) + ) { + return i; + } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } +} + +// MOMENTS + +function getSetDayOfWeek(input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + + var day = get(this, 'Day'); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } +} + +function getSetLocaleDayOfWeek(input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); +} + +function getSetISODayOfWeek(input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + + if (input != null) { + var weekday = parseIsoWeekday(input, this.localeData()); + return this.day(this.day() % 7 ? weekday : weekday - 7); + } else { + return this.day() || 7; + } +} + +function weekdaysRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysStrictRegex; + } else { + return this._weekdaysRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysRegex')) { + this._weekdaysRegex = defaultWeekdaysRegex; + } + return this._weekdaysStrictRegex && isStrict + ? this._weekdaysStrictRegex + : this._weekdaysRegex; + } +} + +function weekdaysShortRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysShortStrictRegex; + } else { + return this._weekdaysShortRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysShortRegex')) { + this._weekdaysShortRegex = defaultWeekdaysShortRegex; + } + return this._weekdaysShortStrictRegex && isStrict + ? this._weekdaysShortStrictRegex + : this._weekdaysShortRegex; + } +} + +function weekdaysMinRegex(isStrict) { + if (this._weekdaysParseExact) { + if (!hasOwnProp(this, '_weekdaysRegex')) { + computeWeekdaysParse.call(this); + } + if (isStrict) { + return this._weekdaysMinStrictRegex; + } else { + return this._weekdaysMinRegex; + } + } else { + if (!hasOwnProp(this, '_weekdaysMinRegex')) { + this._weekdaysMinRegex = defaultWeekdaysMinRegex; + } + return this._weekdaysMinStrictRegex && isStrict + ? this._weekdaysMinStrictRegex + : this._weekdaysMinRegex; + } +} + +function computeWeekdaysParse() { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var minPieces = [], + shortPieces = [], + longPieces = [], + mixedPieces = [], + i, + mom, + minp, + shortp, + longp; + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + mom = createUTC([2000, 1]).day(i); + minp = regexEscape(this.weekdaysMin(mom, '')); + shortp = regexEscape(this.weekdaysShort(mom, '')); + longp = regexEscape(this.weekdays(mom, '')); + minPieces.push(minp); + shortPieces.push(shortp); + longPieces.push(longp); + mixedPieces.push(minp); + mixedPieces.push(shortp); + mixedPieces.push(longp); + } + // Sorting makes sure if one weekday (or abbr) is a prefix of another it + // will match the longer piece. + minPieces.sort(cmpLenRev); + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + + this._weekdaysRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._weekdaysShortRegex = this._weekdaysRegex; + this._weekdaysMinRegex = this._weekdaysRegex; + + this._weekdaysStrictRegex = new RegExp( + '^(' + longPieces.join('|') + ')', + 'i' + ); + this._weekdaysShortStrictRegex = new RegExp( + '^(' + shortPieces.join('|') + ')', + 'i' + ); + this._weekdaysMinStrictRegex = new RegExp( + '^(' + minPieces.join('|') + ')', + 'i' + ); +} + +// FORMATTING + +function hFormat() { + return this.hours() % 12 || 12; +} + +function kFormat() { + return this.hours() || 24; +} + +addFormatToken('H', ['HH', 2], 0, 'hour'); +addFormatToken('h', ['hh', 2], 0, hFormat); +addFormatToken('k', ['kk', 2], 0, kFormat); + +addFormatToken('hmm', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); +}); + +addFormatToken('hmmss', 0, 0, function () { + return ( + '' + + hFormat.apply(this) + + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2) + ); +}); + +addFormatToken('Hmm', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2); +}); + +addFormatToken('Hmmss', 0, 0, function () { + return ( + '' + + this.hours() + + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2) + ); +}); + +function meridiem(token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem( + this.hours(), + this.minutes(), + lowercase + ); + }); +} + +meridiem('a', true); +meridiem('A', false); + +// PARSING + +function matchMeridiem(isStrict, locale) { + return locale._meridiemParse; +} + +addRegexToken('a', matchMeridiem); +addRegexToken('A', matchMeridiem); +addRegexToken('H', match1to2, match1to2HasZero); +addRegexToken('h', match1to2, match1to2NoLeadingZero); +addRegexToken('k', match1to2, match1to2NoLeadingZero); +addRegexToken('HH', match1to2, match2); +addRegexToken('hh', match1to2, match2); +addRegexToken('kk', match1to2, match2); + +addRegexToken('hmm', match3to4); +addRegexToken('hmmss', match5to6); +addRegexToken('Hmm', match3to4); +addRegexToken('Hmmss', match5to6); + +addParseToken(['H', 'HH'], HOUR); +addParseToken(['k', 'kk'], function (input, array, config) { + var kInput = toInt(input); + array[HOUR] = kInput === 24 ? 0 : kInput; +}); +addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; +}); +addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; +}); +addParseToken('hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + getParsingFlags(config).bigHour = true; +}); +addParseToken('hmmss', function (input, array, config) { + var pos1 = input.length - 4, + pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + getParsingFlags(config).bigHour = true; +}); +addParseToken('Hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); +}); +addParseToken('Hmmss', function (input, array, config) { + var pos1 = input.length - 4, + pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); +}); + +// LOCALES + +function localeIsPM(input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return (input + '').toLowerCase().charAt(0) === 'p'; +} + +var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i, + // Setting the hour should keep the time, because the user explicitly + // specified which hour they want. So trying to maintain the same hour (in + // a new timezone) makes sense. Adding/subtracting hours does not follow + // this rule. + getSetHour = makeGetSet('Hours', true); + +function localeMeridiem(hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } +} + +var baseConfig = { + calendar: defaultCalendar, + longDateFormat: defaultLongDateFormat, + invalidDate: defaultInvalidDate, + ordinal: defaultOrdinal, + dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse, + relativeTime: defaultRelativeTime, + + months: defaultLocaleMonths, + monthsShort: defaultLocaleMonthsShort, + + week: defaultLocaleWeek, + + weekdays: defaultLocaleWeekdays, + weekdaysMin: defaultLocaleWeekdaysMin, + weekdaysShort: defaultLocaleWeekdaysShort, + + meridiemParse: defaultLocaleMeridiemParse, +}; + +// internal storage for locale config files +var locales = {}, + localeFamilies = {}, + globalLocale; + +function commonPrefix(arr1, arr2) { + var i, + minl = Math.min(arr1.length, arr2.length); + for (i = 0; i < minl; i += 1) { + if (arr1[i] !== arr2[i]) { + return i; + } + } + return minl; +} + +function normalizeLocale(key) { + return key ? key.toLowerCase().replace('_', '-') : key; +} + +// pick the locale from the array +// try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each +// substring from most specific to least, but move to the next array item if it's a more specific variant than the current root +function chooseLocale(names) { + var i = 0, + j, + next, + locale, + split; + + while (i < names.length) { + split = normalizeLocale(names[i]).split('-'); + j = split.length; + next = normalizeLocale(names[i + 1]); + next = next ? next.split('-') : null; + while (j > 0) { + locale = loadLocale(split.slice(0, j).join('-')); + if (locale) { + return locale; + } + if ( + next && + next.length >= j && + commonPrefix(split, next) >= j - 1 + ) { + //the next array item is better than a shallower substring of this one + break; + } + j--; + } + i++; + } + return globalLocale; +} + +function isLocaleNameSane(name) { + // Prevent names that look like filesystem paths, i.e contain '/' or '\' + // Ensure name is available and function returns boolean + return !!(name && name.match('^[^/\\\\]*$')); +} + +function loadLocale(name) { + var oldLocale = null, + aliasedRequire; + // TODO: Find a better way to register and load all the locales in Node + if ( + locales[name] === undefined && + typeof module !== 'undefined' && + module && + module.exports && + isLocaleNameSane(name) + ) { + try { + oldLocale = globalLocale._abbr; + aliasedRequire = require; + aliasedRequire('./locale/' + name); + getSetGlobalLocale(oldLocale); + } catch (e) { + // mark as not found to avoid repeating expensive file require call causing high CPU + // when trying to find en-US, en_US, en-us for every format call + locales[name] = null; // null means not found + } + } + return locales[name]; +} + +// This function will load locale and then set the global locale. If +// no arguments are passed in, it will simply return the current global +// locale key. +function getSetGlobalLocale(key, values) { + var data; + if (key) { + if (isUndefined(values)) { + data = getLocale(key); + } else { + data = defineLocale(key, values); + } + + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } else { + if (typeof console !== 'undefined' && console.warn) { + //warn user if arguments are passed but the locale could not be set + console.warn( + 'Locale ' + key + ' not found. Did you forget to load it?' + ); + } + } + } + + return globalLocale._abbr; +} + +function defineLocale(name, config) { + if (config !== null) { + var locale, + parentConfig = baseConfig; + config.abbr = name; + if (locales[name] != null) { + deprecateSimple( + 'defineLocaleOverride', + 'use moment.updateLocale(localeName, config) to change ' + + 'an existing locale. moment.defineLocale(localeName, ' + + 'config) should only be used for creating a new locale ' + + 'See http://momentjs.com/guides/#/warnings/define-locale/ for more info.' + ); + parentConfig = locales[name]._config; + } else if (config.parentLocale != null) { + if (locales[config.parentLocale] != null) { + parentConfig = locales[config.parentLocale]._config; + } else { + locale = loadLocale(config.parentLocale); + if (locale != null) { + parentConfig = locale._config; + } else { + if (!localeFamilies[config.parentLocale]) { + localeFamilies[config.parentLocale] = []; + } + localeFamilies[config.parentLocale].push({ + name: name, + config: config, + }); + return null; + } + } + } + locales[name] = new Locale(mergeConfigs(parentConfig, config)); + + if (localeFamilies[name]) { + localeFamilies[name].forEach(function (x) { + defineLocale(x.name, x.config); + }); + } + + // backwards compat for now: also set the locale + // make sure we set the locale AFTER all child locales have been + // created, so we won't end up with the child locale set. + getSetGlobalLocale(name); + + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; + } +} + +function updateLocale(name, config) { + if (config != null) { + var locale, + tmpLocale, + parentConfig = baseConfig; + + if (locales[name] != null && locales[name].parentLocale != null) { + // Update existing child locale in-place to avoid memory-leaks + locales[name].set(mergeConfigs(locales[name]._config, config)); + } else { + // MERGE + tmpLocale = loadLocale(name); + if (tmpLocale != null) { + parentConfig = tmpLocale._config; + } + config = mergeConfigs(parentConfig, config); + if (tmpLocale == null) { + // updateLocale is called for creating a new locale + // Set abbr so it will have a name (getters return + // undefined otherwise). + config.abbr = name; + } + locale = new Locale(config); + locale.parentLocale = locales[name]; + locales[name] = locale; + } + + // backwards compat for now: also set the locale + getSetGlobalLocale(name); + } else { + // pass null for config to unupdate, useful for tests + if (locales[name] != null) { + if (locales[name].parentLocale != null) { + locales[name] = locales[name].parentLocale; + if (name === getSetGlobalLocale()) { + getSetGlobalLocale(name); + } + } else if (locales[name] != null) { + delete locales[name]; + } + } + } + return locales[name]; +} + +// returns locale data +function getLocale(key) { + var locale; + + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); +} + +function listLocales() { + return keys(locales); +} + +function checkOverflow(m) { + var overflow, + a = m._a; + + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 + ? MONTH + : a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) + ? DATE + : a[HOUR] < 0 || + a[HOUR] > 24 || + (a[HOUR] === 24 && + (a[MINUTE] !== 0 || + a[SECOND] !== 0 || + a[MILLISECOND] !== 0)) + ? HOUR + : a[MINUTE] < 0 || a[MINUTE] > 59 + ? MINUTE + : a[SECOND] < 0 || a[SECOND] > 59 + ? SECOND + : a[MILLISECOND] < 0 || a[MILLISECOND] > 999 + ? MILLISECOND + : -1; + + if ( + getParsingFlags(m)._overflowDayOfYear && + (overflow < YEAR || overflow > DATE) + ) { + overflow = DATE; + } + if (getParsingFlags(m)._overflowWeeks && overflow === -1) { + overflow = WEEK; + } + if (getParsingFlags(m)._overflowWeekday && overflow === -1) { + overflow = WEEKDAY; + } + + getParsingFlags(m).overflow = overflow; + } + + return m; +} + +// iso 8601 regex +// 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) +var extendedIsoRegex = + /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + basicIsoRegex = + /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/, + tzRegex = /Z|[+-]\d\d(?::?\d\d)?/, + isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], + ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], + ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], + ['GGGG-[W]WW', /\d{4}-W\d\d/, false], + ['YYYY-DDD', /\d{4}-\d{3}/], + ['YYYY-MM', /\d{4}-\d\d/, false], + ['YYYYYYMMDD', /[+-]\d{10}/], + ['YYYYMMDD', /\d{8}/], + ['GGGG[W]WWE', /\d{4}W\d{3}/], + ['GGGG[W]WW', /\d{4}W\d{2}/, false], + ['YYYYDDD', /\d{7}/], + ['YYYYMM', /\d{6}/, false], + ['YYYY', /\d{4}/, false], + ], + // iso time formats and regexes + isoTimes = [ + ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], + ['HH:mm:ss', /\d\d:\d\d:\d\d/], + ['HH:mm', /\d\d:\d\d/], + ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], + ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], + ['HHmmss', /\d\d\d\d\d\d/], + ['HHmm', /\d\d\d\d/], + ['HH', /\d\d/], + ], + aspNetJsonRegex = /^\/?Date\((-?\d+)/i, + // RFC 2822 regex: For details see https://tools.ietf.org/html/rfc2822#section-3.3 + rfc2822 = + /^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/, + obsOffsets = { + UT: 0, + GMT: 0, + EDT: -4 * 60, + EST: -5 * 60, + CDT: -5 * 60, + CST: -6 * 60, + MDT: -6 * 60, + MST: -7 * 60, + PDT: -7 * 60, + PST: -8 * 60, + }; + +// date from iso format +function configFromISO(config) { + var i, + l, + string = config._i, + match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), + allowTime, + dateFormat, + timeFormat, + tzFormat, + isoDatesLen = isoDates.length, + isoTimesLen = isoTimes.length; + + if (match) { + getParsingFlags(config).iso = true; + for (i = 0, l = isoDatesLen; i < l; i++) { + if (isoDates[i][1].exec(match[1])) { + dateFormat = isoDates[i][0]; + allowTime = isoDates[i][2] !== false; + break; + } + } + if (dateFormat == null) { + config._isValid = false; + return; + } + if (match[3]) { + for (i = 0, l = isoTimesLen; i < l; i++) { + if (isoTimes[i][1].exec(match[3])) { + // match[2] should be 'T' or space + timeFormat = (match[2] || ' ') + isoTimes[i][0]; + break; + } + } + if (timeFormat == null) { + config._isValid = false; + return; + } + } + if (!allowTime && timeFormat != null) { + config._isValid = false; + return; + } + if (match[4]) { + if (tzRegex.exec(match[4])) { + tzFormat = 'Z'; + } else { + config._isValid = false; + return; + } + } + config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); + configFromStringAndFormat(config); + } else { + config._isValid = false; + } +} + +function extractFromRFC2822Strings( + yearStr, + monthStr, + dayStr, + hourStr, + minuteStr, + secondStr +) { + var result = [ + untruncateYear(yearStr), + defaultLocaleMonthsShort.indexOf(monthStr), + parseInt(dayStr, 10), + parseInt(hourStr, 10), + parseInt(minuteStr, 10), + ]; + + if (secondStr) { + result.push(parseInt(secondStr, 10)); + } + + return result; +} + +function untruncateYear(yearStr) { + var year = parseInt(yearStr, 10); + if (year <= 49) { + return 2000 + year; + } else if (year <= 999) { + return 1900 + year; + } + return year; +} + +function preprocessRFC2822(s) { + // Remove comments and folding whitespace and replace multiple-spaces with a single space + return s + .replace(/\([^()]*\)|[\n\t]/g, ' ') + .replace(/(\s\s+)/g, ' ') + .replace(/^\s\s*/, '') + .replace(/\s\s*$/, ''); +} + +function checkWeekday(weekdayStr, parsedInput, config) { + if (weekdayStr) { + // TODO: Replace the vanilla JS Date object with an independent day-of-week check. + var weekdayProvided = defaultLocaleWeekdaysShort.indexOf(weekdayStr), + weekdayActual = new Date( + parsedInput[0], + parsedInput[1], + parsedInput[2] + ).getDay(); + if (weekdayProvided !== weekdayActual) { + getParsingFlags(config).weekdayMismatch = true; + config._isValid = false; + return false; + } + } + return true; +} + +function calculateOffset(obsOffset, militaryOffset, numOffset) { + if (obsOffset) { + return obsOffsets[obsOffset]; + } else if (militaryOffset) { + // the only allowed military tz is Z + return 0; + } else { + var hm = parseInt(numOffset, 10), + m = hm % 100, + h = (hm - m) / 100; + return h * 60 + m; + } +} + +// date and time from ref 2822 format +function configFromRFC2822(config) { + var match = rfc2822.exec(preprocessRFC2822(config._i)), + parsedArray; + if (match) { + parsedArray = extractFromRFC2822Strings( + match[4], + match[3], + match[2], + match[5], + match[6], + match[7] + ); + if (!checkWeekday(match[1], parsedArray, config)) { + return; + } + + config._a = parsedArray; + config._tzm = calculateOffset(match[8], match[9], match[10]); + + config._d = createUTCDate.apply(null, config._a); + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + + getParsingFlags(config).rfc2822 = true; + } else { + config._isValid = false; + } +} + +// date from 1) ASP.NET, 2) ISO, 3) RFC 2822 formats, or 4) optional fallback if parsing isn't strict +function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } + + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + } else { + return; + } + + configFromRFC2822(config); + if (config._isValid === false) { + delete config._isValid; + } else { + return; + } + + if (config._strict) { + config._isValid = false; + } else { + // Final attempt, use Input Fallback + hooks.createFromInputFallback(config); + } +} + +hooks.createFromInputFallback = deprecate( + 'value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), ' + + 'which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are ' + + 'discouraged. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } +); + +// Pick the first defined of two or three arguments. +function defaults(a, b, c) { + if (a != null) { + return a; + } + if (b != null) { + return b; + } + return c; +} + +function currentDateArray(config) { + // hooks is actually the exported moment object + var nowValue = new Date(hooks.now()); + if (config._useUTC) { + return [ + nowValue.getUTCFullYear(), + nowValue.getUTCMonth(), + nowValue.getUTCDate(), + ]; + } + return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; +} + +// convert an array to a date. +// the array should mirror the parameters below +// note: all values past the year are optional and will default to the lowest possible value. +// [year, month, day , hour, minute, second, millisecond] +function configFromArray(config) { + var i, + date, + input = [], + currentDate, + expectedWeekday, + yearToUse; + + if (config._d) { + return; + } + + currentDate = currentDateArray(config); + + //compute day of the year from weeks and weekdays + if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { + dayOfYearFromWeekInfo(config); + } + + //if the day of the year is set, figure out what it is + if (config._dayOfYear != null) { + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); + + if ( + config._dayOfYear > daysInYear(yearToUse) || + config._dayOfYear === 0 + ) { + getParsingFlags(config)._overflowDayOfYear = true; + } + + date = createUTCDate(yearToUse, 0, config._dayOfYear); + config._a[MONTH] = date.getUTCMonth(); + config._a[DATE] = date.getUTCDate(); + } + + // Default to current date. + // * if no year, month, day of month are given, default to today + // * if day of month is given, default month and year + // * if month is given, default only year + // * if year is given, don't default anything + for (i = 0; i < 3 && config._a[i] == null; ++i) { + config._a[i] = input[i] = currentDate[i]; + } + + // Zero out whatever was not defaulted, including time + for (; i < 7; i++) { + config._a[i] = input[i] = + config._a[i] == null ? (i === 2 ? 1 : 0) : config._a[i]; + } + + // Check for 24:00:00.000 + if ( + config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0 + ) { + config._nextDay = true; + config._a[HOUR] = 0; + } + + config._d = (config._useUTC ? createUTCDate : createDate).apply( + null, + input + ); + expectedWeekday = config._useUTC + ? config._d.getUTCDay() + : config._d.getDay(); + + // Apply timezone offset from input. The actual utcOffset can be changed + // with parseZone. + if (config._tzm != null) { + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } + + if (config._nextDay) { + config._a[HOUR] = 24; + } + + // check for mismatching day of week + if ( + config._w && + typeof config._w.d !== 'undefined' && + config._w.d !== expectedWeekday + ) { + getParsingFlags(config).weekdayMismatch = true; + } +} + +function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow, curWeek; + + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; + + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults( + w.GG, + config._a[YEAR], + weekOfYear(createLocal(), 1, 4).year + ); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + if (weekday < 1 || weekday > 7) { + weekdayOverflow = true; + } + } else { + dow = config._locale._week.dow; + doy = config._locale._week.doy; + + curWeek = weekOfYear(createLocal(), dow, doy); + + weekYear = defaults(w.gg, config._a[YEAR], curWeek.year); + + // Default to current week. + week = defaults(w.w, curWeek.week); + + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < 0 || weekday > 6) { + weekdayOverflow = true; + } + } else if (w.e != null) { + // local weekday -- counting starts from beginning of week + weekday = w.e + dow; + if (w.e < 0 || w.e > 6) { + weekdayOverflow = true; + } + } else { + // default to beginning of week + weekday = dow; + } + } + if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { + getParsingFlags(config)._overflowWeeks = true; + } else if (weekdayOverflow != null) { + getParsingFlags(config)._overflowWeekday = true; + } else { + temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; + } +} + +// constant that refers to the ISO standard +hooks.ISO_8601 = function () {}; + +// constant that refers to the RFC 2822 form +hooks.RFC_2822 = function () {}; + +// date from string and format string +function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === hooks.ISO_8601) { + configFromISO(config); + return; + } + if (config._f === hooks.RFC_2822) { + configFromRFC2822(config); + return; + } + config._a = []; + getParsingFlags(config).empty = true; + + // This array is used to make a Date, either with `new Date` or `Date.UTC` + var string = '' + config._i, + i, + parsedInput, + tokens, + token, + skipped, + stringLength = string.length, + totalParsedInputLength = 0, + era, + tokenLen; + + tokens = + expandFormat(config._f, config._locale).match(formattingTokens) || []; + tokenLen = tokens.length; + for (i = 0; i < tokenLen; i++) { + token = tokens[i]; + parsedInput = (string.match(getParseRegexForToken(token, config)) || + [])[0]; + if (parsedInput) { + skipped = string.substr(0, string.indexOf(parsedInput)); + if (skipped.length > 0) { + getParsingFlags(config).unusedInput.push(skipped); + } + string = string.slice( + string.indexOf(parsedInput) + parsedInput.length + ); + totalParsedInputLength += parsedInput.length; + } + // don't parse if it's not a known token + if (formatTokenFunctions[token]) { + if (parsedInput) { + getParsingFlags(config).empty = false; + } else { + getParsingFlags(config).unusedTokens.push(token); + } + addTimeToArrayFromToken(token, parsedInput, config); + } else if (config._strict && !parsedInput) { + getParsingFlags(config).unusedTokens.push(token); + } + } + + // add remaining unparsed input length to the string + getParsingFlags(config).charsLeftOver = + stringLength - totalParsedInputLength; + if (string.length > 0) { + getParsingFlags(config).unusedInput.push(string); + } + + // clear _12h flag if hour is <= 12 + if ( + config._a[HOUR] <= 12 && + getParsingFlags(config).bigHour === true && + config._a[HOUR] > 0 + ) { + getParsingFlags(config).bigHour = undefined; + } + + getParsingFlags(config).parsedDateParts = config._a.slice(0); + getParsingFlags(config).meridiem = config._meridiem; + // handle meridiem + config._a[HOUR] = meridiemFixWrap( + config._locale, + config._a[HOUR], + config._meridiem + ); + + // handle era + era = getParsingFlags(config).era; + if (era !== null) { + config._a[YEAR] = config._locale.erasConvertYear(era, config._a[YEAR]); + } + + configFromArray(config); + checkOverflow(config); +} + +function meridiemFixWrap(locale, hour, meridiem) { + var isPm; + + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } +} + +// date from string and array of format strings +function configFromStringAndArray(config) { + var tempConfig, + bestMoment, + scoreToBeat, + i, + currentScore, + validFormatFound, + bestFormatIsValid = false, + configfLen = config._f.length; + + if (configfLen === 0) { + getParsingFlags(config).invalidFormat = true; + config._d = new Date(NaN); + return; + } + + for (i = 0; i < configfLen; i++) { + currentScore = 0; + validFormatFound = false; + tempConfig = copyConfig({}, config); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } + tempConfig._f = config._f[i]; + configFromStringAndFormat(tempConfig); + + if (isValid(tempConfig)) { + validFormatFound = true; + } + + // if there is any input that was not parsed add a penalty for that format + currentScore += getParsingFlags(tempConfig).charsLeftOver; + + //or tokens + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; + + getParsingFlags(tempConfig).score = currentScore; + + if (!bestFormatIsValid) { + if ( + scoreToBeat == null || + currentScore < scoreToBeat || + validFormatFound + ) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + if (validFormatFound) { + bestFormatIsValid = true; + } + } + } else { + if (currentScore < scoreToBeat) { + scoreToBeat = currentScore; + bestMoment = tempConfig; + } + } + } + + extend(config, bestMoment || tempConfig); +} + +function configFromObject(config) { + if (config._d) { + return; + } + + var i = normalizeObjectUnits(config._i), + dayOrDate = i.day === undefined ? i.date : i.day; + config._a = map( + [i.year, i.month, dayOrDate, i.hour, i.minute, i.second, i.millisecond], + function (obj) { + return obj && parseInt(obj, 10); + } + ); + + configFromArray(config); +} + +function createFromConfig(config) { + var res = new Moment(checkOverflow(prepareConfig(config))); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; + } + + return res; +} + +function prepareConfig(config) { + var input = config._i, + format = config._f; + + config._locale = config._locale || getLocale(config._l); + + if (input === null || (format === undefined && input === '')) { + return createInvalid({ nullInput: true }); + } + + if (typeof input === 'string') { + config._i = input = config._locale.preparse(input); + } + + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isDate(input)) { + config._d = input; + } else if (isArray(format)) { + configFromStringAndArray(config); + } else if (format) { + configFromStringAndFormat(config); + } else { + configFromInput(config); + } + + if (!isValid(config)) { + config._d = null; + } + + return config; +} + +function configFromInput(config) { + var input = config._i; + if (isUndefined(input)) { + config._d = new Date(hooks.now()); + } else if (isDate(input)) { + config._d = new Date(input.valueOf()); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (isObject(input)) { + configFromObject(config); + } else if (isNumber(input)) { + // from milliseconds + config._d = new Date(input); + } else { + hooks.createFromInputFallback(config); + } +} + +function createLocalOrUTC(input, format, locale, strict, isUTC) { + var c = {}; + + if (format === true || format === false) { + strict = format; + format = undefined; + } + + if (locale === true || locale === false) { + strict = locale; + locale = undefined; + } + + if ( + (isObject(input) && isObjectEmpty(input)) || + (isArray(input) && input.length === 0) + ) { + input = undefined; + } + // object construction must be done this way. + // https://github.com/moment/moment/issues/1423 + c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; + c._i = input; + c._f = format; + c._strict = strict; + + return createFromConfig(c); +} + +function createLocal(input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); +} + +var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/', + function () { + var other = createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other < this ? this : other; + } else { + return createInvalid(); + } + } + ), + prototypeMax = deprecate( + 'moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/', + function () { + var other = createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other > this ? this : other; + } else { + return createInvalid(); + } + } + ); + +// Pick a moment m from moments so that m[fn](other) is true for all +// other. This relies on the function fn to be transitive. +// +// moments should either be an array of moment objects or an array, whose +// first element is an array of moment objects. +function pickBy(fn, moments) { + var res, i; + if (moments.length === 1 && isArray(moments[0])) { + moments = moments[0]; + } + if (!moments.length) { + return createLocal(); + } + res = moments[0]; + for (i = 1; i < moments.length; ++i) { + if (!moments[i].isValid() || moments[i][fn](res)) { + res = moments[i]; + } + } + return res; +} + +// TODO: Use [].sort instead? +function min() { + var args = [].slice.call(arguments, 0); + + return pickBy('isBefore', args); +} + +function max() { + var args = [].slice.call(arguments, 0); + + return pickBy('isAfter', args); +} + +var now = function () { + return Date.now ? Date.now() : +new Date(); +}; + +var ordering = [ + 'year', + 'quarter', + 'month', + 'week', + 'day', + 'hour', + 'minute', + 'second', + 'millisecond', +]; + +function isDurationValid(m) { + var key, + unitHasDecimal = false, + i, + orderLen = ordering.length; + for (key in m) { + if ( + hasOwnProp(m, key) && + !( + indexOf.call(ordering, key) !== -1 && + (m[key] == null || !isNaN(m[key])) + ) + ) { + return false; + } + } + + for (i = 0; i < orderLen; ++i) { + if (m[ordering[i]]) { + if (unitHasDecimal) { + return false; // only allow non-integers for smallest unit + } + if (parseFloat(m[ordering[i]]) !== toInt(m[ordering[i]])) { + unitHasDecimal = true; + } + } + } + + return true; +} + +function isValid$1() { + return this._isValid; +} + +function createInvalid$1() { + return createDuration(NaN); +} + +function Duration(duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || normalizedInput.isoWeek || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; + + this._isValid = isDurationValid(normalizedInput); + + // representation for dateAddRemove + this._milliseconds = + +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 1000 * 60 * 60; //using 1000 * 60 * 60 instead of 36e5 to avoid floating point rounding errors https://github.com/moment/moment/issues/2978 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + weeks * 7; + // It is impossible to translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + quarters * 3 + years * 12; + + this._data = {}; + + this._locale = getLocale(); + + this._bubble(); +} + +function isDuration(obj) { + return obj instanceof Duration; +} + +function absRound(number) { + if (number < 0) { + return Math.round(-1 * number) * -1; + } else { + return Math.round(number); + } +} + +// compare two arrays, return the number of differences +function compareArrays(array1, array2, dontConvert) { + var len = Math.min(array1.length, array2.length), + lengthDiff = Math.abs(array1.length - array2.length), + diffs = 0, + i; + for (i = 0; i < len; i++) { + if ( + (dontConvert && array1[i] !== array2[i]) || + (!dontConvert && toInt(array1[i]) !== toInt(array2[i])) + ) { + diffs++; + } + } + return diffs + lengthDiff; +} + +// FORMATTING + +function offset(token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(), + sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return ( + sign + + zeroFill(~~(offset / 60), 2) + + separator + + zeroFill(~~offset % 60, 2) + ); + }); +} + +offset('Z', ':'); +offset('ZZ', ''); + +// PARSING + +addRegexToken('Z', matchShortOffset); +addRegexToken('ZZ', matchShortOffset); +addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(matchShortOffset, input); +}); + +// HELPERS + +// timezone chunker +// '+10:00' > ['10', '00'] +// '-1530' > ['-15', '30'] +var chunkOffset = /([\+\-]|\d\d)/gi; + +function offsetFromString(matcher, string) { + var matches = (string || '').match(matcher), + chunk, + parts, + minutes; + + if (matches === null) { + return null; + } + + chunk = matches[matches.length - 1] || []; + parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + minutes = +(parts[1] * 60) + toInt(parts[2]); + + return minutes === 0 ? 0 : parts[0] === '+' ? minutes : -minutes; +} + +// Return a moment from input, that is local/utc/zone equivalent to model. +function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = + (isMoment(input) || isDate(input) + ? input.valueOf() + : createLocal(input).valueOf()) - res.valueOf(); + // Use low-level api, because this fn is low-level api. + res._d.setTime(res._d.valueOf() + diff); + hooks.updateOffset(res, false); + return res; + } else { + return createLocal(input).local(); + } +} + +function getDateOffset(m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset()); +} + +// HOOKS + +// This function will be called whenever a moment is mutated. +// It is intended to keep the offset in sync with the timezone. +hooks.updateOffset = function () {}; + +// MOMENTS + +// keepLocalTime = true means only change the timezone, without +// affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> +// 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset +// +0200, so we adjust the time as needed, to be valid. +// +// Keeping the time actually adds/subtracts (one hour) +// from the actual represented time. That is why we call updateOffset +// a second time. In case it wants us to change the offset again +// _changeInProgress == true case, then we have to adjust, because +// there is no such time in the given timezone. +function getSetOffset(input, keepLocalTime, keepMinutes) { + var offset = this._offset || 0, + localAdjust; + if (!this.isValid()) { + return input != null ? this : NaN; + } + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(matchShortOffset, input); + if (input === null) { + return this; + } + } else if (Math.abs(input) < 16 && !keepMinutes) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + addSubtract( + this, + createDuration(input - offset, 'm'), + 1, + false + ); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } +} + +function getSetZone(input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } +} + +function setOffsetToUTC(keepLocalTime) { + return this.utcOffset(0, keepLocalTime); +} + +function setOffsetToLocal(keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; +} + +function setOffsetToParsedOffset() { + if (this._tzm != null) { + this.utcOffset(this._tzm, false, true); + } else if (typeof this._i === 'string') { + var tZone = offsetFromString(matchOffset, this._i); + if (tZone != null) { + this.utcOffset(tZone); + } else { + this.utcOffset(0, true); + } + } + return this; +} + +function hasAlignedHourOffset(input) { + if (!this.isValid()) { + return false; + } + input = input ? createLocal(input).utcOffset() : 0; + + return (this.utcOffset() - input) % 60 === 0; +} + +function isDaylightSavingTime() { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); +} + +function isDaylightSavingTimeShifted() { + if (!isUndefined(this._isDSTShifted)) { + return this._isDSTShifted; + } + + var c = {}, + other; + + copyConfig(c, this); + c = prepareConfig(c); + + if (c._a) { + other = c._isUTC ? createUTC(c._a) : createLocal(c._a); + this._isDSTShifted = + this.isValid() && compareArrays(c._a, other.toArray()) > 0; + } else { + this._isDSTShifted = false; + } + + return this._isDSTShifted; +} + +function isLocal() { + return this.isValid() ? !this._isUTC : false; +} + +function isUtcOffset() { + return this.isValid() ? this._isUTC : false; +} + +function isUtc() { + return this.isValid() ? this._isUTC && this._offset === 0 : false; +} + +// ASP.NET json date format regex +var aspNetRegex = /^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/, + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + // and further modified to allow for strings containing both week and day + isoRegex = + /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/; + +function createDuration(input, key) { + var duration = input, + // matching against regexp is expensive, do it on demand + match = null, + sign, + ret, + diffRes; + + if (isDuration(input)) { + duration = { + ms: input._milliseconds, + d: input._days, + M: input._months, + }; + } else if (isNumber(input) || !isNaN(+input)) { + duration = {}; + if (key) { + duration[key] = +input; + } else { + duration.milliseconds = +input; + } + } else if ((match = aspNetRegex.exec(input))) { + sign = match[1] === '-' ? -1 : 1; + duration = { + y: 0, + d: toInt(match[DATE]) * sign, + h: toInt(match[HOUR]) * sign, + m: toInt(match[MINUTE]) * sign, + s: toInt(match[SECOND]) * sign, + ms: toInt(absRound(match[MILLISECOND] * 1000)) * sign, // the millisecond decimal point is included in the match + }; + } else if ((match = isoRegex.exec(input))) { + sign = match[1] === '-' ? -1 : 1; + duration = { + y: parseIso(match[2], sign), + M: parseIso(match[3], sign), + w: parseIso(match[4], sign), + d: parseIso(match[5], sign), + h: parseIso(match[6], sign), + m: parseIso(match[7], sign), + s: parseIso(match[8], sign), + }; + } else if (duration == null) { + // checks for null or undefined + duration = {}; + } else if ( + typeof duration === 'object' && + ('from' in duration || 'to' in duration) + ) { + diffRes = momentsDifference( + createLocal(duration.from), + createLocal(duration.to) + ); + + duration = {}; + duration.ms = diffRes.milliseconds; + duration.M = diffRes.months; + } + + ret = new Duration(duration); + + if (isDuration(input) && hasOwnProp(input, '_locale')) { + ret._locale = input._locale; + } + + if (isDuration(input) && hasOwnProp(input, '_isValid')) { + ret._isValid = input._isValid; + } + + return ret; +} + +createDuration.fn = Duration.prototype; +createDuration.invalid = createInvalid$1; + +function parseIso(inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; +} + +function positiveMomentsDifference(base, other) { + var res = {}; + + res.months = + other.month() - base.month() + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; + } + + res.milliseconds = +other - +base.clone().add(res.months, 'M'); + + return res; +} + +function momentsDifference(base, other) { + var res; + if (!(base.isValid() && other.isValid())) { + return { milliseconds: 0, months: 0 }; + } + + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); + } else { + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; + } + + return res; +} + +// TODO: remove 'name' arg after deprecation is removed +function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple( + name, + 'moment().' + + name + + '(period, number) is deprecated. Please use moment().' + + name + + '(number, period). ' + + 'See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info.' + ); + tmp = val; + val = period; + period = tmp; + } + + dur = createDuration(val, period); + addSubtract(this, dur, direction); + return this; + }; +} + +function addSubtract(mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = absRound(duration._days), + months = absRound(duration._months); + + if (!mom.isValid()) { + // No op + return; + } + + updateOffset = updateOffset == null ? true : updateOffset; + + if (months) { + setMonth(mom, get(mom, 'Month') + months * isAdding); + } + if (days) { + set$1(mom, 'Date', get(mom, 'Date') + days * isAdding); + } + if (milliseconds) { + mom._d.setTime(mom._d.valueOf() + milliseconds * isAdding); + } + if (updateOffset) { + hooks.updateOffset(mom, days || months); + } +} + +var add = createAdder(1, 'add'), + subtract = createAdder(-1, 'subtract'); + +function isString(input) { + return typeof input === 'string' || input instanceof String; +} + +// type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined +function isMomentInput(input) { + return ( + isMoment(input) || + isDate(input) || + isString(input) || + isNumber(input) || + isNumberOrStringArray(input) || + isMomentInputObject(input) || + input === null || + input === undefined + ); +} + +function isMomentInputObject(input) { + var objectTest = isObject(input) && !isObjectEmpty(input), + propertyTest = false, + properties = [ + 'years', + 'year', + 'y', + 'months', + 'month', + 'M', + 'days', + 'day', + 'd', + 'dates', + 'date', + 'D', + 'hours', + 'hour', + 'h', + 'minutes', + 'minute', + 'm', + 'seconds', + 'second', + 's', + 'milliseconds', + 'millisecond', + 'ms', + ], + i, + property, + propertyLen = properties.length; + + for (i = 0; i < propertyLen; i += 1) { + property = properties[i]; + propertyTest = propertyTest || hasOwnProp(input, property); + } + + return objectTest && propertyTest; +} + +function isNumberOrStringArray(input) { + var arrayTest = isArray(input), + dataTypeTest = false; + if (arrayTest) { + dataTypeTest = + input.filter(function (item) { + return !isNumber(item) && isString(input); + }).length === 0; + } + return arrayTest && dataTypeTest; +} + +function isCalendarSpec(input) { + var objectTest = isObject(input) && !isObjectEmpty(input), + propertyTest = false, + properties = [ + 'sameDay', + 'nextDay', + 'lastDay', + 'nextWeek', + 'lastWeek', + 'sameElse', + ], + i, + property; + + for (i = 0; i < properties.length; i += 1) { + property = properties[i]; + propertyTest = propertyTest || hasOwnProp(input, property); + } + + return objectTest && propertyTest; +} + +function getCalendarFormat(myMoment, now) { + var diff = myMoment.diff(now, 'days', true); + return diff < -6 + ? 'sameElse' + : diff < -1 + ? 'lastWeek' + : diff < 0 + ? 'lastDay' + : diff < 1 + ? 'sameDay' + : diff < 2 + ? 'nextDay' + : diff < 7 + ? 'nextWeek' + : 'sameElse'; +} + +function calendar$1(time, formats) { + // Support for single parameter, formats only overload to the calendar function + if (arguments.length === 1) { + if (!arguments[0]) { + time = undefined; + formats = undefined; + } else if (isMomentInput(arguments[0])) { + time = arguments[0]; + formats = undefined; + } else if (isCalendarSpec(arguments[0])) { + formats = arguments[0]; + time = undefined; + } + } + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + format = hooks.calendarFormat(this, sod) || 'sameElse', + output = + formats && + (isFunction(formats[format]) + ? formats[format].call(this, now) + : formats[format]); + + return this.format( + output || this.localeData().calendar(format, this, createLocal(now)) + ); +} + +function clone() { + return new Moment(this); +} + +function isAfter(input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() > localInput.valueOf(); + } else { + return localInput.valueOf() < this.clone().startOf(units).valueOf(); + } +} + +function isBefore(input, units) { + var localInput = isMoment(input) ? input : createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() < localInput.valueOf(); + } else { + return this.clone().endOf(units).valueOf() < localInput.valueOf(); + } +} + +function isBetween(from, to, units, inclusivity) { + var localFrom = isMoment(from) ? from : createLocal(from), + localTo = isMoment(to) ? to : createLocal(to); + if (!(this.isValid() && localFrom.isValid() && localTo.isValid())) { + return false; + } + inclusivity = inclusivity || '()'; + return ( + (inclusivity[0] === '(' + ? this.isAfter(localFrom, units) + : !this.isBefore(localFrom, units)) && + (inclusivity[1] === ')' + ? this.isBefore(localTo, units) + : !this.isAfter(localTo, units)) + ); +} + +function isSame(input, units) { + var localInput = isMoment(input) ? input : createLocal(input), + inputMs; + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units) || 'millisecond'; + if (units === 'millisecond') { + return this.valueOf() === localInput.valueOf(); + } else { + inputMs = localInput.valueOf(); + return ( + this.clone().startOf(units).valueOf() <= inputMs && + inputMs <= this.clone().endOf(units).valueOf() + ); + } +} + +function isSameOrAfter(input, units) { + return this.isSame(input, units) || this.isAfter(input, units); +} + +function isSameOrBefore(input, units) { + return this.isSame(input, units) || this.isBefore(input, units); +} + +function diff(input, units, asFloat) { + var that, zoneDelta, output; + + if (!this.isValid()) { + return NaN; + } + + that = cloneWithOffset(input, this); + + if (!that.isValid()) { + return NaN; + } + + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; + + units = normalizeUnits(units); + + switch (units) { + case 'year': + output = monthDiff(this, that) / 12; + break; + case 'month': + output = monthDiff(this, that); + break; + case 'quarter': + output = monthDiff(this, that) / 3; + break; + case 'second': + output = (this - that) / 1e3; + break; // 1000 + case 'minute': + output = (this - that) / 6e4; + break; // 1000 * 60 + case 'hour': + output = (this - that) / 36e5; + break; // 1000 * 60 * 60 + case 'day': + output = (this - that - zoneDelta) / 864e5; + break; // 1000 * 60 * 60 * 24, negate dst + case 'week': + output = (this - that - zoneDelta) / 6048e5; + break; // 1000 * 60 * 60 * 24 * 7, negate dst + default: + output = this - that; + } + + return asFloat ? output : absFloor(output); +} + +function monthDiff(a, b) { + if (a.date() < b.date()) { + // end-of-month calculations work correct when the start month has more + // days than the end month. + return -monthDiff(b, a); + } + // difference in months + var wholeMonthDiff = (b.year() - a.year()) * 12 + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, + adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + //check for negative zero, return zero if negative zero + return -(wholeMonthDiff + adjust) || 0; +} + +hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; +hooks.defaultFormatUtc = 'YYYY-MM-DDTHH:mm:ss[Z]'; + +function toString() { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); +} + +function toISOString(keepOffset) { + if (!this.isValid()) { + return null; + } + var utc = keepOffset !== true, + m = utc ? this.clone().utc() : this; + if (m.year() < 0 || m.year() > 9999) { + return formatMoment( + m, + utc + ? 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]' + : 'YYYYYY-MM-DD[T]HH:mm:ss.SSSZ' + ); + } + if (isFunction(Date.prototype.toISOString)) { + // native implementation is ~50x faster, use it when we can + if (utc) { + return this.toDate().toISOString(); + } else { + return new Date(this.valueOf() + this.utcOffset() * 60 * 1000) + .toISOString() + .replace('Z', formatMoment(m, 'Z')); + } + } + return formatMoment( + m, + utc ? 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]' : 'YYYY-MM-DD[T]HH:mm:ss.SSSZ' + ); +} + +/** + * Return a human readable representation of a moment that can + * also be evaluated to get a new moment which is the same + * + * @link https://nodejs.org/dist/latest/docs/api/util.html#util_custom_inspect_function_on_objects + */ +function inspect() { + if (!this.isValid()) { + return 'moment.invalid(/* ' + this._i + ' */)'; + } + var func = 'moment', + zone = '', + prefix, + year, + datetime, + suffix; + if (!this.isLocal()) { + func = this.utcOffset() === 0 ? 'moment.utc' : 'moment.parseZone'; + zone = 'Z'; + } + prefix = '[' + func + '("]'; + year = 0 <= this.year() && this.year() <= 9999 ? 'YYYY' : 'YYYYYY'; + datetime = '-MM-DD[T]HH:mm:ss.SSS'; + suffix = zone + '[")]'; + + return this.format(prefix + year + datetime + suffix); +} + +function format(inputString) { + if (!inputString) { + inputString = this.isUtc() + ? hooks.defaultFormatUtc + : hooks.defaultFormat; + } + var output = formatMoment(this, inputString); + return this.localeData().postformat(output); +} + +function from(time, withoutSuffix) { + if ( + this.isValid() && + ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) + ) { + return createDuration({ to: this, from: time }) + .locale(this.locale()) + .humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } +} + +function fromNow(withoutSuffix) { + return this.from(createLocal(), withoutSuffix); +} + +function to(time, withoutSuffix) { + if ( + this.isValid() && + ((isMoment(time) && time.isValid()) || createLocal(time).isValid()) + ) { + return createDuration({ from: this, to: time }) + .locale(this.locale()) + .humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } +} + +function toNow(withoutSuffix) { + return this.to(createLocal(), withoutSuffix); +} + +// If passed a locale key, it will set the locale for this +// instance. Otherwise, it will return the locale configuration +// variables for this instance. +function locale(key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } +} + +var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } +); + +function localeData() { + return this._locale; +} + +var MS_PER_SECOND = 1000, + MS_PER_MINUTE = 60 * MS_PER_SECOND, + MS_PER_HOUR = 60 * MS_PER_MINUTE, + MS_PER_400_YEARS = (365 * 400 + 97) * 24 * MS_PER_HOUR; + +// actual modulo - handles negative numbers (for dates before 1970): +function mod$1(dividend, divisor) { + return ((dividend % divisor) + divisor) % divisor; +} + +function localStartOfDate(y, m, d) { + // the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + return new Date(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return new Date(y, m, d).valueOf(); + } +} + +function utcStartOfDate(y, m, d) { + // Date.UTC remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0) { + // preserve leap years using a full 400 year cycle, then reset + return Date.UTC(y + 400, m, d) - MS_PER_400_YEARS; + } else { + return Date.UTC(y, m, d); + } +} + +function startOf(units) { + var time, startOfDate; + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + + switch (units) { + case 'year': + time = startOfDate(this.year(), 0, 1); + break; + case 'quarter': + time = startOfDate( + this.year(), + this.month() - (this.month() % 3), + 1 + ); + break; + case 'month': + time = startOfDate(this.year(), this.month(), 1); + break; + case 'week': + time = startOfDate( + this.year(), + this.month(), + this.date() - this.weekday() + ); + break; + case 'isoWeek': + time = startOfDate( + this.year(), + this.month(), + this.date() - (this.isoWeekday() - 1) + ); + break; + case 'day': + case 'date': + time = startOfDate(this.year(), this.month(), this.date()); + break; + case 'hour': + time = this._d.valueOf(); + time -= mod$1( + time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), + MS_PER_HOUR + ); + break; + case 'minute': + time = this._d.valueOf(); + time -= mod$1(time, MS_PER_MINUTE); + break; + case 'second': + time = this._d.valueOf(); + time -= mod$1(time, MS_PER_SECOND); + break; + } + + this._d.setTime(time); + hooks.updateOffset(this, true); + return this; +} + +function endOf(units) { + var time, startOfDate; + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond' || !this.isValid()) { + return this; + } + + startOfDate = this._isUTC ? utcStartOfDate : localStartOfDate; + + switch (units) { + case 'year': + time = startOfDate(this.year() + 1, 0, 1) - 1; + break; + case 'quarter': + time = + startOfDate( + this.year(), + this.month() - (this.month() % 3) + 3, + 1 + ) - 1; + break; + case 'month': + time = startOfDate(this.year(), this.month() + 1, 1) - 1; + break; + case 'week': + time = + startOfDate( + this.year(), + this.month(), + this.date() - this.weekday() + 7 + ) - 1; + break; + case 'isoWeek': + time = + startOfDate( + this.year(), + this.month(), + this.date() - (this.isoWeekday() - 1) + 7 + ) - 1; + break; + case 'day': + case 'date': + time = startOfDate(this.year(), this.month(), this.date() + 1) - 1; + break; + case 'hour': + time = this._d.valueOf(); + time += + MS_PER_HOUR - + mod$1( + time + (this._isUTC ? 0 : this.utcOffset() * MS_PER_MINUTE), + MS_PER_HOUR + ) - + 1; + break; + case 'minute': + time = this._d.valueOf(); + time += MS_PER_MINUTE - mod$1(time, MS_PER_MINUTE) - 1; + break; + case 'second': + time = this._d.valueOf(); + time += MS_PER_SECOND - mod$1(time, MS_PER_SECOND) - 1; + break; + } + + this._d.setTime(time); + hooks.updateOffset(this, true); + return this; +} + +function valueOf() { + return this._d.valueOf() - (this._offset || 0) * 60000; +} + +function unix() { + return Math.floor(this.valueOf() / 1000); +} + +function toDate() { + return new Date(this.valueOf()); +} + +function toArray() { + var m = this; + return [ + m.year(), + m.month(), + m.date(), + m.hour(), + m.minute(), + m.second(), + m.millisecond(), + ]; +} + +function toObject() { + var m = this; + return { + years: m.year(), + months: m.month(), + date: m.date(), + hours: m.hours(), + minutes: m.minutes(), + seconds: m.seconds(), + milliseconds: m.milliseconds(), + }; +} + +function toJSON() { + // new Date(NaN).toJSON() === null + return this.isValid() ? this.toISOString() : null; +} + +function isValid$2() { + return isValid(this); +} + +function parsingFlags() { + return extend({}, getParsingFlags(this)); +} + +function invalidAt() { + return getParsingFlags(this).overflow; +} + +function creationData() { + return { + input: this._i, + format: this._f, + locale: this._locale, + isUTC: this._isUTC, + strict: this._strict, + }; +} + +addFormatToken('N', 0, 0, 'eraAbbr'); +addFormatToken('NN', 0, 0, 'eraAbbr'); +addFormatToken('NNN', 0, 0, 'eraAbbr'); +addFormatToken('NNNN', 0, 0, 'eraName'); +addFormatToken('NNNNN', 0, 0, 'eraNarrow'); + +addFormatToken('y', ['y', 1], 'yo', 'eraYear'); +addFormatToken('y', ['yy', 2], 0, 'eraYear'); +addFormatToken('y', ['yyy', 3], 0, 'eraYear'); +addFormatToken('y', ['yyyy', 4], 0, 'eraYear'); + +addRegexToken('N', matchEraAbbr); +addRegexToken('NN', matchEraAbbr); +addRegexToken('NNN', matchEraAbbr); +addRegexToken('NNNN', matchEraName); +addRegexToken('NNNNN', matchEraNarrow); + +addParseToken( + ['N', 'NN', 'NNN', 'NNNN', 'NNNNN'], + function (input, array, config, token) { + var era = config._locale.erasParse(input, token, config._strict); + if (era) { + getParsingFlags(config).era = era; + } else { + getParsingFlags(config).invalidEra = input; + } + } +); + +addRegexToken('y', matchUnsigned); +addRegexToken('yy', matchUnsigned); +addRegexToken('yyy', matchUnsigned); +addRegexToken('yyyy', matchUnsigned); +addRegexToken('yo', matchEraYearOrdinal); + +addParseToken(['y', 'yy', 'yyy', 'yyyy'], YEAR); +addParseToken(['yo'], function (input, array, config, token) { + var match; + if (config._locale._eraYearOrdinalRegex) { + match = input.match(config._locale._eraYearOrdinalRegex); + } + + if (config._locale.eraYearOrdinalParse) { + array[YEAR] = config._locale.eraYearOrdinalParse(input, match); + } else { + array[YEAR] = parseInt(input, 10); + } +}); + +function localeEras(m, format) { + var i, + l, + date, + eras = this._eras || getLocale('en')._eras; + for (i = 0, l = eras.length; i < l; ++i) { + switch (typeof eras[i].since) { + case 'string': + // truncate time + date = hooks(eras[i].since).startOf('day'); + eras[i].since = date.valueOf(); + break; + } + + switch (typeof eras[i].until) { + case 'undefined': + eras[i].until = +Infinity; + break; + case 'string': + // truncate time + date = hooks(eras[i].until).startOf('day').valueOf(); + eras[i].until = date.valueOf(); + break; + } + } + return eras; +} + +function localeErasParse(eraName, format, strict) { + var i, + l, + eras = this.eras(), + name, + abbr, + narrow; + eraName = eraName.toUpperCase(); + + for (i = 0, l = eras.length; i < l; ++i) { + name = eras[i].name.toUpperCase(); + abbr = eras[i].abbr.toUpperCase(); + narrow = eras[i].narrow.toUpperCase(); + + if (strict) { + switch (format) { + case 'N': + case 'NN': + case 'NNN': + if (abbr === eraName) { + return eras[i]; + } + break; + + case 'NNNN': + if (name === eraName) { + return eras[i]; + } + break; + + case 'NNNNN': + if (narrow === eraName) { + return eras[i]; + } + break; + } + } else if ([name, abbr, narrow].indexOf(eraName) >= 0) { + return eras[i]; + } + } +} + +function localeErasConvertYear(era, year) { + var dir = era.since <= era.until ? +1 : -1; + if (year === undefined) { + return hooks(era.since).year(); + } else { + return hooks(era.since).year() + (year - era.offset) * dir; + } +} + +function getEraName() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].name; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].name; + } + } + + return ''; +} + +function getEraNarrow() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].narrow; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].narrow; + } + } + + return ''; +} + +function getEraAbbr() { + var i, + l, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + // truncate time + val = this.clone().startOf('day').valueOf(); + + if (eras[i].since <= val && val <= eras[i].until) { + return eras[i].abbr; + } + if (eras[i].until <= val && val <= eras[i].since) { + return eras[i].abbr; + } + } + + return ''; +} + +function getEraYear() { + var i, + l, + dir, + val, + eras = this.localeData().eras(); + for (i = 0, l = eras.length; i < l; ++i) { + dir = eras[i].since <= eras[i].until ? +1 : -1; + + // truncate time + val = this.clone().startOf('day').valueOf(); + + if ( + (eras[i].since <= val && val <= eras[i].until) || + (eras[i].until <= val && val <= eras[i].since) + ) { + return ( + (this.year() - hooks(eras[i].since).year()) * dir + + eras[i].offset + ); + } + } + + return this.year(); +} + +function erasNameRegex(isStrict) { + if (!hasOwnProp(this, '_erasNameRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasNameRegex : this._erasRegex; +} + +function erasAbbrRegex(isStrict) { + if (!hasOwnProp(this, '_erasAbbrRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasAbbrRegex : this._erasRegex; +} + +function erasNarrowRegex(isStrict) { + if (!hasOwnProp(this, '_erasNarrowRegex')) { + computeErasParse.call(this); + } + return isStrict ? this._erasNarrowRegex : this._erasRegex; +} + +function matchEraAbbr(isStrict, locale) { + return locale.erasAbbrRegex(isStrict); +} + +function matchEraName(isStrict, locale) { + return locale.erasNameRegex(isStrict); +} + +function matchEraNarrow(isStrict, locale) { + return locale.erasNarrowRegex(isStrict); +} + +function matchEraYearOrdinal(isStrict, locale) { + return locale._eraYearOrdinalRegex || matchUnsigned; +} + +function computeErasParse() { + var abbrPieces = [], + namePieces = [], + narrowPieces = [], + mixedPieces = [], + i, + l, + erasName, + erasAbbr, + erasNarrow, + eras = this.eras(); + + for (i = 0, l = eras.length; i < l; ++i) { + erasName = regexEscape(eras[i].name); + erasAbbr = regexEscape(eras[i].abbr); + erasNarrow = regexEscape(eras[i].narrow); + + namePieces.push(erasName); + abbrPieces.push(erasAbbr); + narrowPieces.push(erasNarrow); + mixedPieces.push(erasName); + mixedPieces.push(erasAbbr); + mixedPieces.push(erasNarrow); + } + + this._erasRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._erasNameRegex = new RegExp('^(' + namePieces.join('|') + ')', 'i'); + this._erasAbbrRegex = new RegExp('^(' + abbrPieces.join('|') + ')', 'i'); + this._erasNarrowRegex = new RegExp( + '^(' + narrowPieces.join('|') + ')', + 'i' + ); +} + +// FORMATTING + +addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; +}); + +addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; +}); + +function addWeekYearFormatToken(token, getter) { + addFormatToken(0, [token, token.length], 0, getter); +} + +addWeekYearFormatToken('gggg', 'weekYear'); +addWeekYearFormatToken('ggggg', 'weekYear'); +addWeekYearFormatToken('GGGG', 'isoWeekYear'); +addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + +// ALIASES + +// PARSING + +addRegexToken('G', matchSigned); +addRegexToken('g', matchSigned); +addRegexToken('GG', match1to2, match2); +addRegexToken('gg', match1to2, match2); +addRegexToken('GGGG', match1to4, match4); +addRegexToken('gggg', match1to4, match4); +addRegexToken('GGGGG', match1to6, match6); +addRegexToken('ggggg', match1to6, match6); + +addWeekParseToken( + ['gggg', 'ggggg', 'GGGG', 'GGGGG'], + function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + } +); + +addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = hooks.parseTwoDigitYear(input); +}); + +// MOMENTS + +function getSetWeekYear(input) { + return getSetWeekYearHelper.call( + this, + input, + this.week(), + this.weekday() + this.localeData()._week.dow, + this.localeData()._week.dow, + this.localeData()._week.doy + ); +} + +function getSetISOWeekYear(input) { + return getSetWeekYearHelper.call( + this, + input, + this.isoWeek(), + this.isoWeekday(), + 1, + 4 + ); +} + +function getISOWeeksInYear() { + return weeksInYear(this.year(), 1, 4); +} + +function getISOWeeksInISOWeekYear() { + return weeksInYear(this.isoWeekYear(), 1, 4); +} + +function getWeeksInYear() { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); +} + +function getWeeksInWeekYear() { + var weekInfo = this.localeData()._week; + return weeksInYear(this.weekYear(), weekInfo.dow, weekInfo.doy); +} + +function getSetWeekYearHelper(input, week, weekday, dow, doy) { + var weeksTarget; + if (input == null) { + return weekOfYear(this, dow, doy).year; + } else { + weeksTarget = weeksInYear(input, dow, doy); + if (week > weeksTarget) { + week = weeksTarget; + } + return setWeekAll.call(this, input, week, weekday, dow, doy); + } +} + +function setWeekAll(weekYear, week, weekday, dow, doy) { + var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), + date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); + + this.year(date.getUTCFullYear()); + this.month(date.getUTCMonth()); + this.date(date.getUTCDate()); + return this; +} + +// FORMATTING + +addFormatToken('Q', 0, 'Qo', 'quarter'); + +// PARSING + +addRegexToken('Q', match1); +addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; +}); + +// MOMENTS + +function getSetQuarter(input) { + return input == null + ? Math.ceil((this.month() + 1) / 3) + : this.month((input - 1) * 3 + (this.month() % 3)); +} + +// FORMATTING + +addFormatToken('D', ['DD', 2], 'Do', 'date'); + +// PARSING + +addRegexToken('D', match1to2, match1to2NoLeadingZero); +addRegexToken('DD', match1to2, match2); +addRegexToken('Do', function (isStrict, locale) { + // TODO: Remove "ordinalParse" fallback in next major release. + return isStrict + ? locale._dayOfMonthOrdinalParse || locale._ordinalParse + : locale._dayOfMonthOrdinalParseLenient; +}); + +addParseToken(['D', 'DD'], DATE); +addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0]); +}); + +// MOMENTS + +var getSetDayOfMonth = makeGetSet('Date', true); + +// FORMATTING + +addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + +// PARSING + +addRegexToken('DDD', match1to3); +addRegexToken('DDDD', match3); +addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); +}); + +// HELPERS + +// MOMENTS + +function getSetDayOfYear(input) { + var dayOfYear = + Math.round( + (this.clone().startOf('day') - this.clone().startOf('year')) / 864e5 + ) + 1; + return input == null ? dayOfYear : this.add(input - dayOfYear, 'd'); +} + +// FORMATTING + +addFormatToken('m', ['mm', 2], 0, 'minute'); + +// PARSING + +addRegexToken('m', match1to2, match1to2HasZero); +addRegexToken('mm', match1to2, match2); +addParseToken(['m', 'mm'], MINUTE); + +// MOMENTS + +var getSetMinute = makeGetSet('Minutes', false); + +// FORMATTING + +addFormatToken('s', ['ss', 2], 0, 'second'); + +// PARSING + +addRegexToken('s', match1to2, match1to2HasZero); +addRegexToken('ss', match1to2, match2); +addParseToken(['s', 'ss'], SECOND); + +// MOMENTS + +var getSetSecond = makeGetSet('Seconds', false); + +// FORMATTING + +addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); +}); + +addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); +}); + +addFormatToken(0, ['SSS', 3], 0, 'millisecond'); +addFormatToken(0, ['SSSS', 4], 0, function () { + return this.millisecond() * 10; +}); +addFormatToken(0, ['SSSSS', 5], 0, function () { + return this.millisecond() * 100; +}); +addFormatToken(0, ['SSSSSS', 6], 0, function () { + return this.millisecond() * 1000; +}); +addFormatToken(0, ['SSSSSSS', 7], 0, function () { + return this.millisecond() * 10000; +}); +addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + return this.millisecond() * 100000; +}); +addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + return this.millisecond() * 1000000; +}); + +// PARSING + +addRegexToken('S', match1to3, match1); +addRegexToken('SS', match1to3, match2); +addRegexToken('SSS', match1to3, match3); + +var token, getSetMillisecond; +for (token = 'SSSS'; token.length <= 9; token += 'S') { + addRegexToken(token, matchUnsigned); +} + +function parseMs(input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); +} + +for (token = 'S'; token.length <= 9; token += 'S') { + addParseToken(token, parseMs); +} + +getSetMillisecond = makeGetSet('Milliseconds', false); + +// FORMATTING + +addFormatToken('z', 0, 0, 'zoneAbbr'); +addFormatToken('zz', 0, 0, 'zoneName'); + +// MOMENTS + +function getZoneAbbr() { + return this._isUTC ? 'UTC' : ''; +} + +function getZoneName() { + return this._isUTC ? 'Coordinated Universal Time' : ''; +} + +var proto = Moment.prototype; + +proto.add = add; +proto.calendar = calendar$1; +proto.clone = clone; +proto.diff = diff; +proto.endOf = endOf; +proto.format = format; +proto.from = from; +proto.fromNow = fromNow; +proto.to = to; +proto.toNow = toNow; +proto.get = stringGet; +proto.invalidAt = invalidAt; +proto.isAfter = isAfter; +proto.isBefore = isBefore; +proto.isBetween = isBetween; +proto.isSame = isSame; +proto.isSameOrAfter = isSameOrAfter; +proto.isSameOrBefore = isSameOrBefore; +proto.isValid = isValid$2; +proto.lang = lang; +proto.locale = locale; +proto.localeData = localeData; +proto.max = prototypeMax; +proto.min = prototypeMin; +proto.parsingFlags = parsingFlags; +proto.set = stringSet; +proto.startOf = startOf; +proto.subtract = subtract; +proto.toArray = toArray; +proto.toObject = toObject; +proto.toDate = toDate; +proto.toISOString = toISOString; +proto.inspect = inspect; +if (typeof Symbol !== 'undefined' && Symbol.for != null) { + proto[Symbol.for('nodejs.util.inspect.custom')] = function () { + return 'Moment<' + this.format() + '>'; + }; +} +proto.toJSON = toJSON; +proto.toString = toString; +proto.unix = unix; +proto.valueOf = valueOf; +proto.creationData = creationData; +proto.eraName = getEraName; +proto.eraNarrow = getEraNarrow; +proto.eraAbbr = getEraAbbr; +proto.eraYear = getEraYear; +proto.year = getSetYear; +proto.isLeapYear = getIsLeapYear; +proto.weekYear = getSetWeekYear; +proto.isoWeekYear = getSetISOWeekYear; +proto.quarter = proto.quarters = getSetQuarter; +proto.month = getSetMonth; +proto.daysInMonth = getDaysInMonth; +proto.week = proto.weeks = getSetWeek; +proto.isoWeek = proto.isoWeeks = getSetISOWeek; +proto.weeksInYear = getWeeksInYear; +proto.weeksInWeekYear = getWeeksInWeekYear; +proto.isoWeeksInYear = getISOWeeksInYear; +proto.isoWeeksInISOWeekYear = getISOWeeksInISOWeekYear; +proto.date = getSetDayOfMonth; +proto.day = proto.days = getSetDayOfWeek; +proto.weekday = getSetLocaleDayOfWeek; +proto.isoWeekday = getSetISODayOfWeek; +proto.dayOfYear = getSetDayOfYear; +proto.hour = proto.hours = getSetHour; +proto.minute = proto.minutes = getSetMinute; +proto.second = proto.seconds = getSetSecond; +proto.millisecond = proto.milliseconds = getSetMillisecond; +proto.utcOffset = getSetOffset; +proto.utc = setOffsetToUTC; +proto.local = setOffsetToLocal; +proto.parseZone = setOffsetToParsedOffset; +proto.hasAlignedHourOffset = hasAlignedHourOffset; +proto.isDST = isDaylightSavingTime; +proto.isLocal = isLocal; +proto.isUtcOffset = isUtcOffset; +proto.isUtc = isUtc; +proto.isUTC = isUtc; +proto.zoneAbbr = getZoneAbbr; +proto.zoneName = getZoneName; +proto.dates = deprecate( + 'dates accessor is deprecated. Use date instead.', + getSetDayOfMonth +); +proto.months = deprecate( + 'months accessor is deprecated. Use month instead', + getSetMonth +); +proto.years = deprecate( + 'years accessor is deprecated. Use year instead', + getSetYear +); +proto.zone = deprecate( + 'moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/', + getSetZone +); +proto.isDSTShifted = deprecate( + 'isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information', + isDaylightSavingTimeShifted +); + +function createUnix(input) { + return createLocal(input * 1000); +} + +function createInZone() { + return createLocal.apply(null, arguments).parseZone(); +} + +function preParsePostFormat(string) { + return string; +} + +var proto$1 = Locale.prototype; + +proto$1.calendar = calendar; +proto$1.longDateFormat = longDateFormat; +proto$1.invalidDate = invalidDate; +proto$1.ordinal = ordinal; +proto$1.preparse = preParsePostFormat; +proto$1.postformat = preParsePostFormat; +proto$1.relativeTime = relativeTime; +proto$1.pastFuture = pastFuture; +proto$1.set = set; +proto$1.eras = localeEras; +proto$1.erasParse = localeErasParse; +proto$1.erasConvertYear = localeErasConvertYear; +proto$1.erasAbbrRegex = erasAbbrRegex; +proto$1.erasNameRegex = erasNameRegex; +proto$1.erasNarrowRegex = erasNarrowRegex; + +proto$1.months = localeMonths; +proto$1.monthsShort = localeMonthsShort; +proto$1.monthsParse = localeMonthsParse; +proto$1.monthsRegex = monthsRegex; +proto$1.monthsShortRegex = monthsShortRegex; +proto$1.week = localeWeek; +proto$1.firstDayOfYear = localeFirstDayOfYear; +proto$1.firstDayOfWeek = localeFirstDayOfWeek; + +proto$1.weekdays = localeWeekdays; +proto$1.weekdaysMin = localeWeekdaysMin; +proto$1.weekdaysShort = localeWeekdaysShort; +proto$1.weekdaysParse = localeWeekdaysParse; + +proto$1.weekdaysRegex = weekdaysRegex; +proto$1.weekdaysShortRegex = weekdaysShortRegex; +proto$1.weekdaysMinRegex = weekdaysMinRegex; + +proto$1.isPM = localeIsPM; +proto$1.meridiem = localeMeridiem; + +function get$1(format, index, field, setter) { + var locale = getLocale(), + utc = createUTC().set(setter, index); + return locale[field](utc, format); +} + +function listMonthsImpl(format, index, field) { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + + if (index != null) { + return get$1(format, index, field, 'month'); + } + + var i, + out = []; + for (i = 0; i < 12; i++) { + out[i] = get$1(format, i, field, 'month'); + } + return out; +} + +// () +// (5) +// (fmt, 5) +// (fmt) +// (true) +// (true, 5) +// (true, fmt, 5) +// (true, fmt) +function listWeekdaysImpl(localeSorted, format, index, field) { + if (typeof localeSorted === 'boolean') { + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } else { + format = localeSorted; + index = format; + localeSorted = false; + + if (isNumber(format)) { + index = format; + format = undefined; + } + + format = format || ''; + } + + var locale = getLocale(), + shift = localeSorted ? locale._week.dow : 0, + i, + out = []; + + if (index != null) { + return get$1(format, (index + shift) % 7, field, 'day'); + } + + for (i = 0; i < 7; i++) { + out[i] = get$1(format, (i + shift) % 7, field, 'day'); + } + return out; +} + +function listMonths(format, index) { + return listMonthsImpl(format, index, 'months'); +} + +function listMonthsShort(format, index) { + return listMonthsImpl(format, index, 'monthsShort'); +} + +function listWeekdays(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdays'); +} + +function listWeekdaysShort(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysShort'); +} + +function listWeekdaysMin(localeSorted, format, index) { + return listWeekdaysImpl(localeSorted, format, index, 'weekdaysMin'); +} + +getSetGlobalLocale('en', { + eras: [ + { + since: '0001-01-01', + until: +Infinity, + offset: 1, + name: 'Anno Domini', + narrow: 'AD', + abbr: 'AD', + }, + { + since: '0000-12-31', + until: -Infinity, + offset: 1, + name: 'Before Christ', + narrow: 'BC', + abbr: 'BC', + }, + ], + dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal: function (number) { + var b = number % 10, + output = + toInt((number % 100) / 10) === 1 + ? 'th' + : b === 1 + ? 'st' + : b === 2 + ? 'nd' + : b === 3 + ? 'rd' + : 'th'; + return number + output; + }, +}); + +// Side effect imports + +hooks.lang = deprecate( + 'moment.lang is deprecated. Use moment.locale instead.', + getSetGlobalLocale +); +hooks.langData = deprecate( + 'moment.langData is deprecated. Use moment.localeData instead.', + getLocale +); + +var mathAbs = Math.abs; + +function abs() { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; +} + +function addSubtract$1(duration, input, value, direction) { + var other = createDuration(input, value); + + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); +} + +// supports only 2.0-style add(1, 's') or add(duration) +function add$1(input, value) { + return addSubtract$1(this, input, value, 1); +} + +// supports only 2.0-style subtract(1, 's') or subtract(duration) +function subtract$1(input, value) { + return addSubtract$1(this, input, value, -1); +} + +function absCeil(number) { + if (number < 0) { + return Math.floor(number); + } else { + return Math.ceil(number); + } +} + +function bubble() { + var milliseconds = this._milliseconds, + days = this._days, + months = this._months, + data = this._data, + seconds, + minutes, + hours, + years, + monthsFromDays; + + // if we have a mix of positive and negative values, bubble down first + // check: https://github.com/moment/moment/issues/2166 + if ( + !( + (milliseconds >= 0 && days >= 0 && months >= 0) || + (milliseconds <= 0 && days <= 0 && months <= 0) + ) + ) { + milliseconds += absCeil(monthsToDays(months) + days) * 864e5; + days = 0; + months = 0; + } + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; + + hours = absFloor(minutes / 60); + data.hours = hours % 24; + + days += absFloor(hours / 24); + + // convert days to months + monthsFromDays = absFloor(daysToMonths(days)); + months += monthsFromDays; + days -= absCeil(monthsToDays(monthsFromDays)); + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; +} + +function daysToMonths(days) { + // 400 years have 146097 days (taking into account leap year rules) + // 400 years have 12 months === 4800 + return (days * 4800) / 146097; +} + +function monthsToDays(months) { + // the reverse of daysToMonths + return (months * 146097) / 4800; +} + +function as(units) { + if (!this.isValid()) { + return NaN; + } + var days, + months, + milliseconds = this._milliseconds; + + units = normalizeUnits(units); + + if (units === 'month' || units === 'quarter' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToMonths(days); + switch (units) { + case 'month': + return months; + case 'quarter': + return months / 3; + case 'year': + return months / 12; + } + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(monthsToDays(this._months)); + switch (units) { + case 'week': + return days / 7 + milliseconds / 6048e5; + case 'day': + return days + milliseconds / 864e5; + case 'hour': + return days * 24 + milliseconds / 36e5; + case 'minute': + return days * 1440 + milliseconds / 6e4; + case 'second': + return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': + return Math.floor(days * 864e5) + milliseconds; + default: + throw new Error('Unknown unit ' + units); + } + } +} + +function makeAs(alias) { + return function () { + return this.as(alias); + }; +} + +var asMilliseconds = makeAs('ms'), + asSeconds = makeAs('s'), + asMinutes = makeAs('m'), + asHours = makeAs('h'), + asDays = makeAs('d'), + asWeeks = makeAs('w'), + asMonths = makeAs('M'), + asQuarters = makeAs('Q'), + asYears = makeAs('y'), + valueOf$1 = asMilliseconds; + +function clone$1() { + return createDuration(this); +} + +function get$2(units) { + units = normalizeUnits(units); + return this.isValid() ? this[units + 's']() : NaN; +} + +function makeGetter(name) { + return function () { + return this.isValid() ? this._data[name] : NaN; + }; +} + +var milliseconds = makeGetter('milliseconds'), + seconds = makeGetter('seconds'), + minutes = makeGetter('minutes'), + hours = makeGetter('hours'), + days = makeGetter('days'), + months = makeGetter('months'), + years = makeGetter('years'); + +function weeks() { + return absFloor(this.days() / 7); +} + +var round = Math.round, + thresholds = { + ss: 44, // a few seconds to seconds + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month/week + w: null, // weeks to month + M: 11, // months to year + }; + +// helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize +function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); +} + +function relativeTime$1(posNegDuration, withoutSuffix, thresholds, locale) { + var duration = createDuration(posNegDuration).abs(), + seconds = round(duration.as('s')), + minutes = round(duration.as('m')), + hours = round(duration.as('h')), + days = round(duration.as('d')), + months = round(duration.as('M')), + weeks = round(duration.as('w')), + years = round(duration.as('y')), + a = + (seconds <= thresholds.ss && ['s', seconds]) || + (seconds < thresholds.s && ['ss', seconds]) || + (minutes <= 1 && ['m']) || + (minutes < thresholds.m && ['mm', minutes]) || + (hours <= 1 && ['h']) || + (hours < thresholds.h && ['hh', hours]) || + (days <= 1 && ['d']) || + (days < thresholds.d && ['dd', days]); + + if (thresholds.w != null) { + a = + a || + (weeks <= 1 && ['w']) || + (weeks < thresholds.w && ['ww', weeks]); + } + a = a || + (months <= 1 && ['M']) || + (months < thresholds.M && ['MM', months]) || + (years <= 1 && ['y']) || ['yy', years]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); +} + +// This function allows you to set the rounding function for relative time strings +function getSetRelativeTimeRounding(roundingFunction) { + if (roundingFunction === undefined) { + return round; + } + if (typeof roundingFunction === 'function') { + round = roundingFunction; + return true; + } + return false; +} + +// This function allows you to set a threshold for relative time strings +function getSetRelativeTimeThreshold(threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + if (threshold === 's') { + thresholds.ss = limit - 1; + } + return true; +} + +function humanize(argWithSuffix, argThresholds) { + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var withSuffix = false, + th = thresholds, + locale, + output; + + if (typeof argWithSuffix === 'object') { + argThresholds = argWithSuffix; + argWithSuffix = false; + } + if (typeof argWithSuffix === 'boolean') { + withSuffix = argWithSuffix; + } + if (typeof argThresholds === 'object') { + th = Object.assign({}, thresholds, argThresholds); + if (argThresholds.s != null && argThresholds.ss == null) { + th.ss = argThresholds.s - 1; + } + } + + locale = this.localeData(); + output = relativeTime$1(this, !withSuffix, th, locale); + + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); +} + +var abs$1 = Math.abs; + +function sign(x) { + return (x > 0) - (x < 0) || +x; +} + +function toISOString$1() { + // for ISO strings we do not use the normal bubbling rules: + // * milliseconds bubble up until they become hours + // * days do not bubble at all + // * months bubble up until they become years + // This is because there is no context-free conversion between hours and days + // (think of clock changes) + // and also not between days and months (28-31 days per month) + if (!this.isValid()) { + return this.localeData().invalidDate(); + } + + var seconds = abs$1(this._milliseconds) / 1000, + days = abs$1(this._days), + months = abs$1(this._months), + minutes, + hours, + years, + s, + total = this.asSeconds(), + totalSign, + ymSign, + daysSign, + hmsSign; + + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + // 3600 seconds -> 60 minutes -> 1 hour + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); + seconds %= 60; + minutes %= 60; + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + s = seconds ? seconds.toFixed(3).replace(/\.?0+$/, '') : ''; + + totalSign = total < 0 ? '-' : ''; + ymSign = sign(this._months) !== sign(total) ? '-' : ''; + daysSign = sign(this._days) !== sign(total) ? '-' : ''; + hmsSign = sign(this._milliseconds) !== sign(total) ? '-' : ''; + + return ( + totalSign + + 'P' + + (years ? ymSign + years + 'Y' : '') + + (months ? ymSign + months + 'M' : '') + + (days ? daysSign + days + 'D' : '') + + (hours || minutes || seconds ? 'T' : '') + + (hours ? hmsSign + hours + 'H' : '') + + (minutes ? hmsSign + minutes + 'M' : '') + + (seconds ? hmsSign + s + 'S' : '') + ); +} + +var proto$2 = Duration.prototype; + +proto$2.isValid = isValid$1; +proto$2.abs = abs; +proto$2.add = add$1; +proto$2.subtract = subtract$1; +proto$2.as = as; +proto$2.asMilliseconds = asMilliseconds; +proto$2.asSeconds = asSeconds; +proto$2.asMinutes = asMinutes; +proto$2.asHours = asHours; +proto$2.asDays = asDays; +proto$2.asWeeks = asWeeks; +proto$2.asMonths = asMonths; +proto$2.asQuarters = asQuarters; +proto$2.asYears = asYears; +proto$2.valueOf = valueOf$1; +proto$2._bubble = bubble; +proto$2.clone = clone$1; +proto$2.get = get$2; +proto$2.milliseconds = milliseconds; +proto$2.seconds = seconds; +proto$2.minutes = minutes; +proto$2.hours = hours; +proto$2.days = days; +proto$2.weeks = weeks; +proto$2.months = months; +proto$2.years = years; +proto$2.humanize = humanize; +proto$2.toISOString = toISOString$1; +proto$2.toString = toISOString$1; +proto$2.toJSON = toISOString$1; +proto$2.locale = locale; +proto$2.localeData = localeData; + +proto$2.toIsoString = deprecate( + 'toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', + toISOString$1 +); +proto$2.lang = lang; + +// FORMATTING + +addFormatToken('X', 0, 0, 'unix'); +addFormatToken('x', 0, 0, 'valueOf'); + +// PARSING + +addRegexToken('x', matchSigned); +addRegexToken('X', matchTimestamp); +addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input) * 1000); +}); +addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); +}); + +//! moment.js + +hooks.version = '2.30.1'; + +setHookCallback(createLocal); + +hooks.fn = proto; +hooks.min = min; +hooks.max = max; +hooks.now = now; +hooks.utc = createUTC; +hooks.unix = createUnix; +hooks.months = listMonths; +hooks.isDate = isDate; +hooks.locale = getSetGlobalLocale; +hooks.invalid = createInvalid; +hooks.duration = createDuration; +hooks.isMoment = isMoment; +hooks.weekdays = listWeekdays; +hooks.parseZone = createInZone; +hooks.localeData = getLocale; +hooks.isDuration = isDuration; +hooks.monthsShort = listMonthsShort; +hooks.weekdaysMin = listWeekdaysMin; +hooks.defineLocale = defineLocale; +hooks.updateLocale = updateLocale; +hooks.locales = listLocales; +hooks.weekdaysShort = listWeekdaysShort; +hooks.normalizeUnits = normalizeUnits; +hooks.relativeTimeRounding = getSetRelativeTimeRounding; +hooks.relativeTimeThreshold = getSetRelativeTimeThreshold; +hooks.calendarFormat = getCalendarFormat; +hooks.prototype = proto; + +// currently HTML5 input type only supports 24-hour formats +hooks.HTML5_FMT = { + DATETIME_LOCAL: 'YYYY-MM-DDTHH:mm', // + DATETIME_LOCAL_SECONDS: 'YYYY-MM-DDTHH:mm:ss', // + DATETIME_LOCAL_MS: 'YYYY-MM-DDTHH:mm:ss.SSS', // + DATE: 'YYYY-MM-DD', // + TIME: 'HH:mm', // + TIME_SECONDS: 'HH:mm:ss', // + TIME_MS: 'HH:mm:ss.SSS', // + WEEK: 'GGGG-[W]WW', // + MONTH: 'YYYY-MM', // +}; + +export default hooks; diff --git a/tests/test_app/templates/pyscript.html b/tests/test_app/templates/pyscript.html new file mode 100644 index 00000000..57a5dd15 --- /dev/null +++ b/tests/test_app/templates/pyscript.html @@ -0,0 +1,33 @@ +{% load static %} {% load reactpy %} + + + + + + + + + ReactPy + {% pyscript_setup extra_js='{"/static/moment.js":"moment"}' config="{}" %} + + + +

ReactPy PyScript Test Page

+
+ {% pyscript_component "./test_app/pyscript/components/hello_world.py" initial="
Loading...
" %} +
+ {% pyscript_component "./test_app/pyscript/components/custom_root.py" root="main" %} +
+ {% pyscript_component "./test_app/pyscript/components/multifile_parent.py" "./test_app/pyscript/components/multifile_child.py" %} +
+ {% pyscript_component "./test_app/pyscript/components/counter.py" %} +
+ {% component "test_app.pyscript.components.server_side.parent" %} +
+ {% component "test_app.pyscript.components.server_side.parent_toggle" %} +
+ {% pyscript_component "./test_app/pyscript/components/remote_js_module.py" %} +
+ + + diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index d92867cd..11fdc390 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -678,3 +678,47 @@ def test_channel_layer_components(self): finally: new_page.close() + + def test_pyscript_components(self): + new_page = self.browser.new_page() + try: + new_page.goto(f"{self.live_server_url}/pyscript/") + new_page.wait_for_selector("#hello-world-loading") + new_page.wait_for_selector("#hello-world") + new_page.wait_for_selector("#custom-root") + new_page.wait_for_selector("#multifile-parent") + new_page.wait_for_selector("#multifile-child") + + new_page.wait_for_selector("#counter") + new_page.wait_for_selector("#counter pre[data-value='0']") + new_page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#counter pre[data-value='1']") + new_page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#counter pre[data-value='2']") + new_page.wait_for_selector("#counter .minus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#counter pre[data-value='1']") + + new_page.wait_for_selector("#parent") + new_page.wait_for_selector("#child") + new_page.wait_for_selector("#child pre[data-value='0']") + new_page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#child pre[data-value='1']") + new_page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#child pre[data-value='2']") + new_page.wait_for_selector("#child .minus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#child pre[data-value='1']") + + new_page.wait_for_selector("#parent-toggle") + new_page.wait_for_selector("#parent-toggle button").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#parent-toggle") + new_page.wait_for_selector("#parent-toggle pre[data-value='0']") + new_page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#parent-toggle pre[data-value='1']") + new_page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#parent-toggle pre[data-value='2']") + new_page.wait_for_selector("#parent-toggle .minus").click(delay=CLICK_DELAY) + new_page.wait_for_selector("#parent-toggle pre[data-value='1']") + + new_page.wait_for_selector("#moment[data-success=true]") + finally: + new_page.close() diff --git a/tests/test_app/urls.py b/tests/test_app/urls.py index 05acb163..070b74f1 100644 --- a/tests/test_app/urls.py +++ b/tests/test_app/urls.py @@ -14,6 +14,7 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ + from django.contrib import admin from django.urls import include, path @@ -30,6 +31,7 @@ path("", include("test_app.prerender.urls")), path("", include("test_app.performance.urls")), path("", include("test_app.router.urls")), + path("", include("test_app.pyscript.urls")), path("", include("test_app.offline.urls")), path("", include("test_app.channel_layers.urls")), path("reactpy/", include("reactpy_django.http.urls")), From 9b36b6d371c611ceaa1f51815b87ddc43ead6804 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sat, 22 Jun 2024 04:51:24 -0700 Subject: [PATCH 07/35] v4.0.0 (#244) --- CHANGELOG.md | 7 ++++++- src/reactpy_django/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e1e5b29..620c1f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ Using the following categories, list your changes in this order: ## [Unreleased] +- Nothing (yet)! + +## [4.0.0] + ### Added - Client-side Python components can now be rendered via the new `{% pyscript_component %}` template tag @@ -497,7 +501,8 @@ Using the following categories, list your changes in this order: - Support for IDOM within the Django -[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/3.8.1...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...HEAD +[4.0.0]: https://github.com/reactive-python/reactpy-django/compare/3.8.1...4.0.0 [3.8.1]: https://github.com/reactive-python/reactpy-django/compare/3.8.0...3.8.1 [3.8.0]: https://github.com/reactive-python/reactpy-django/compare/3.7.0...3.8.0 [3.7.0]: https://github.com/reactive-python/reactpy-django/compare/3.6.0...3.7.0 diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index 0bbff9d1..d4ece375 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -17,7 +17,7 @@ REACTPY_WEBSOCKET_ROUTE, ) -__version__ = "3.8.1" +__version__ = "4.0.0" __all__ = [ "REACTPY_WEBSOCKET_PATH", "REACTPY_WEBSOCKET_ROUTE", From 8531c04da6799be20b10599903d88cfdae56d0ed Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 23 Sep 2024 18:59:15 -0700 Subject: [PATCH 08/35] Fix Python and docs tests (#250) - Fix python tests failures caused by upgraded mypy stubs - Switch `linkcheckmd` to it's replacement `linkspector` --- .github/workflows/test-docs.yml | 9 ++++++--- .gitignore | 1 + .linkspector.yml | 7 +++++++ requirements/build-docs.txt | 1 - src/reactpy_django/clean.py | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 .linkspector.yml diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 907e1a2c..3062ebc1 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -20,12 +20,15 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x + - name: Check docs links + uses: umbrelladocs/action-linkspector@v1 + with: + github_token: ${{ secrets.github_token }} + reporter: github-pr-review + fail_on_error: true - name: Check docs build run: | pip install -r requirements/build-docs.txt - linkcheckMarkdown docs/ -v -r - linkcheckMarkdown README.md -v -r - linkcheckMarkdown CHANGELOG.md -v -r cd docs mkdocs build --strict - name: Check docs examples diff --git a/.gitignore b/.gitignore index ffabb7fc..e3cbc599 100644 --- a/.gitignore +++ b/.gitignore @@ -101,6 +101,7 @@ venv.bak/ # mkdocs documentation /site +docs/site # mypy .mypy_cache/ diff --git a/.linkspector.yml b/.linkspector.yml new file mode 100644 index 00000000..6c0747e7 --- /dev/null +++ b/.linkspector.yml @@ -0,0 +1,7 @@ +dirs: + - ./docs +files: + - README.md + - CHANGELOG.md +useGitIgnore: true +modifiedFilesOnly: false diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt index f32563ca..a7d5b1cf 100644 --- a/requirements/build-docs.txt +++ b/requirements/build-docs.txt @@ -2,7 +2,6 @@ mkdocs mkdocs-git-revision-date-localized-plugin mkdocs-material==9.4.0 mkdocs-include-markdown-plugin -linkcheckmd mkdocs-spellcheck[all] mkdocs-git-authors-plugin mkdocs-minify-plugin diff --git a/src/reactpy_django/clean.py b/src/reactpy_django/clean.py index a5953adc..93df7be6 100644 --- a/src/reactpy_django/clean.py +++ b/src/reactpy_django/clean.py @@ -87,7 +87,7 @@ def clean_user_data(verbosity: int = 1): start_time = timezone.now() user_model = get_user_model() all_users = user_model.objects.all() - all_user_pks = all_users.values_list(user_model._meta.pk.name, flat=True) # type: ignore + all_user_pks = all_users.values_list(user_model._meta.pk.name, flat=True) # Django doesn't support using QuerySets as an argument with cross-database relations. if user_model.objects.db != UserDataModel.objects.db: From 50c26e459b3b7a4b98caf94e64c786f509bed772 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Tue, 24 Sep 2024 01:33:21 -0700 Subject: [PATCH 09/35] Docs updates & Docs CI concurrency fix (#249) - Minor improvements to docs clarity. - Prevent concurrent docs publishing runs from stomping on each other. --- .github/workflows/publish-develop-docs.yml | 2 + .github/workflows/publish-release-docs.yml | 2 + CHANGELOG.md | 4 +- .../home-code-examples/add-interactivity.py | 12 +- .../create-user-interfaces.py | 18 +- .../write-components-with-python.py | 5 +- docs/src/assets/css/home.css | 474 +++++++++--------- docs/src/assets/img/add-interactivity.png | Bin 20118 -> 20821 bytes .../src/assets/img/create-user-interfaces.png | Bin 12381 -> 12427 bytes .../img/write-components-with-python.png | Bin 15412 -> 16519 bytes docs/src/reference/components.md | 2 + docs/src/reference/decorators.md | 2 +- docs/src/reference/hooks.md | 4 +- docs/src/reference/html.md | 6 +- docs/src/reference/template-tag.md | 7 +- docs/src/reference/utils.md | 2 +- src/reactpy_django/utils.py | 2 +- 17 files changed, 277 insertions(+), 265 deletions(-) diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index b79d3cd2..c2b62d95 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -21,3 +21,5 @@ jobs: git config user.email github-actions@github.com cd docs mike deploy --push develop + concurrency: + group: publish-docs diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml index a98e9869..3f24d129 100644 --- a/.github/workflows/publish-release-docs.yml +++ b/.github/workflows/publish-release-docs.yml @@ -21,3 +21,5 @@ jobs: git config user.email github-actions@github.com cd docs mike deploy --push --update-aliases ${{ github.event.release.name }} latest + concurrency: + group: publish-docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 620c1f75..9652b644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,7 +28,9 @@ Using the following categories, list your changes in this order: ### Security - for vulnerability fixes. - --> + +Don't forget to remove deprecated code on each major release! +--> diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/home-code-examples/add-interactivity.py index 90976446..f29ba3c8 100644 --- a/docs/overrides/home-code-examples/add-interactivity.py +++ b/docs/overrides/home-code-examples/add-interactivity.py @@ -1,16 +1,14 @@ +# pylint: disable=assignment-from-no-return, unnecessary-lambda from reactpy import component, html, use_state -def filter_videos(videos, search_text): - return None +def filter_videos(*_, **__): ... -def search_input(dictionary, value): - return None +def search_input(*_, **__): ... -def video_list(videos, empty_heading): - return None +def video_list(*_, **__): ... @component @@ -20,7 +18,7 @@ def searchable_video_list(videos): return html._( search_input( - {"on_change": lambda new_text: set_search_text(new_text)}, + {"onChange": lambda new_text: set_search_text(new_text)}, value=search_text, ), video_list( diff --git a/docs/overrides/home-code-examples/create-user-interfaces.py b/docs/overrides/home-code-examples/create-user-interfaces.py index 37776ab1..873b9d88 100644 --- a/docs/overrides/home-code-examples/create-user-interfaces.py +++ b/docs/overrides/home-code-examples/create-user-interfaces.py @@ -1,22 +1,20 @@ from reactpy import component, html -def thumbnail(video): - return None +def thumbnail(*_, **__): ... -def like_button(video): - return None +def like_button(*_, **__): ... @component -def video(video): +def video(data): return html.div( - thumbnail(video), + thumbnail(data), html.a( - {"href": video.url}, - html.h3(video.title), - html.p(video.description), + {"href": data.url}, + html.h3(data.title), + html.p(data.description), ), - like_button(video), + like_button(data), ) diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/home-code-examples/write-components-with-python.py index 6af43baa..47e28b68 100644 --- a/docs/overrides/home-code-examples/write-components-with-python.py +++ b/docs/overrides/home-code-examples/write-components-with-python.py @@ -1,6 +1,9 @@ from reactpy import component, html +def video(*_, **__): ... + + @component def video_list(videos, empty_heading): count = len(videos) @@ -11,5 +14,5 @@ def video_list(videos, empty_heading): return html.section( html.h2(heading), - [video(video) for video in videos], + [video(x, key=x.id) for x in videos], ) diff --git a/docs/src/assets/css/home.css b/docs/src/assets/css/home.css index c72e7093..70f05cf2 100644 --- a/docs/src/assets/css/home.css +++ b/docs/src/assets/css/home.css @@ -1,335 +1,337 @@ img.home-logo { - height: 120px; + height: 120px; } .home .row { - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - padding: 6rem 0.8rem; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + padding: 6rem 0.8rem; } .home .row:not(.first, .stripe) { - background: var(--row-bg-color); + background: var(--row-bg-color); } .home .row.stripe { - background: var(--row-stripe-bg-color); - border: 0 solid var(--stripe-border-color); - border-top-width: 1px; - border-bottom-width: 1px; + background: var(--row-stripe-bg-color); + border: 0 solid var(--stripe-border-color); + border-top-width: 1px; + border-bottom-width: 1px; } .home .row.first { - text-align: center; + text-align: center; } .home .row h1 { - max-width: 28rem; - line-height: 1.15; - font-weight: 500; - margin-bottom: 0.55rem; - margin-top: -1rem; + max-width: 28rem; + line-height: 1.15; + font-weight: 500; + margin-bottom: 0.55rem; + margin-top: -1rem; } .home .row.first h1 { - margin-top: 0.55rem; - margin-bottom: -0.75rem; + margin-top: 0.55rem; + margin-bottom: -0.75rem; } .home .row > p { - max-width: 35rem; - line-height: 1.5; - font-weight: 400; + max-width: 35rem; + line-height: 1.5; + font-weight: 400; } .home .row.first > p { - font-size: 32px; - font-weight: 500; + font-size: 32px; + font-weight: 500; } /* Code blocks */ .home .row .tabbed-set { - background: var(--home-tabbed-set-bg-color); - margin: 0; + background: var(--home-tabbed-set-bg-color); + margin: 0; } .home .row .tabbed-content { - padding: 20px 18px; - overflow-x: auto; + padding: 20px 18px; + overflow-x: auto; } .home .row .tabbed-content img { - user-select: none; - -moz-user-select: none; - -webkit-user-drag: none; - -webkit-user-select: none; - -ms-user-select: none; - max-width: 580px; + user-select: none; + -moz-user-select: none; + -webkit-user-drag: none; + -webkit-user-select: none; + -ms-user-select: none; + max-width: 580px; } .home .row .tabbed-content { - -webkit-filter: var(--code-block-filter); - filter: var(--code-block-filter); + -webkit-filter: var(--code-block-filter); + filter: var(--code-block-filter); } /* Code examples */ .home .example-container { - background: radial-gradient( - circle at 0% 100%, - rgb(41 84 147 / 11%) 0%, - rgb(22 89 189 / 4%) 70%, - rgb(48 99 175 / 0%) 80% - ), - radial-gradient( - circle at 100% 100%, - rgb(24 87 45 / 55%) 0%, - rgb(29 61 12 / 4%) 70%, - rgb(94 116 93 / 0%) 80% - ), - radial-gradient( - circle at 100% 0%, - rgba(54, 66, 84, 0.55) 0%, - rgb(102 111 125 / 4%) 70%, - rgba(54, 66, 84, 0) 80% - ), - radial-gradient( - circle at 0% 0%, - rgba(91, 114, 135, 0.55) 0%, - rgb(45 111 171 / 4%) 70%, - rgb(5 82 153 / 0%) 80% - ), - rgb(0, 0, 0) center center/cover no-repeat fixed; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - align-items: center; - border-radius: 16px; - margin: 30px 0; - max-width: 100%; - grid-column-gap: 20px; - padding-left: 20px; - padding-right: 20px; + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 11%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 87 45 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: center; + border-radius: 16px; + margin: 30px 0; + max-width: 100%; + grid-column-gap: 20px; + padding-left: 20px; + padding-right: 20px; } .home .demo .white-bg { - background: #fff; - border-radius: 16px; - display: flex; - flex-direction: column; - max-width: 590px; - min-width: -webkit-min-content; - min-width: -moz-min-content; - min-width: min-content; - row-gap: 1rem; - padding: 1rem; + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + max-width: 590px; + min-width: -webkit-min-content; + min-width: -moz-min-content; + min-width: min-content; + row-gap: 1rem; + padding: 1rem; + border: 1px rgb(0 0 0 / 20%) solid; + overflow: hidden; } .home .demo .vid-row { - display: flex; - flex-direction: row; - -moz-column-gap: 12px; - column-gap: 12px; + display: flex; + flex-direction: row; + -moz-column-gap: 12px; + column-gap: 12px; } .home .demo { - color: #000; + color: #000; } .home .demo .vid-thumbnail { - background: radial-gradient( - circle at 0% 100%, - rgb(41 84 147 / 55%) 0%, - rgb(22 89 189 / 4%) 70%, - rgb(48 99 175 / 0%) 80% - ), - radial-gradient( - circle at 100% 100%, - rgb(24 63 87 / 55%) 0%, - rgb(29 61 12 / 4%) 70%, - rgb(94 116 93 / 0%) 80% - ), - radial-gradient( - circle at 100% 0%, - rgba(54, 66, 84, 0.55) 0%, - rgb(102 111 125 / 4%) 70%, - rgba(54, 66, 84, 0) 80% - ), - radial-gradient( - circle at 0% 0%, - rgba(91, 114, 135, 0.55) 0%, - rgb(45 111 171 / 4%) 70%, - rgb(5 82 153 / 0%) 80% - ), - rgb(0, 0, 0) center center/cover no-repeat fixed; - width: 9rem; - aspect-ratio: 16 / 9; - border-radius: 8px; - display: flex; - justify-content: center; - align-items: center; + background: radial-gradient( + circle at 0% 100%, + rgb(41 84 147 / 55%) 0%, + rgb(22 89 189 / 4%) 70%, + rgb(48 99 175 / 0%) 80% + ), + radial-gradient( + circle at 100% 100%, + rgb(24 63 87 / 55%) 0%, + rgb(29 61 12 / 4%) 70%, + rgb(94 116 93 / 0%) 80% + ), + radial-gradient( + circle at 100% 0%, + rgba(54, 66, 84, 0.55) 0%, + rgb(102 111 125 / 4%) 70%, + rgba(54, 66, 84, 0) 80% + ), + radial-gradient( + circle at 0% 0%, + rgba(91, 114, 135, 0.55) 0%, + rgb(45 111 171 / 4%) 70%, + rgb(5 82 153 / 0%) 80% + ), + rgb(0, 0, 0) center center/cover no-repeat fixed; + width: 9rem; + aspect-ratio: 16 / 9; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; } .home .demo .vid-text { - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-start; - width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + width: 100%; } .home .demo h2 { - font-size: 18px; - line-height: 1.375; - margin: 0; - text-align: left; - font-weight: 700; + font-size: 18px; + line-height: 1.375; + margin: 0; + text-align: left; + font-weight: 700; } .home .demo h3 { - font-size: 16px; - line-height: 1.25; - margin: 0; + font-size: 16px; + line-height: 1.25; + margin: 0; } .home .demo p { - font-size: 14px; - line-height: 1.375; - margin: 0; + font-size: 14px; + line-height: 1.375; + margin: 0; } .home .demo .browser-nav-url { - background: rgba(153, 161, 179, 0.2); - border-radius: 9999px; - font-size: 14px; - color: grey; - display: flex; - align-items: center; - justify-content: center; - -moz-column-gap: 5px; - column-gap: 5px; + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + font-size: 14px; + color: grey; + display: flex; + align-items: center; + justify-content: center; + -moz-column-gap: 5px; + column-gap: 5px; } .home .demo .browser-navbar { - margin: -1rem; - margin-bottom: 0; - padding: 0.75rem 1rem; - border-bottom: 1px solid darkgrey; + margin: -1rem; + margin-bottom: 0; + padding: 0.75rem 1rem; + border-bottom: 1px solid darkgrey; } .home .demo .browser-viewport { - background: #fff; - border-radius: 16px; - display: flex; - flex-direction: column; - row-gap: 1rem; - height: 400px; - overflow-y: scroll; - margin: -1rem; - padding: 1rem; + background: #fff; + border-radius: 16px; + display: flex; + flex-direction: column; + row-gap: 1rem; + height: 400px; + overflow-y: scroll; + margin: -1rem; + padding: 1rem; } .home .demo .browser-viewport .search-header > h1 { - color: #000; - text-align: left; - font-size: 24px; - margin: 0; + color: #000; + text-align: left; + font-size: 24px; + margin: 0; } .home .demo .browser-viewport .search-header > p { - text-align: left; - font-size: 16px; - margin: 10px 0; + text-align: left; + font-size: 16px; + margin: 10px 0; } .home .demo .search-bar input { - width: 100%; - background: rgba(153, 161, 179, 0.2); - border-radius: 9999px; - padding-left: 40px; - padding-right: 40px; - height: 40px; - color: #000; + width: 100%; + background: rgba(153, 161, 179, 0.2); + border-radius: 9999px; + padding-left: 40px; + padding-right: 40px; + height: 40px; + color: #000; } .home .demo .search-bar svg { - height: 40px; - position: absolute; - transform: translateX(75%); + height: 40px; + position: absolute; + transform: translateX(75%); } .home .demo .search-bar { - position: relative; + position: relative; } /* Desktop Styling */ @media screen and (min-width: 60em) { - .home .row { - text-align: center; - } - .home .row > p { - font-size: 21px; - } - .home .row > h1 { - font-size: 52px; - } - .home .row .pop-left { - margin-left: -20px; - margin-right: 0; - margin-top: -20px; - margin-bottom: -20px; - } - .home .row .pop-right { - margin-left: 0px; - margin-right: 0px; - margin-top: -20px; - margin-bottom: -20px; - } + .home .row { + text-align: center; + } + .home .row > p { + font-size: 21px; + } + .home .row > h1 { + font-size: 52px; + } + .home .row .pop-left { + margin-left: -20px; + margin-right: 0; + margin-top: -20px; + margin-bottom: -20px; + } + .home .row .pop-right { + margin-left: 0px; + margin-right: 0px; + margin-top: -20px; + margin-bottom: -20px; + } } /* Mobile Styling */ @media screen and (max-width: 60em) { - .home .row { - padding: 4rem 0.8rem; - } - .home .row > h1, - .home .row > p { - padding-left: 1rem; - padding-right: 1rem; - } - .home .row.first { - padding-top: 2rem; - } - .home-btns { - width: 100%; - display: grid; - grid-gap: 0.5rem; - gap: 0.5rem; - } - .home .example-container { - display: flex; - flex-direction: column; - row-gap: 20px; - width: 100%; - justify-content: center; - border-radius: 0; - padding: 1rem 0; - } - .home .row { - padding-left: 0; - padding-right: 0; - } - .home .tabbed-set { - width: 100%; - border-radius: 0; - } - .home .demo { - width: 100%; - display: flex; - justify-content: center; - } - .home .demo > .white-bg { - width: 80%; - max-width: 80%; - } + .home .row { + padding: 4rem 0.8rem; + } + .home .row > h1, + .home .row > p { + padding-left: 1rem; + padding-right: 1rem; + } + .home .row.first { + padding-top: 2rem; + } + .home-btns { + width: 100%; + display: grid; + grid-gap: 0.5rem; + gap: 0.5rem; + } + .home .example-container { + display: flex; + flex-direction: column; + row-gap: 20px; + width: 100%; + justify-content: center; + border-radius: 0; + padding: 1rem 0; + } + .home .row { + padding-left: 0; + padding-right: 0; + } + .home .tabbed-set { + width: 100%; + border-radius: 0; + } + .home .demo { + width: 100%; + display: flex; + justify-content: center; + } + .home .demo > .white-bg { + width: 80%; + max-width: 80%; + } } diff --git a/docs/src/assets/img/add-interactivity.png b/docs/src/assets/img/add-interactivity.png index e5e24d2925db020621ed3a157b3b34bd516353c1..009b52ac123e2854259c3b2b3e478a721ed6b110 100644 GIT binary patch literal 20821 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 diff --git a/docs/src/assets/img/create-user-interfaces.png b/docs/src/assets/img/create-user-interfaces.png index 13abd064d9ba90d6640f3769bba0eade6e366868..06f6ea0cb6fdd4e17a6af5e374ced578cc0728f3 100644 GIT binary patch delta 11314 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 Cg~xyZ delta 11435 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�wQwLK+5Z%JTb!YxBy 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 ZTP@T1>bv9!&_5i4q(3Q$SBM$}{C`(dFKhq+ diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index aaeabba7..1b7ae9b2 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -14,6 +14,8 @@ This allows you to embedded any number of client-side PyScript components within {% include-markdown "../reference/template-tag.md" start="" end="" %} +{% include-markdown "../reference/template-tag.md" start="" end="" %} + === "components.py" ```python diff --git a/docs/src/reference/decorators.md b/docs/src/reference/decorators.md index 6a0345da..962ec1e6 100644 --- a/docs/src/reference/decorators.md +++ b/docs/src/reference/decorators.md @@ -12,7 +12,7 @@ Decorator functions can be used within your `components.py` to help simplify dev You can limit component access to users that pass a test function by using this decorator. -This decorator is inspired by Django's [`user_passes_test`](http://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) decorator, but works with ReactPy components. +This decorator is inspired by Django's [`user_passes_test`](http://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test) decorator, but this one works with ReactPy components. === "components.py" diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 6d7da90f..43c4641b 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -400,7 +400,7 @@ This is often used to create chat systems, synchronize data between components, In these cases, you can use the `#!python use_channel_layer` hook to receive a signal within your component, and then use the `#!python get_channel_layer().send(...)` to send the signal. - In the example below, the sender will send a signal every time `#!python ExampleModel` is saved. Then, when the receiver component gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render. + In the example below, the sender will signal every time `#!python ExampleModel` is saved. Then, when the receiver gets this signal, it explicitly calls `#!python set_message(...)` to trigger a re-render. === "signals.py" @@ -522,7 +522,7 @@ You can expect this hook to provide strings such as `http://example.com`. Shortcut that returns the root component's `#!python id` from the WebSocket or HTTP connection. -The root ID is currently a randomly generated `#!python uuid4` (unique across all root component). +The root ID is a randomly generated `#!python uuid4`. It is notable to mention that it is persistent across the current connection. The `uuid` is reset when the page is refreshed. This is useful when used in combination with [`#!python use_channel_layer`](#use-channel-layer) to send messages to a specific component instance, and/or retain a backlog of messages in case that component is disconnected via `#!python use_channel_layer( ... , group_discard=False)`. diff --git a/docs/src/reference/html.md b/docs/src/reference/html.md index fd63c033..3fe3fe46 100644 --- a/docs/src/reference/html.md +++ b/docs/src/reference/html.md @@ -10,11 +10,11 @@ We supply some pre-generated that HTML nodes can be used to help simplify develo ## PyScript -Primitive HTML tag that is leveraged by [`reactpy_django.components.pyscript_component`](./components.md#pyscript-component). +PyScript code block. The text content of this tag are executed within the PyScript interpreter. This can be used as an alternative to the `#!python reactpy.html.script`. -This can be used as an alternative to the `#!python reactpy.html.script` tag to execute JavaScript and run client-side Python code. +This is a primitive HTML tag that is leveraged by [`reactpy_django.components.pyscript_component`](./components.md#pyscript-component). -Additionally, this tag functions identically to any other tag contained within `#!python reactpy.html`, and can be used in the same way. +The `pyscript` tag functions identically to HTML tags contained within `#!python reactpy.html`. === "components.py" diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 434c81d0..197a3b29 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -10,7 +10,7 @@ Django template tags can be used within your HTML templates to provide ReactPy f ## Component -This template tag can be used to insert any number of ReactPy components onto your page. +This template tag can be used to insert any number of **server-side** ReactPy components onto your page. Each component loaded via this template tag will receive a dedicated WebSocket connection to the server. @@ -159,9 +159,10 @@ This template tag can be used to insert any number of **client-side** ReactPy co By default, the only dependencies available are the Python standard library, `pyscript`, `pyodide`, `reactpy` core. -Your PyScript component file requires a `#!python def root()` component to function as the entry point. +The entire file path provided is loaded directly into the browser, and must have a `#!python def root()` component to act as the entry point. + !!! warning "Pitfall" @@ -169,6 +170,8 @@ Your PyScript component file requires a `#!python def root()` component to funct As a result of running client-side, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components. + + === "my_template.html" ```jinja diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index 6590012c..5402cc85 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -14,7 +14,7 @@ Utility functions provide various miscellaneous functionality for advanced use c ## Register Iframe -This function is used register a view as an `#!python iframe` with ReactPy. +This function is used register a Django view as a ReactPy `#!python iframe`. It is mandatory to use this function alongside [`view_to_iframe`](../reference/components.md#view-to-iframe). diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index 48559e84..401cf724 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -334,7 +334,7 @@ def django_query_postprocessor( "One of the following may have occurred:\n" " - You are using a non-Django ORM.\n" " - You are attempting to use `use_query` to fetch non-ORM data.\n\n" - "If these situations seem correct, you may want to consider disabling the postprocessor." + "If these situations apply, you may want to disable the postprocessor." ) return data From 741de489419af95cba51228fc63db41bb7b3de0a Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 21 Oct 2024 17:52:41 -0700 Subject: [PATCH 10/35] Support ReactPy Router v1.0.0 (#252) - Adds support for ReactPy Router v1.0.0 - Docs cleanup - Tests cleanup - Remove deprecated code --- CHANGELOG.md | 20 ++- README.md | 2 +- ...module.html => pyscript-local-import.html} | 0 ...t-js-execution.py => pyodide-js-module.py} | 0 ...-js-module.py => pyscript-local-import.py} | 0 docs/mkdocs.yml | 2 - docs/src/dictionary.txt | 64 ++++----- .../learn/add-reactpy-to-a-django-project.md | 16 +-- docs/src/learn/your-first-component.md | 22 +-- docs/src/reference/components.md | 12 +- docs/src/reference/decorators.md | 2 +- docs/src/reference/hooks.md | 8 +- docs/src/reference/html.md | 2 +- docs/src/reference/management-commands.md | 2 +- docs/src/reference/router.md | 11 +- docs/src/reference/settings.md | 7 +- docs/src/reference/template-tag.md | 32 +++-- docs/src/reference/utils.md | 8 +- requirements/build-docs.txt | 1 - requirements/pkg-deps.txt | 3 +- setup.cfg | 2 - src/reactpy_django/__init__.py | 8 +- src/reactpy_django/checks.py | 68 +++++---- src/reactpy_django/components.py | 90 +++--------- src/reactpy_django/config.py | 10 +- src/reactpy_django/database.py | 2 +- src/reactpy_django/decorators.py | 40 +----- src/reactpy_django/http/views.py | 6 +- src/reactpy_django/router/__init__.py | 2 +- src/reactpy_django/router/converters.py | 3 +- src/reactpy_django/router/resolvers.py | 64 ++------- src/reactpy_django/websocket/paths.py | 6 +- tests/test_app/apps.py | 12 +- tests/test_app/components.py | 104 ++++---------- tests/test_app/router/components.py | 47 +++---- tests/test_app/router/urls.py | 2 +- tests/test_app/templates/base.html | 20 +-- ...ple-button.js => button-from-js-module.js} | 0 tests/test_app/tests/test_components.py | 129 ++++++++---------- tests/test_app/views.py | 10 +- 40 files changed, 332 insertions(+), 507 deletions(-) rename docs/examples/html/{pyscript-js-module.html => pyscript-local-import.html} (100%) rename docs/examples/python/{pyscript-js-execution.py => pyodide-js-module.py} (100%) rename docs/examples/python/{pyscript-js-module.py => pyscript-local-import.py} (100%) delete mode 100644 setup.cfg rename tests/test_app/tests/js/{simple-button.js => button-from-js-module.js} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9652b644..6680348a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,9 +36,21 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] -- Nothing (yet)! +### Changed + +- Now using ReactPy-Router v1 for URL routing, which comes with a slightly different API than before. +- Removed dependency on `aiofile`. + +### Removed + +- Removed the following **deprecated** features: + - The `compatibility` argument on `reactpy_django.components.view_to_component` + - `reactpy_django.components.view_to_component` **usage as a decorator** + - `reactpy_django.decorators.auth_required` + - `reactpy_django.REACTPY_WEBSOCKET_PATH` + - `settings.py:REACTPY_WEBSOCKET_URL` -## [4.0.0] +## [4.0.0] - 2024-06-22 ### Added @@ -112,8 +124,8 @@ Don't forget to remove deprecated code on each major release! - New Django `User` related features! - `reactpy_django.hooks.use_user` can be used to access the current user. - `reactpy_django.hooks.use_user_data` provides a simplified interface for storing user key-value data. - - `reactpy_django.decorators.user_passes_test` is inspired by the [equivalent Django decorator](http://docs.djangoproject.com/en/dev/topics/auth/default/#django.contrib.auth.decorators.user_passes_test), but ours works with ReactPy components. - - `settings.py:REACTPY_AUTO_RELOGIN` will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. This is useful to continuously update `last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/dev/topics/http/sessions/). + - `reactpy_django.decorators.user_passes_test` is inspired by the [equivalent Django decorator](http://docs.djangoproject.com/en/stable/topics/auth/default/#django.contrib.auth.decorators.user_passes_test), but ours works with ReactPy components. + - `settings.py:REACTPY_AUTO_RELOGIN` will cause component WebSocket connections to automatically [re-login](https://channels.readthedocs.io/en/latest/topics/authentication.html#how-to-log-a-user-in-out) users that are already authenticated. This is useful to continuously update `last_login` timestamps and refresh the [Django login session](https://docs.djangoproject.com/en/stable/topics/http/sessions/). ### Changed diff --git a/README.md b/README.md index 817e684b..d3d2a1a9 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ def hello_world(recipient: str): -## [`my_app/templates/my_template.html`](https://docs.djangoproject.com/en/dev/topics/templates/) +## [`my_app/templates/my_template.html`](https://docs.djangoproject.com/en/stable/topics/templates/) diff --git a/docs/examples/html/pyscript-js-module.html b/docs/examples/html/pyscript-local-import.html similarity index 100% rename from docs/examples/html/pyscript-js-module.html rename to docs/examples/html/pyscript-local-import.html diff --git a/docs/examples/python/pyscript-js-execution.py b/docs/examples/python/pyodide-js-module.py similarity index 100% rename from docs/examples/python/pyscript-js-execution.py rename to docs/examples/python/pyodide-js-module.py diff --git a/docs/examples/python/pyscript-js-module.py b/docs/examples/python/pyscript-local-import.py similarity index 100% rename from docs/examples/python/pyscript-js-module.py rename to docs/examples/python/pyscript-local-import.py diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e4159640..c1b5922f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -89,8 +89,6 @@ plugins: - spellcheck: known_words: dictionary.txt allow_unicode: no - ignore_code: yes - # - section-index extra: generator: false diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 66265e78..14aa7a61 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -1,43 +1,45 @@ +asgi +async +backend +backends +backhaul +broadcasted +changelog django -sanic -plotly +frontend +frontends +hello_world +html +iframe +jupyter +keyworded +middleware +misconfiguration +misconfigurations +my_template nox -WebSocket -WebSockets -changelog -async +plotly +postfixed +postprocessing +postprocessor pre prefetch prefetching preloader -whitespace +preprocessor +py +pyodide +pyscript +reactpy refetch refetched refetching -html -jupyter -iframe -keyworded +sanic +serializable stylesheet stylesheets -unstyled -py -reactpy -asgi -postfixed -postprocessing -serializable -postprocessor -preprocessor -middleware -backends -backend -frontend -frontends -misconfiguration -misconfigurations -backhaul sublicense -broadcasted -hello_world -my_template +unstyled +WebSocket +WebSockets +whitespace diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md index dd258737..0bf919e2 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -8,7 +8,7 @@ If you want to add some interactivity to your existing **Django project**, you d !!! abstract "Note" - These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/dev/intro/tutorial01/), which involves creating and installing at least one **Django app**. + These docs assumes you have already created [a **Django project**](https://docs.djangoproject.com/en/stable/intro/tutorial01/), which involves creating and installing at least one **Django app**. If do not have a **Django project**, check out this [9 minute YouTube tutorial](https://www.youtube.com/watch?v=ZsJRXS_vrw0) created by _IDG TECHtalk_. @@ -24,7 +24,7 @@ pip install reactpy-django ## Step 2: Configure `settings.py` -Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/dev/topics/settings/) file. +Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-INSTALLED_APPS) in your [`settings.py`](https://docs.djangoproject.com/en/stable/topics/settings/) file. === "settings.py" @@ -36,7 +36,7 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject ReactPy-Django requires Django ASGI and [Django Channels](https://github.com/django/channels) WebSockets. - If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: + If you have not enabled ASGI on your **Django project** yet, here is a summary of the [`django`](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/) and [`channels`](https://channels.readthedocs.io/en/stable/installation.html) installation docs: 1. Install `channels[daphne]` 2. Add `#!python "daphne"` to `#!python INSTALLED_APPS`. @@ -59,7 +59,7 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject ## Step 3: Configure `urls.py` -Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/) file. +Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https://docs.djangoproject.com/en/stable/topics/http/urls/) file. === "urls.py" @@ -69,7 +69,7 @@ Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https: ## Step 4: Configure `asgi.py` -Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/) file. +Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [`asgi.py`](https://docs.djangoproject.com/en/stable/howto/deployment/asgi/) file. === "asgi.py" @@ -97,7 +97,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [` ## Step 5: Run database migrations -Run Django's [`migrate` command](https://docs.djangoproject.com/en/dev/topics/migrations/) to initialize ReactPy-Django's database table. +Run Django's [`migrate` command](https://docs.djangoproject.com/en/stable/topics/migrations/) to initialize ReactPy-Django's database table. ```bash linenums="0" python manage.py migrate @@ -105,7 +105,7 @@ python manage.py migrate ## Step 6: Check your configuration -Run Django's [`check` command](https://docs.djangoproject.com/en/dev/ref/django-admin/#check) to verify if ReactPy was set up correctly. +Run Django's [`check` command](https://docs.djangoproject.com/en/stable/ref/django-admin/#check) to verify if ReactPy was set up correctly. ```bash linenums="0" python manage.py check @@ -113,7 +113,7 @@ python manage.py check ## Step 7: Create your first component -The [next step](./your-first-component.md) will show you how to create your first ReactPy component. +The [next page](./your-first-component.md) will show you how to create your first ReactPy component. Prefer a quick summary? Read the **At a Glance** section below. diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md index 08df6a57..85af4109 100644 --- a/docs/src/learn/your-first-component.md +++ b/docs/src/learn/your-first-component.md @@ -18,7 +18,7 @@ You will now need to pick at least one **Django app** to start using ReactPy-Dja For the following examples, we will assume the following: -1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/dev/intro/tutorial01/#creating-the-polls-app). +1. You have a **Django app** named `my_app`, which was created by Django's [`startapp` command](https://docs.djangoproject.com/en/stable/intro/tutorial01/#creating-the-polls-app). 2. You have placed `my_app` directly into your **Django project** folder (`./example_project/my_app`). This is common for small projects. ??? question "How do I organize my Django project for ReactPy?" @@ -31,7 +31,7 @@ You will need a file to start creating ReactPy components. We recommend creating a `components.py` file within your chosen **Django app** to start out. For this example, the file path will look like this: `./example_project/my_app/components.py`. -Within this file, you can define your component functions using ReactPy's `#!python @component` decorator. +Within this file, you will define your component function(s) using the `#!python @component` decorator. === "components.py" @@ -43,7 +43,7 @@ Within this file, you can define your component functions using ReactPy's `#!pyt We recommend creating a `components.py` for small **Django apps**. If your app has a lot of components, you should consider breaking them apart into individual modules such as `components/navbar.py`. - Ultimately, components are referenced by Python dotted path in `my_template.html` ([_see next step_](#embedding-in-a-template)). This path must be valid to Python's `#!python importlib`. + Ultimately, components are referenced by Python dotted path in `my_template.html` ([_see next step_](#embedding-in-a-template)). This dotted path must be valid to Python's `#!python importlib`. ??? question "What does the decorator actually do?" @@ -66,17 +66,23 @@ Additionally, you can pass in `#!python args` and `#!python kwargs` into your co {% include-markdown "../../../README.md" start="" end="" %} +???+ tip "Components are automatically registered!" + + ReactPy-Django will automatically register any component that is referenced in a Django HTML template. This means you [typically](../reference/utils.md#register-component) do not need to manually register components in your **Django app**. + + Please note that this HTML template must be properly stored within a registered Django app. ReactPy-Django will output a console log message containing all detected components when the server starts up. + {% include-markdown "../reference/template-tag.md" start="" end="" %} {% include-markdown "../reference/template-tag.md" start="" end="" %} ??? question "Where is my templates folder?" - If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/dev/ref/applications/#configuring-applications). + If you do not have a `./templates/` folder in your **Django app**, you can simply create one! Keep in mind, templates within this folder will not be detected by Django unless you [add the corresponding **Django app** to `settings.py:INSTALLED_APPS`](https://docs.djangoproject.com/en/stable/ref/applications/#configuring-applications). ## Setting up a Django view -Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) to render the HTML template `my_template.html` ([_from the previous step_](#embedding-in-a-template)). +Within your **Django app**'s `views.py` file, you will need to [create a view function](https://docs.djangoproject.com/en/stable/intro/tutorial01/#write-your-first-view) to render the HTML template `my_template.html` ([_from the previous step_](#embedding-in-a-template)). === "views.py" @@ -84,7 +90,7 @@ Within your **Django app**'s `views.py` file, you will need to [create a view fu {% include "../../examples/python/example/views.py" %} ``` -We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/dev/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. +We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/stable/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. === "urls.py" @@ -98,7 +104,7 @@ We will add this new view into your [`urls.py`](https://docs.djangoproject.com/e Once you reach that point, we recommend creating an individual `urls.py` within each of your **Django apps**. - Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/dev/ref/urls/#include) to link it all together. + Then, within your **Django project's** `urls.py` you will use Django's [`include` function](https://docs.djangoproject.com/en/stable/ref/urls/#include) to link it all together. ## Viewing your component @@ -114,7 +120,7 @@ If you copy-pasted our example component, you will now see your component displa ??? warning "Do not use `manage.py runserver` for production" - This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/dev/howto/deployment/). + This command is only intended for development purposes. For production deployments make sure to read [Django's documentation](https://docs.djangoproject.com/en/stable/howto/deployment/). ## Learn more diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index 1b7ae9b2..943b76c0 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -124,7 +124,7 @@ Automatically convert a Django view into a component. At this time, this works best with static views with no interactivity. -Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). +Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/stable/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/stable/topics/class-based-views/). === "components.py" @@ -254,7 +254,7 @@ Automatically convert a Django view into an [`iframe` element](https://www.techt The contents of this `#!python iframe` is handled entirely by traditional Django view rendering. While this solution is compatible with more views than `#!python view_to_component`, it comes with different limitations. -Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/dev/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/dev/topics/class-based-views/). +Compatible with sync or async [Function Based Views](https://docs.djangoproject.com/en/stable/topics/http/views/) and [Class Based Views](https://docs.djangoproject.com/en/stable/topics/class-based-views/). === "components.py" @@ -383,7 +383,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ## Django CSS -Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). +Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/stable/howto/static-files/). === "components.py" @@ -436,11 +436,11 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. ## Django JS -Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/dev/howto/static-files/). +Allows you to defer loading JavaScript until a component begins rendering. This JavaScript must be stored within [Django's static files](https://docs.djangoproject.com/en/stable/howto/static-files/). - - You can use the `#!python prerender` argument in your [template tag](../reference/template-tag.md#component) to manually override this default. --- diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 197a3b29..e5c60a79 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -78,7 +78,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c

{% component "example_project.my_app.components.my_title" %}

{% component "example_project.my_app_2.components.goodbye_world" class="bold small-font" %}

- {% component "example_project.my_app_3.components.simple_button" %} + {% component "example_project.my_app_3.components.my_button" %} ``` @@ -157,7 +157,7 @@ This template tag can be used to insert any number of **client-side** ReactPy co -By default, the only dependencies available are the Python standard library, `pyscript`, `pyodide`, `reactpy` core. +By default, the only [available dependencies](./template-tag.md#pyscript-setup) are the Python standard library, `pyscript`, `pyodide`, `reactpy` core. The entire file path provided is loaded directly into the browser, and must have a `#!python def root()` component to act as the entry point. @@ -166,9 +166,9 @@ The entire file path provided is loaded directly into the browser, and must have !!! warning "Pitfall" - Your provided Python file is loaded directly into the client (web browser) **as raw text**, and ran using a PyScript interpreter. Be cautious about what you include in your Python file. + Similar to JavaScript, your provided Python file is loaded directly into the client (web browser) **as raw text** to run using the PyScript interpreter. Be cautious about what you include in your Python file. - As a result of running client-side, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components. + As a result being client-sided, Python packages within your local environment (such as those installed via `pip install ...`) are **not accessible** within PyScript components. @@ -198,28 +198,40 @@ The entire file path provided is loaded directly into the browser, and must have ??? question "How do I execute JavaScript within PyScript components?" - PyScript components have the ability to directly execute standard library JavaScript using the [`pyodide` `js` module](https://pyodide.org/en/stable/usage/type-conversions.html#importing-javascript-objects-into-python) or [`pyscript` foreign function interface](https://docs.pyscript.net/2024.6.1/user-guide/dom/). + PyScript components several options available to execute JavaScript, including... - The `#!python js` module has access to everything within the browser's JavaScript environment. Therefore, any global JavaScript functions loaded within your HTML `#!html ` can be called as well. However, be mindful of JavaScript load order! + - [Pyodide's `js` module](https://pyodide.org/en/stable/usage/type-conversions.html#importing-javascript-objects-into-python) + - [Pyscript's foreign function interface](https://docs.pyscript.net/latest/user-guide/dom/#ffi) + - [Pyscript's JavaScript modules](https://docs.pyscript.net/latest/user-guide/configuration/#javascript-modules). + + **Pyodide JS Module** + + The Pyodide `#!python js` module has access to everything within the browser's JavaScript environment. Therefore, any global JavaScript functions loaded within your HTML `#!html ` can be called as well. However, you will need to be mindful of JavaScript load order if using [`async` or `deferred`](https://javascript.info/script-async-defer) loading! === "root.py" ```python - {% include "../../examples/python/pyscript-js-execution.py" %} + {% include "../../examples/python/pyodide-js-module.py" %} ``` - To import JavaScript modules in a fashion similar to `#!javascript import {moment} from 'static/moment.js'`, you will need to configure your `#!jinja {% pyscript_setup %}` block to make the module available to PyScript. This module will be accessed within `#!python pyscript.js_modules.*`. For more information, see the [PyScript JS modules docs](https://docs.pyscript.net/2024.6.2/user-guide/configuration/#javascript-modules). + **PyScript FFI** + + ... + + **PyScript JS Modules** + + Assuming you have a local bundle stored within your project's static files, you can import JavaScript modules in a fashion similar to `#!javascript import {moment} from 'static/moment.js'`. You will first need to configure your `#!jinja {% pyscript_setup %}` block to make the `moment.js` module available to PyScript. Then, this module can be accessed within `#!python pyscript.js_modules.*`. === "root.py" ```python - {% include "../../examples/python/pyscript-js-module.py" %} + {% include "../../examples/python/pyscript-local-import.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-js-module.html" %} + {% include "../../examples/html/pyscript-local-import.html" %} ``` diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index 5402cc85..917ba959 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -36,9 +36,9 @@ It is mandatory to use this function alongside [`view_to_iframe`](../reference/c `#!python None` -??? warning "Only use this within `#!python MyAppConfig.ready()`" +??? warning "Only use this within `#!python AppConfig.ready()`" - You should always call `#!python register_iframe` within a Django [`MyAppConfig.ready()` method](https://docs.djangoproject.com/en/dev/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. + You should always call `#!python register_iframe` within a Django [`AppConfig.ready()` method](https://docs.djangoproject.com/en/stable/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. --- @@ -68,7 +68,7 @@ Typically, this function is automatically called on all components contained wit ??? warning "Only use this within `#!python MyAppConfig.ready()`" - You should always call `#!python register_component` within a Django [`MyAppConfig.ready()` method](https://docs.djangoproject.com/en/dev/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. + You should always call `#!python register_component` within a Django [`MyAppConfig.ready()` method](https://docs.djangoproject.com/en/stable/ref/applications/#django.apps.AppConfig.ready). This ensures you will retain multiprocessing compatibility, such as with ASGI web server workers. ??? question "Do I need to use this?" @@ -84,7 +84,7 @@ Typically, this function is automatically called on all components contained wit This is the default postprocessor for the `#!python use_query` hook. -Since ReactPy is rendered within an `#!python asyncio` loop, this postprocessor is exists to prevent Django's `#!python SynchronousOnlyException` by recursively prefetching fields within a `#!python Model` or `#!python QuerySet`. This prefetching step works to eliminate Django's [lazy execution](https://docs.djangoproject.com/en/dev/topics/db/queries/#querysets-are-lazy) behavior. +Since ReactPy is rendered within an `#!python asyncio` loop, this postprocessor is exists to prevent Django's `#!python SynchronousOnlyException` by recursively prefetching fields within Django's ORM. Note that this effectively eliminates Django's [lazy execution](https://docs.djangoproject.com/en/stable/topics/db/queries/#querysets-are-lazy) behavior. === "components.py" diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt index a7d5b1cf..846a7ba3 100644 --- a/requirements/build-docs.txt +++ b/requirements/build-docs.txt @@ -5,5 +5,4 @@ mkdocs-include-markdown-plugin mkdocs-spellcheck[all] mkdocs-git-authors-plugin mkdocs-minify-plugin -mkdocs-section-index mike diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt index c6102c18..cec6a9e1 100644 --- a/requirements/pkg-deps.txt +++ b/requirements/pkg-deps.txt @@ -1,8 +1,7 @@ channels >=4.0.0 django >=4.2.0 reactpy >=1.0.2, <1.1.0 -reactpy-router >=0.1.1, <1.0.0 -aiofile >=3.0 +reactpy-router >=1.0.0, <2.0.0 dill >=0.3.5 orjson >=3.6.0 nest_asyncio >=1.5.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index d4ece375..77b56743 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -3,7 +3,6 @@ import nest_asyncio from reactpy_django import ( - checks, components, decorators, hooks, @@ -12,14 +11,10 @@ types, utils, ) -from reactpy_django.websocket.paths import ( - REACTPY_WEBSOCKET_PATH, - REACTPY_WEBSOCKET_ROUTE, -) +from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_ROUTE __version__ = "4.0.0" __all__ = [ - "REACTPY_WEBSOCKET_PATH", "REACTPY_WEBSOCKET_ROUTE", "html", "hooks", @@ -27,7 +22,6 @@ "decorators", "types", "utils", - "checks", "router", ] diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index d836a9ca..740df974 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -19,7 +19,7 @@ def reactpy_warnings(app_configs, **kwargs): warnings = [] INSTALLED_APPS: list[str] = getattr(settings, "INSTALLED_APPS", []) - # REACTPY_DATABASE is not an in-memory database. + # Check if REACTPY_DATABASE is not an in-memory database. if ( getattr(settings, "DATABASES", {}) .get(getattr(settings, "REACTPY_DATABASE", "default"), {}) @@ -36,7 +36,7 @@ def reactpy_warnings(app_configs, **kwargs): ) ) - # ReactPy URLs exist + # Check if ReactPy URLs are reachable try: reverse("reactpy:web_modules", kwargs={"file": "example"}) reverse("reactpy:view_to_iframe", kwargs={"dotted_path": "example"}) @@ -102,16 +102,7 @@ def reactpy_warnings(app_configs, **kwargs): # DELETED W007: Check if REACTPY_WEBSOCKET_URL doesn't end with a slash # DELETED W008: Check if REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character - - # Removed REACTPY_WEBSOCKET_URL setting - if getattr(settings, "REACTPY_WEBSOCKET_URL", None): - warnings.append( - Warning( - "REACTPY_WEBSOCKET_URL has been removed.", - hint="Use REACTPY_URL_PREFIX instead.", - id="reactpy_django.W009", - ) - ) + # DELETED W009: Check if deprecated value REACTPY_WEBSOCKET_URL exists # Check if REACTPY_URL_PREFIX is being used properly in our HTTP URLs with contextlib.suppress(NoReverseMatch): @@ -152,22 +143,16 @@ def reactpy_warnings(app_configs, **kwargs): ): warnings.append( Warning( - "You have not configured runserver to use ASGI.", + "You have not configured the `runserver` command to use ASGI. " + "ReactPy will work properly in this configuration.", hint="Add daphne to settings.py:INSTALLED_APPS.", id="reactpy_django.W012", ) ) - # Removed REACTPY_RECONNECT_MAX setting - if getattr(settings, "REACTPY_RECONNECT_MAX", None): - warnings.append( - Warning( - "REACTPY_RECONNECT_MAX has been removed.", - hint="See the docs for the new REACTPY_RECONNECT_* settings.", - id="reactpy_django.W013", - ) - ) + # DELETED W013: Check if deprecated value REACTPY_RECONNECT_MAX exists + # Check if REACTPY_RECONNECT_INTERVAL is set to a large value if ( isinstance(config.REACTPY_RECONNECT_INTERVAL, int) and config.REACTPY_RECONNECT_INTERVAL > 30000 @@ -181,20 +166,22 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_RETRIES is set to a large value if ( isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) and config.REACTPY_RECONNECT_MAX_RETRIES > 5000 ): warnings.append( Warning( - "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value. Are you sure this is intentional? " + "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value " + f"{config.REACTPY_RECONNECT_MAX_RETRIES}. Are you sure this is intentional? " "This may leave your clients attempting reconnections for a long time.", hint="Check your value for REACTPY_RECONNECT_MAX_RETRIES or suppress this warning.", id="reactpy_django.W015", ) ) - # Check if the value is too large (greater than 50) + # Check if the REACTPY_RECONNECT_BACKOFF_MULTIPLIER is set to a large value if ( isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)) and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 100 @@ -207,6 +194,7 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is reachable if ( isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and isinstance(config.REACTPY_RECONNECT_INTERVAL, int) @@ -239,6 +227,7 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if 'reactpy_django' is in the correct position in INSTALLED_APPS position_to_beat = 0 for app in INSTALLED_APPS: if app.startswith("django.contrib."): @@ -255,6 +244,7 @@ def reactpy_warnings(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_SESSION is not a valid property if getattr(settings, "REACTPY_CLEAN_SESSION", None): warnings.append( Warning( @@ -301,7 +291,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) - # All settings in reactpy_django.conf are the correct data type + # Check if REACTPY_URL_PREFIX is a valid data type if not isinstance(getattr(settings, "REACTPY_URL_PREFIX", ""), str): errors.append( Error( @@ -311,6 +301,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E003", ) ) + + # Check if REACTPY_SESSION_MAX_AGE is a valid data type if not isinstance(getattr(settings, "REACTPY_SESSION_MAX_AGE", 0), int): errors.append( Error( @@ -320,6 +312,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E004", ) ) + + # Check if REACTPY_CACHE is a valid data type if not isinstance(getattr(settings, "REACTPY_CACHE", ""), str): errors.append( Error( @@ -329,6 +323,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E005", ) ) + + # Check if REACTPY_DATABASE is a valid data type if not isinstance(getattr(settings, "REACTPY_DATABASE", ""), str): errors.append( Error( @@ -338,6 +334,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E006", ) ) + + # Check if REACTPY_DEFAULT_QUERY_POSTPROCESSOR is a valid data type if not isinstance( getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None)) ): @@ -349,6 +347,8 @@ def reactpy_errors(app_configs, **kwargs): id="reactpy_django.E007", ) ) + + # Check if REACTPY_AUTH_BACKEND is a valid data type if not isinstance(getattr(settings, "REACTPY_AUTH_BACKEND", ""), str): errors.append( Error( @@ -361,6 +361,7 @@ def reactpy_errors(app_configs, **kwargs): # DELETED E009: Check if `channels` is in INSTALLED_APPS + # Check if REACTPY_DEFAULT_HOSTS is a valid data type if not isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", []), list): errors.append( Error( @@ -371,7 +372,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) - # Check of all values in the list are strings + # Check of all values in the REACTPY_DEFAULT_HOSTS are strings if isinstance(getattr(settings, "REACTPY_DEFAULT_HOSTS", None), list): for host in settings.REACTPY_DEFAULT_HOSTS: if not isinstance(host, str): @@ -385,6 +386,7 @@ def reactpy_errors(app_configs, **kwargs): ) break + # Check if REACTPY_RECONNECT_INTERVAL is a valid data type if not isinstance(config.REACTPY_RECONNECT_INTERVAL, int): errors.append( Error( @@ -394,6 +396,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_INTERVAL is a positive integer if ( isinstance(config.REACTPY_RECONNECT_INTERVAL, int) and config.REACTPY_RECONNECT_INTERVAL < 0 @@ -406,6 +409,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is a valid data type if not isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int): errors.append( Error( @@ -415,6 +419,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is a positive integer if ( isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and config.REACTPY_RECONNECT_MAX_INTERVAL < 0 @@ -427,6 +432,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_INTERVAL is greater than REACTPY_RECONNECT_INTERVAL if ( isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and isinstance(config.REACTPY_RECONNECT_INTERVAL, int) @@ -440,6 +446,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_RETRIES is a valid data type if not isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int): errors.append( Error( @@ -449,6 +456,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_MAX_RETRIES is a positive integer if ( isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) and config.REACTPY_RECONNECT_MAX_RETRIES < 0 @@ -461,6 +469,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_BACKOFF_MULTIPLIER is a valid data type if not isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)): errors.append( Error( @@ -470,6 +479,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_RECONNECT_BACKOFF_MULTIPLIER is greater than or equal to 1 if ( isinstance(config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER, (int, float)) and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER < 1 @@ -482,6 +492,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_PRERENDER is a valid data type if not isinstance(config.REACTPY_PRERENDER, bool): errors.append( Error( @@ -491,6 +502,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_AUTO_RELOGIN is a valid data type if not isinstance(config.REACTPY_AUTO_RELOGIN, bool): errors.append( Error( @@ -500,6 +512,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_INTERVAL is a valid data type if not isinstance(config.REACTPY_CLEAN_INTERVAL, (int, type(None))): errors.append( Error( @@ -509,6 +522,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_INTERVAL is a positive integer if ( isinstance(config.REACTPY_CLEAN_INTERVAL, int) and config.REACTPY_CLEAN_INTERVAL < 0 @@ -521,6 +535,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_SESSIONS is a valid data type if not isinstance(config.REACTPY_CLEAN_SESSIONS, bool): errors.append( Error( @@ -530,6 +545,7 @@ def reactpy_errors(app_configs, **kwargs): ) ) + # Check if REACTPY_CLEAN_USER_DATA is a valid data type if not isinstance(config.REACTPY_CLEAN_USER_DATA, bool): errors.append( Error( diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 579c73e3..3794ba73 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -2,10 +2,9 @@ import json import os -from typing import Any, Callable, Sequence, Union, cast, overload +from typing import Any, Callable, Sequence, Union, cast from urllib.parse import urlencode from uuid import uuid4 -from warnings import warn from django.contrib.staticfiles.finders import find from django.core.cache import caches @@ -26,40 +25,15 @@ ) -# Type hints for: -# 1. example = view_to_component(my_view, ...) -# 2. @view_to_component -@overload def view_to_component( view: Callable | View | str, - compatibility: bool = False, transforms: Sequence[Callable[[VdomDict], Any]] = (), strict_parsing: bool = True, -) -> Any: ... - - -# Type hints for: -# 1. @view_to_component(...) -@overload -def view_to_component( - view: None = ..., - compatibility: bool = False, - transforms: Sequence[Callable[[VdomDict], Any]] = (), - strict_parsing: bool = True, -) -> Callable[[Callable], Any]: ... - - -def view_to_component( - view: Callable | View | str | None = None, - compatibility: bool = False, - transforms: Sequence[Callable[[VdomDict], Any]] = (), - strict_parsing: bool = True, -) -> Any | Callable[[Callable], Any]: +) -> Any: """Converts a Django view to a ReactPy component. Keyword Args: view: The view to convert, or the view's dotted path as a string. - compatibility: **DEPRECATED.** Use `view_to_iframe` instead. transforms: A list of functions that transforms the newly generated VDOM. \ The functions will be called on each VDOM node. strict_parsing: If True, an exception will be generated if the HTML does not \ @@ -69,37 +43,23 @@ def view_to_component( A function that takes `request, *args, key, **kwargs` and returns a ReactPy component. """ - def decorator(view: Callable | View | str): - if not view: - raise ValueError("A view must be provided to `view_to_component`") - - def constructor( - request: HttpRequest | None = None, - *args, - key: Key | None = None, - **kwargs, - ): - return _view_to_component( - view=view, - compatibility=compatibility, - transforms=transforms, - strict_parsing=strict_parsing, - request=request, - args=args, - kwargs=kwargs, - key=key, - ) - - return constructor - - if not view: - warn( - "Using `view_to_component` as a decorator is deprecated. " - "This functionality will be removed in a future version.", - DeprecationWarning, + def constructor( + request: HttpRequest | None = None, + *args, + key: Key | None = None, + **kwargs, + ): + return _view_to_component( + view=view, + transforms=transforms, + strict_parsing=strict_parsing, + request=request, + args=args, + kwargs=kwargs, + key=key, ) - return decorator(view) if view else decorator + return constructor def view_to_iframe( @@ -180,7 +140,6 @@ def pyscript_component( @component def _view_to_component( view: Callable | View | str, - compatibility: bool, transforms: Sequence[Callable[[VdomDict], Any]], strict_parsing: bool, request: HttpRequest | None, @@ -209,10 +168,6 @@ def _view_to_component( ) async def async_render(): """Render the view in an async hook to avoid blocking the main thread.""" - # Compatibility mode doesn't require a traditional render - if compatibility: - return - # Render the view response = await render_view(resolved_view, _request, _args, _kwargs) set_converted_view( @@ -224,17 +179,6 @@ async def async_render(): ) ) - # Render in compatibility mode, if needed - if compatibility: - # Warn the user that compatibility mode is deprecated - warn( - "view_to_component(compatibility=True) is deprecated and will be removed in a future version. " - "Please use `view_to_iframe` instead.", - DeprecationWarning, - ) - - return view_to_iframe(resolved_view)(*_args, **_kwargs) - # Return the view if it's been rendered via the `async_render` hook return converted_view diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 21a30a32..a891cb5d 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -23,19 +23,11 @@ REACTPY_FAILED_COMPONENTS: set[str] = set() REACTPY_REGISTERED_IFRAME_VIEWS: dict[str, Callable | View] = {} - -# Remove in a future release -REACTPY_WEBSOCKET_URL = getattr( - settings, - "REACTPY_WEBSOCKET_URL", - "reactpy/", -) - # Configurable through Django settings.py REACTPY_URL_PREFIX: str = getattr( settings, "REACTPY_URL_PREFIX", - REACTPY_WEBSOCKET_URL, + "reactpy/", ).strip("/") REACTPY_SESSION_MAX_AGE: int = getattr( settings, diff --git a/src/reactpy_django/database.py b/src/reactpy_django/database.py index 2a7f826d..0d0b2065 100644 --- a/src/reactpy_django/database.py +++ b/src/reactpy_django/database.py @@ -21,7 +21,7 @@ def db_for_write(self, model, **hints): def allow_relation(self, obj1, obj2, **hints): """Returning `None` only allow relations within the same database. - https://docs.djangoproject.com/en/dev/topics/db/multi-db/#limitations-of-multiple-databases + https://docs.djangoproject.com/en/stable/topics/db/multi-db/#limitations-of-multiple-databases """ return None diff --git a/src/reactpy_django/decorators.py b/src/reactpy_django/decorators.py index 59c110b3..39a028a4 100644 --- a/src/reactpy_django/decorators.py +++ b/src/reactpy_django/decorators.py @@ -2,53 +2,17 @@ from functools import wraps from typing import TYPE_CHECKING, Any, Callable -from warnings import warn from reactpy import component -from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict +from reactpy.core.types import ComponentConstructor from reactpy_django.exceptions import DecoratorParamError -from reactpy_django.hooks import use_scope, use_user +from reactpy_django.hooks import use_user if TYPE_CHECKING: from django.contrib.auth.models import AbstractUser -def auth_required( - component: Callable | None = None, - auth_attribute: str = "is_active", - fallback: ComponentType | Callable | VdomDict | None = None, -) -> Callable: - """If the user passes authentication criteria, the decorated component will be rendered. - Otherwise, the fallback component will be rendered. - - This decorator can be used with or without parentheses. - - Args: - auth_attribute: The value to check within the user object. \ - This is checked in the form of `UserModel.`. \ - fallback: The component or VDOM (`reactpy.html` snippet) to render if the user is not authenticated. - """ - - warn( - "auth_required is deprecated and will be removed in the next major version. " - "An equivalent to this decorator's default is @user_passes_test(lambda user: user.is_active).", - DeprecationWarning, - ) - - def decorator(component): - @wraps(component) - def _wrapped_func(*args, **kwargs): - scope = use_scope() - - if getattr(scope["user"], auth_attribute): - return component(*args, **kwargs) - return fallback(*args, **kwargs) if callable(fallback) else fallback - - return _wrapped_func - - # Return for @authenticated(...) and @authenticated respectively - return decorator if component is None else decorator(component) def user_passes_test( diff --git a/src/reactpy_django/http/views.py b/src/reactpy_django/http/views.py index 983c8361..522d3dcf 100644 --- a/src/reactpy_django/http/views.py +++ b/src/reactpy_django/http/views.py @@ -1,7 +1,7 @@ +import asyncio import os from urllib.parse import parse_qs -from aiofile import async_open from django.core.cache import caches from django.core.exceptions import SuspiciousOperation from django.http import HttpRequest, HttpResponse, HttpResponseNotFound @@ -31,8 +31,8 @@ async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: cache_key, version=int(last_modified_time) ) if file_contents is None: - async with async_open(path, "r") as fp: - file_contents = await fp.read() + with open(path, "r", encoding="utf-8") as fp: + file_contents = await asyncio.to_thread(fp.read) await caches[REACTPY_CACHE].adelete(cache_key) await caches[REACTPY_CACHE].aset( cache_key, file_contents, timeout=604800, version=int(last_modified_time) diff --git a/src/reactpy_django/router/__init__.py b/src/reactpy_django/router/__init__.py index 3c48e1ab..4c9c0efd 100644 --- a/src/reactpy_django/router/__init__.py +++ b/src/reactpy_django/router/__init__.py @@ -1,4 +1,4 @@ -from reactpy_router.core import create_router +from reactpy_router import create_router from reactpy_django.router.resolvers import DjangoResolver diff --git a/src/reactpy_django/router/converters.py b/src/reactpy_django/router/converters.py index 3611f63e..483dbcbb 100644 --- a/src/reactpy_django/router/converters.py +++ b/src/reactpy_django/router/converters.py @@ -1,7 +1,8 @@ from django.urls.converters import get_converters -from reactpy_router.simple import ConversionInfo +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 7c095081..4568786c 100644 --- a/src/reactpy_django/router/resolvers.py +++ b/src/reactpy_django/router/resolvers.py @@ -1,58 +1,22 @@ from __future__ import annotations -import re -from typing import Any - -from reactpy_router.simple import ConverterMapping -from reactpy_router.types import Route +from reactpy_router.resolvers import StarletteResolver +from reactpy_router.types import ConversionInfo, Route from reactpy_django.router.converters import CONVERTERS -PARAM_PATTERN = re.compile(r"<(?P\w+:)?(?P\w+)>") - -# TODO: Make reactpy_router's SimpleResolver generic enough to where we don't have to define our own -class DjangoResolver: +class DjangoResolver(StarletteResolver): """A simple route resolver that uses regex to match paths""" - def __init__(self, route: Route) -> None: - self.element = route.element - self.pattern, self.converters = parse_path(route.path) - self.key = self.pattern.pattern - - def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: - match = self.pattern.match(path) - if match: - return ( - self.element, - {k: self.converters[k](v) for k, v in match.groupdict().items()}, - ) - return None - - -# TODO: Make reactpy_router's parse_path generic enough to where we don't have to define our own -def parse_path(path: str) -> tuple[re.Pattern[str], ConverterMapping]: - # Convert path to regex pattern, and make sure to interpret the registered converters (ex. ) - pattern = "^" - last_match_end = 0 - converters: ConverterMapping = {} - for match in PARAM_PATTERN.finditer(path): - param_name = match.group("name") - param_type = (match.group("type") or "str").strip(":") - try: - param_conv = CONVERTERS[param_type] - except KeyError as e: - raise ValueError( - f"Unknown conversion type {param_type!r} in {path!r}" - ) from e - pattern += re.escape(path[last_match_end : match.start()]) - pattern += f"(?P<{param_name}>{param_conv['regex']})" - converters[param_name] = param_conv["func"] - last_match_end = match.end() - pattern += f"{re.escape(path[last_match_end:])}$" - - # Replace literal `*` with "match anything" regex pattern, if it's at the end of the path - if pattern.endswith(r"\*$"): - pattern = f"{pattern[:-3]}.*$" - - return re.compile(pattern), converters + 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, + ) diff --git a/src/reactpy_django/websocket/paths.py b/src/reactpy_django/websocket/paths.py index 17a9c48e..435f9b71 100644 --- a/src/reactpy_django/websocket/paths.py +++ b/src/reactpy_django/websocket/paths.py @@ -10,8 +10,6 @@ ) """A URL path for :class:`ReactpyAsyncWebsocketConsumer`. -Required since the `reverse()` function does not exist for Django Channels, but we need -to know the websocket path. +Required since the `reverse()` function does not exist for Django Channels, but ReactPy needs +to know the current websocket path. """ - -REACTPY_WEBSOCKET_PATH = REACTPY_WEBSOCKET_ROUTE diff --git a/tests/test_app/apps.py b/tests/test_app/apps.py index a5bad495..2bef8446 100644 --- a/tests/test_app/apps.py +++ b/tests/test_app/apps.py @@ -2,8 +2,8 @@ import sys from django.apps import AppConfig -from reactpy_django.utils import register_iframe +from reactpy_django.utils import register_iframe from test_app import views @@ -13,11 +13,11 @@ class TestAppConfig(AppConfig): def ready(self): from django.contrib.auth.models import User - register_iframe("test_app.views.view_to_component_sync_func_compatibility") - register_iframe(views.view_to_component_async_func_compatibility) - register_iframe(views.ViewToComponentSyncClassCompatibility) - register_iframe(views.ViewToComponentAsyncClassCompatibility) - register_iframe(views.ViewToComponentTemplateViewClassCompatibility) + register_iframe("test_app.views.view_to_iframe_sync_func") + register_iframe(views.view_to_iframe_async_func) + register_iframe(views.ViewToIframeSyncClass) + register_iframe(views.ViewToIframeAsyncClass) + register_iframe(views.ViewToIframeTemplateViewClass) register_iframe(views.view_to_iframe_args) if "test" in sys.argv: diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 69b1541c..4ae0544e 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -2,15 +2,14 @@ import inspect from pathlib import Path -import reactpy_django from channels.auth import login, logout from channels.db import database_sync_to_async from django.contrib.auth import get_user_model from django.http import HttpRequest -from django.shortcuts import render from reactpy import component, hooks, html, web -from reactpy_django.components import view_to_component, view_to_iframe +import reactpy_django +from reactpy_django.components import view_to_component, view_to_iframe from test_app.models import ( AsyncForiegnChild, AsyncRelationalChild, @@ -72,7 +71,7 @@ def object_in_templatetag(my_object: TestObject): SimpleButtonModule = web.module_from_file( "SimpleButton", - Path(__file__).parent / "tests" / "js" / "simple-button.js", + Path(__file__).parent / "tests" / "js" / "button-from-js-module.js", resolve_exports=False, fallback="...", ) @@ -80,8 +79,10 @@ def object_in_templatetag(my_object: TestObject): @component -def simple_button(): - return html._("simple_button:", SimpleButton({"id": "simple-button"})) +def button_from_js_module(): + return html._( + "button_from_js_module:", SimpleButton({"id": "button-from-js-module"}) + ) @component @@ -146,45 +147,24 @@ def django_js(): ) -@component -@reactpy_django.decorators.auth_required( - fallback=html.div( - {"id": "unauthorized-user-fallback"}, "unauthorized_user: Success" - ) -) -def unauthorized_user(): - return html.div({"id": "unauthorized-user"}, "unauthorized_user: Fail") - - -@component -@reactpy_django.decorators.auth_required( - auth_attribute="is_anonymous", - fallback=html.div({"id": "authorized-user-fallback"}, "authorized_user: Fail"), -) -def authorized_user(): - return html.div({"id": "authorized-user"}, "authorized_user: Success") - - @reactpy_django.decorators.user_passes_test( lambda user: user.is_anonymous, - fallback=html.div( - {"id": "authorized-user-test-fallback"}, "authorized_user_test: Fail" - ), + fallback=html.div({"id": "authorized-user-fallback"}, "authorized_user: Fail"), ) @component -def authorized_user_test(): - return html.div({"id": "authorized-user-test"}, "authorized_user_test: Success") +def authorized_user(): + return html.div({"id": "authorized-user"}, "authorized_user: Success") @reactpy_django.decorators.user_passes_test( lambda user: user.is_active, fallback=html.div( - {"id": "unauthorized-user-test-fallback"}, "unauthorized_user_test: Success" + {"id": "unauthorized-user-fallback"}, "unauthorized_user: Success" ), ) @component -def unauthorized_user_test(): - return html.div({"id": "unauthorized-user-test"}, "unauthorized_user_test: Fail") +def unauthorized_user(): + return html.div({"id": "unauthorized-user"}, "unauthorized_user: Fail") @reactpy_django.decorators.user_passes_test(lambda user: True) @@ -485,20 +465,12 @@ async def on_change(event): view_to_component_template_view_class = view_to_component( views.ViewToComponentTemplateViewClass.as_view() ) -_view_to_component_sync_func_compatibility = view_to_component( - views.view_to_component_sync_func_compatibility, compatibility=True -) -_view_to_component_async_func_compatibility = view_to_component( - views.view_to_component_async_func_compatibility, compatibility=True -) -_view_to_component_sync_class_compatibility = view_to_component( - views.ViewToComponentSyncClassCompatibility.as_view(), compatibility=True -) -_view_to_component_async_class_compatibility = view_to_component( - views.ViewToComponentAsyncClassCompatibility.as_view(), compatibility=True -) -_view_to_component_template_view_class_compatibility = view_to_component( - views.ViewToComponentTemplateViewClassCompatibility.as_view(), compatibility=True +_view_to_iframe_sync_func = view_to_iframe(views.view_to_iframe_sync_func) +_view_to_iframe_async_func = view_to_iframe(views.view_to_iframe_async_func) +_view_to_iframe_sync_class = view_to_iframe(views.ViewToIframeSyncClass.as_view()) +_view_to_iframe_async_class = view_to_iframe(views.ViewToIframeAsyncClass.as_view()) +_view_to_iframe_template_view_class = view_to_iframe( + views.ViewToIframeTemplateViewClass.as_view() ) _view_to_iframe_args = view_to_iframe(views.view_to_iframe_args) _view_to_iframe_not_registered = view_to_iframe("view_does_not_exist") @@ -509,42 +481,42 @@ async def on_change(event): @component -def view_to_component_sync_func_compatibility(): +def view_to_iframe_sync_func(): return html.div( {"id": inspect.currentframe().f_code.co_name}, # type: ignore - _view_to_component_sync_func_compatibility(key="test"), + _view_to_iframe_sync_func(key="test"), ) @component -def view_to_component_async_func_compatibility(): +def view_to_iframe_async_func(): return html.div( {"id": inspect.currentframe().f_code.co_name}, # type: ignore - _view_to_component_async_func_compatibility(), + _view_to_iframe_async_func(), ) @component -def view_to_component_sync_class_compatibility(): +def view_to_iframe_sync_class(): return html.div( {"id": inspect.currentframe().f_code.co_name}, # type: ignore - _view_to_component_sync_class_compatibility(), + _view_to_iframe_sync_class(), ) @component -def view_to_component_async_class_compatibility(): +def view_to_iframe_async_class(): return html.div( {"id": inspect.currentframe().f_code.co_name}, # type: ignore - _view_to_component_async_class_compatibility(), + _view_to_iframe_async_class(), ) @component -def view_to_component_template_view_class_compatibility(): +def view_to_iframe_template_view_class(): return html.div( {"id": inspect.currentframe().f_code.co_name}, # type: ignore - _view_to_component_template_view_class_compatibility(), + _view_to_iframe_template_view_class(), ) @@ -623,24 +595,6 @@ def on_click(_): ) -@view_to_component -def view_to_component_decorator(request): - return render( - request, - "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore - ) - - -@view_to_component(strict_parsing=False) -def view_to_component_decorator_args(request): - return render( - request, - "view_to_component.html", - {"test_name": inspect.currentframe().f_code.co_name}, # type: ignore - ) - - @component def custom_host(number=0): scope = reactpy_django.hooks.use_scope() diff --git a/tests/test_app/router/components.py b/tests/test_app/router/components.py index 19eee0ba..ea95c5f2 100644 --- a/tests/test_app/router/components.py +++ b/tests/test_app/router/components.py @@ -1,45 +1,42 @@ from reactpy import component, html, use_location +from reactpy_router import route, use_params, use_search_params +from reactpy_router.types import Route + from reactpy_django.router import django_router -from reactpy_router import route, use_params, use_query @component def display_params(string: str): location = use_location() - query = use_query() - params = use_params() + search_params = use_search_params() + url_params = use_params() return html._( html.div({"id": "router-string"}, string), - html.div(f"Params: {params}"), html.div( {"id": "router-path", "data-path": location.pathname}, - f"Path Name: {location.pathname}", + f"path: {location.pathname}", ), - html.div(f"Query String: {location.search}"), - html.div(f"Query: {query}"), + html.div(f"url_params: {url_params}"), + html.div(f"location.search: {location.search}"), + html.div(f"search_params: {search_params}"), ) +def show_route(path: str, *children: Route) -> Route: + return route(path, display_params(path), *children) + + @component def main(): return django_router( - route("/router/", display_params("Path 1")), - route("/router/any//", display_params("Path 2")), - route("/router/integer//", display_params("Path 3")), - route("/router/path//", display_params("Path 4")), - route("/router/slug//", display_params("Path 5")), - route("/router/string//", display_params("Path 6")), - route("/router/uuid//", display_params("Path 7")), - route("/router/", None, route("abc/", display_params("Path 8"))), - route( - "/router/two///", - display_params("Path 9"), - ), - route( - "/router/star/", - None, - route("one/", display_params("Path 11")), - route("*", display_params("Path 12")), - ), + 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///"), ) diff --git a/tests/test_app/router/urls.py b/tests/test_app/router/urls.py index b497b951..73b60990 100644 --- a/tests/test_app/router/urls.py +++ b/tests/test_app/router/urls.py @@ -3,5 +3,5 @@ from test_app.router.views import router urlpatterns = [ - re_path(r"^router/(?P.*)/?$", router), + re_path(r"^router/(?P.*)$", router), ] diff --git a/tests/test_app/templates/base.html b/tests/test_app/templates/base.html index f15094ac..117e867d 100644 --- a/tests/test_app/templates/base.html +++ b/tests/test_app/templates/base.html @@ -27,7 +27,7 @@

ReactPy Test Page


{% component "test_app.components.object_in_templatetag" my_object %}
- {% component "test_app.components.simple_button" %} + {% component "test_app.components.button_from_js_module" %}
{% component "test_app.components.use_connection" %}
@@ -45,10 +45,6 @@

ReactPy Test Page


{% component "test_app.components.authorized_user" %}
- {% component "test_app.components.unauthorized_user_test" %} -
- {% component "test_app.components.authorized_user_test" %} -
{% component "test_app.components.relational_query" %}
{% component "test_app.components.async_relational_query" %} @@ -75,19 +71,15 @@

ReactPy Test Page


{% component "test_app.components.view_to_component_kwargs" %}
- {% component "test_app.components.view_to_component_decorator" %} -
- {% component "test_app.components.view_to_component_decorator_args" %} -
- {% component "test_app.components.view_to_component_sync_func_compatibility" %} + {% component "test_app.components.view_to_iframe_sync_func" %}
- {% component "test_app.components.view_to_component_async_func_compatibility" %} + {% component "test_app.components.view_to_iframe_async_func" %}
- {% component "test_app.components.view_to_component_sync_class_compatibility" %} + {% component "test_app.components.view_to_iframe_sync_class" %}
- {% component "test_app.components.view_to_component_async_class_compatibility" %} + {% component "test_app.components.view_to_iframe_async_class" %}
- {% component "test_app.components.view_to_component_template_view_class_compatibility" %} + {% component "test_app.components.view_to_iframe_template_view_class" %}
{% component "test_app.components.view_to_iframe_args" %}
diff --git a/tests/test_app/tests/js/simple-button.js b/tests/test_app/tests/js/button-from-js-module.js similarity index 100% rename from tests/test_app/tests/js/simple-button.js rename to tests/test_app/tests/js/button-from-js-module.js diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index 11fdc390..f3726a4c 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -12,6 +12,7 @@ from django.db import connections from django.test.utils import modify_settings from playwright.sync_api import TimeoutError, sync_playwright + from reactpy_django.models import ComponentSession from reactpy_django.utils import strtobool @@ -21,6 +22,7 @@ class ComponentTests(ChannelsLiveServerTestCase): from django.db import DEFAULT_DB_ALIAS + from reactpy_django import config databases = {"default"} @@ -65,6 +67,7 @@ def setUpClass(cls): headless = strtobool(os.environ.get("PLAYWRIGHT_HEADLESS", GITHUB_ACTIONS)) cls.browser = cls.playwright.chromium.launch(headless=bool(headless)) cls.page = cls.browser.new_page() + cls.page.set_default_timeout(5000) @classmethod def tearDownClass(cls): @@ -74,14 +77,14 @@ def tearDownClass(cls): cls.playwright.stop() # Close the other server processes + cls._server_process.terminate() + cls._server_process.join() cls._server_process2.terminate() cls._server_process2.join() cls._server_process3.terminate() cls._server_process3.join() # Repurposed from ChannelsLiveServerTestCase._post_teardown - cls._server_process.terminate() - cls._server_process.join() cls._live_server_modified_settings.disable() for db_name in {"default", config.REACTPY_DATABASE}: call_command( @@ -94,13 +97,11 @@ def tearDownClass(cls): def _pre_setup(self): """Handled manually in `setUpClass` to speed things up.""" - pass def _post_teardown(self): """Handled manually in `tearDownClass` to prevent TransactionTestCase from doing database flushing. This is needed to prevent a `SynchronousOnlyOperation` from occuring due to a bug within `ChannelsLiveServerTestCase`.""" - pass def setUp(self): if self.page.url == "about:blank": @@ -121,7 +122,7 @@ def test_object_in_templatetag(self): self.page.locator("#object_in_templatetag[data-success=true]").wait_for() def test_component_from_web_module(self): - self.page.wait_for_selector("#simple-button") + self.page.wait_for_selector("#button-from-js-module") def test_use_connection(self): self.page.locator("#use-connection[data-success=true]").wait_for() @@ -164,24 +165,6 @@ def test_authorized_user(self): ) self.page.wait_for_selector("#authorized-user") - def test_unauthorized_user_test(self): - self.assertRaises( - TimeoutError, - self.page.wait_for_selector, - "#unauthorized-user-test", - timeout=1, - ) - self.page.wait_for_selector("#unauthorized-user-test-fallback") - - def test_authorized_user_test(self): - self.assertRaises( - TimeoutError, - self.page.wait_for_selector, - "#authorized-user-test-fallback", - timeout=1, - ) - self.page.wait_for_selector("#authorized-user-test") - def test_relational_query(self): self.page.locator("#relational-query[data-success=true]").wait_for() @@ -260,39 +243,29 @@ def test_view_to_component_args(self): def test_view_to_component_kwargs(self): self._click_btn_and_check_success("view_to_component_kwargs") - def test_view_to_component_sync_func_compatibility(self): - self.page.frame_locator( - "#view_to_component_sync_func_compatibility > iframe" - ).locator( - "#view_to_component_sync_func_compatibility[data-success=true]" + def test_view_to_iframe_sync_func(self): + self.page.frame_locator("#view_to_iframe_sync_func > iframe").locator( + "#view_to_iframe_sync_func[data-success=true]" ).wait_for() - def test_view_to_component_async_func_compatibility(self): - self.page.frame_locator( - "#view_to_component_async_func_compatibility > iframe" - ).locator( - "#view_to_component_async_func_compatibility[data-success=true]" + def test_view_to_iframe_async_func(self): + self.page.frame_locator("#view_to_iframe_async_func > iframe").locator( + "#view_to_iframe_async_func[data-success=true]" ).wait_for() - def test_view_to_component_sync_class_compatibility(self): - self.page.frame_locator( - "#view_to_component_sync_class_compatibility > iframe" - ).locator( - "#ViewToComponentSyncClassCompatibility[data-success=true]" + def test_view_to_iframe_sync_class(self): + self.page.frame_locator("#view_to_iframe_sync_class > iframe").locator( + "#ViewToIframeSyncClass[data-success=true]" ).wait_for() - def test_view_to_component_async_class_compatibility(self): - self.page.frame_locator( - "#view_to_component_async_class_compatibility > iframe" - ).locator( - "#ViewToComponentAsyncClassCompatibility[data-success=true]" + def test_view_to_iframe_async_class(self): + self.page.frame_locator("#view_to_iframe_async_class > iframe").locator( + "#ViewToIframeAsyncClass[data-success=true]" ).wait_for() - def test_view_to_component_template_view_class_compatibility(self): - self.page.frame_locator( - "#view_to_component_template_view_class_compatibility > iframe" - ).locator( - "#ViewToComponentTemplateViewClassCompatibility[data-success=true]" + def test_view_to_iframe_template_view_class(self): + self.page.frame_locator("#view_to_iframe_template_view_class > iframe").locator( + "#ViewToIframeTemplateViewClass[data-success=true]" ).wait_for() def test_view_to_iframe_args(self): @@ -300,14 +273,6 @@ def test_view_to_iframe_args(self): "#view_to_iframe_args[data-success=Success]" ).wait_for() - def test_view_to_component_decorator(self): - self.page.locator("#view_to_component_decorator[data-success=true]").wait_for() - - def test_view_to_component_decorator_args(self): - self.page.locator( - "#view_to_component_decorator_args[data-success=true]" - ).wait_for() - def test_component_session_exists(self): """Session should exist for components with args/kwargs.""" component = self.page.locator("#parametrized-component") @@ -322,7 +287,7 @@ def test_component_session_exists(self): def test_component_session_missing(self): """No session should exist for components that don't have args/kwargs.""" - component = self.page.locator("#simple-button") + component = self.page.locator("#button-from-js-module") component.wait_for() parent = component.locator("..") session_id = parent.get_attribute("id") @@ -577,26 +542,44 @@ def test_url_router(self): new_page.goto(f"{self.live_server_url}/router/") path = new_page.wait_for_selector("#router-path") self.assertIn("/router/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/", string.text_content()) - new_page.goto(f"{self.live_server_url}/router/any/123/") + new_page.goto(f"{self.live_server_url}/router/subroute/") path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/any/123/", path.get_attribute("data-path")) + self.assertIn("/router/subroute/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("subroute/", string.text_content()) + + new_page.goto(f"{self.live_server_url}/router/unspecified/123/") + path = new_page.wait_for_selector("#router-path") + self.assertIn("/router/unspecified/123/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/unspecified//", string.text_content()) new_page.goto(f"{self.live_server_url}/router/integer/123/") path = new_page.wait_for_selector("#router-path") self.assertIn("/router/integer/123/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/integer//", string.text_content()) new_page.goto(f"{self.live_server_url}/router/path/abc/123/") path = new_page.wait_for_selector("#router-path") self.assertIn("/router/path/abc/123/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/path//", string.text_content()) new_page.goto(f"{self.live_server_url}/router/slug/abc-123/") path = new_page.wait_for_selector("#router-path") self.assertIn("/router/slug/abc-123/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/slug//", string.text_content()) new_page.goto(f"{self.live_server_url}/router/string/abc/") path = new_page.wait_for_selector("#router-path") self.assertIn("/router/string/abc/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/string//", string.text_content()) new_page.goto( f"{self.live_server_url}/router/uuid/123e4567-e89b-12d3-a456-426614174000/" @@ -606,29 +589,27 @@ def test_url_router(self): "/router/uuid/123e4567-e89b-12d3-a456-426614174000/", path.get_attribute("data-path"), ) - - new_page.goto(f"{self.live_server_url}/router/abc/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/abc/", path.get_attribute("data-path")) - - new_page.goto(f"{self.live_server_url}/router/two/123/abc/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/two/123/abc/", path.get_attribute("data-path")) - - new_page.goto(f"{self.live_server_url}/router/star/one/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/star/one/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual("/router/uuid//", string.text_content()) new_page.goto( - f"{self.live_server_url}/router/star/adslkjgklasdjhfah/6789543256/" + f"{self.live_server_url}/router/any/adslkjgklasdjhfah/6789543256/" ) path = new_page.wait_for_selector("#router-path") self.assertIn( - "/router/star/adslkjgklasdjhfah/6789543256/", + "/router/any/adslkjgklasdjhfah/6789543256/", path.get_attribute("data-path"), ) string = new_page.query_selector("#router-string") - self.assertEqual("Path 12", string.text_content()) + self.assertEqual("/router/any/", string.text_content()) + + new_page.goto(f"{self.live_server_url}/router/two/123/abc/") + path = new_page.wait_for_selector("#router-path") + self.assertIn("/router/two/123/abc/", path.get_attribute("data-path")) + string = new_page.query_selector("#router-string") + self.assertEqual( + "/router/two///", string.text_content() + ) finally: new_page.close() diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 7af05f61..0c75b357 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -87,7 +87,7 @@ def get_context_data(self, **kwargs): return {"test_name": self.__class__.__name__} -def view_to_component_sync_func_compatibility(request): +def view_to_iframe_sync_func(request): return render( request, "view_to_component.html", @@ -95,7 +95,7 @@ def view_to_component_sync_func_compatibility(request): ) -async def view_to_component_async_func_compatibility(request): +async def view_to_iframe_async_func(request): return await database_sync_to_async(render)( request, "view_to_component.html", @@ -103,7 +103,7 @@ async def view_to_component_async_func_compatibility(request): ) -class ViewToComponentSyncClassCompatibility(View): +class ViewToIframeSyncClass(View): def get(self, request, *args, **kwargs): return render( request, @@ -112,7 +112,7 @@ def get(self, request, *args, **kwargs): ) -class ViewToComponentAsyncClassCompatibility(View): +class ViewToIframeAsyncClass(View): async def get(self, request, *args, **kwargs): return await database_sync_to_async(render)( request, @@ -121,7 +121,7 @@ async def get(self, request, *args, **kwargs): ) -class ViewToComponentTemplateViewClassCompatibility(TemplateView): +class ViewToIframeTemplateViewClass(TemplateView): template_name = "view_to_component.html" def get_context_data(self, **kwargs): From 7948ac4eddad97fddc3a2a8aa781f12a75376d58 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Tue, 22 Oct 2024 00:40:13 -0700 Subject: [PATCH 11/35] Bump javascript dependencies (#253) --- .editorconfig | 32 + .prettierrc | 4 + src/js/package-lock.json | 1497 +++++++++++++++++++++----------------- src/js/package.json | 20 +- src/js/src/index.tsx | 4 +- src/js/tsconfig.json | 6 +- 6 files changed, 866 insertions(+), 697 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierrc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..356385d7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +end_of_line = lf + +[*.py] +indent_size = 4 +max_line_length = 120 + +[*.md] +indent_size = 4 + +[*.html] +max_line_length = off + +[*.js] +max_line_length = off + +[*.css] +indent_size = 4 +max_line_length = off + +# Tests can violate line width restrictions in the interest of clarity. +[**/test_*.py] +max_line_length = off diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..32ad81f3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "proseWrap": "never", + "trailingComma": "all" +} diff --git a/src/js/package-lock.json b/src/js/package-lock.json index d4cb1c0b..0bdd2ca5 100644 --- a/src/js/package-lock.json +++ b/src/js/package-lock.json @@ -5,23 +5,23 @@ "packages": { "": { "dependencies": { - "@pyscript/core": "^0.4.48", + "@pyscript/core": "^0.6.7", "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^11.1.6", - "morphdom": "^2.7.3", - "tslib": "^2.6.2" + "@rollup/plugin-typescript": "^12.1.1", + "morphdom": "^2.7.4", + "tslib": "^2.8.0" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", - "prettier": "^3.2.3", - "rollup": "^4.9.5", - "typescript": "^5.3.3" + "prettier": "^3.3.3", + "rollup": "^4.24.0", + "typescript": "^5.6.3" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -49,9 +49,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -80,44 +80,23 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -125,28 +104,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -161,9 +118,10 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true }, "node_modules/@jridgewell/sourcemap-codec": { @@ -208,16 +166,22 @@ } }, "node_modules/@pyscript/core": { - "version": "0.4.48", - "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.48.tgz", - "integrity": "sha512-cVZ//1WDkWhjZ1tOjUB1YJ5mKxDf3kMpzS/pw7Oe9/BMrB/NM3TxxCQ9Oyvq7Fkfv1F+srIcsi1xZ5gQeP+5Tg==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.6.7.tgz", + "integrity": "sha512-Ij9nrIAp49IkavzTYYwoV4K12MzzVCvcfmkGeqc9nPhtwBxPWSDJ9LEJtfO9MzoMV3dog5FwHdUh2WbZbqqZfw==", "dependencies": { "@ungap/with-resolvers": "^0.1.0", + "@webreflection/idb-map": "^0.3.2", + "add-promise-listener": "^0.1.3", "basic-devtools": "^0.1.6", - "polyscript": "^0.13.5", + "polyscript": "^0.16.3", + "sabayon": "^0.5.2", "sticky-module": "^0.1.1", "to-json-callback": "^0.1.1", "type-checked-collections": "^0.1.7" + }, + "engines": { + "node": ">=20" } }, "node_modules/@reactpy/client": { @@ -234,20 +198,21 @@ } }, "node_modules/@rollup/plugin-commonjs": { - "version": "25.0.7", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", - "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "version": "28.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", + "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", - "glob": "^8.0.3", + "fdir": "^6.2.0", "is-reference": "1.2.1", - "magic-string": "^0.30.3" + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0 || 14 >= 14.17" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" @@ -258,16 +223,41 @@ } } }, + "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", - "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.1", "is-module": "^1.0.0", "resolve": "^1.22.1" }, @@ -284,9 +274,9 @@ } }, "node_modules/@rollup/plugin-replace": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.5.tgz", - "integrity": "sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", + "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", "dev": true, "dependencies": { "@rollup/pluginutils": "^5.0.1", @@ -305,9 +295,9 @@ } }, "node_modules/@rollup/plugin-typescript": { - "version": "11.1.6", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz", - "integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==", + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", + "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" @@ -351,9 +341,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.5.tgz", - "integrity": "sha512-idWaG8xeSRCfRq9KpRysDHJ/rEHBEXcHuJ82XY0yYFIWnLMjZv9vF/7DOq8djQ2n3Lk6+3qfSH8AqlmHlmi1MA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], @@ -364,9 +354,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.9.5.tgz", - "integrity": "sha512-f14d7uhAMtsCGjAYwZGv6TwuS3IFaM4ZnGMUn3aCBgkcHAYErhV1Ad97WzBvS2o0aaDv4mVz+syiN0ElMyfBPg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], @@ -377,9 +367,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.9.5.tgz", - "integrity": "sha512-ndoXeLx455FffL68OIUrVr89Xu1WLzAG4n65R8roDlCoYiQcGGg6MALvs2Ap9zs7AHg8mpHtMpwC8jBBjZrT/w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], @@ -390,9 +380,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.9.5.tgz", - "integrity": "sha512-UmElV1OY2m/1KEEqTlIjieKfVwRg0Zwg4PLgNf0s3glAHXBN99KLpw5A5lrSYCa1Kp63czTpVll2MAqbZYIHoA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], @@ -403,9 +393,22 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.9.5.tgz", - "integrity": "sha512-Q0LcU61v92tQB6ae+udZvOyZ0wfpGojtAKrrpAaIqmJ7+psq4cMIhT/9lfV6UQIpeItnq/2QDROhNLo00lOD1g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], @@ -416,9 +419,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.9.5.tgz", - "integrity": "sha512-dkRscpM+RrR2Ee3eOQmRWFjmV/payHEOrjyq1VZegRUa5OrZJ2MAxBNs05bZuY0YCtpqETDy1Ix4i/hRqX98cA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], @@ -429,9 +432,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.9.5.tgz", - "integrity": "sha512-QaKFVOzzST2xzY4MAmiDmURagWLFh+zZtttuEnuNn19AiZ0T3fhPyjPPGwLNdiDT82ZE91hnfJsUiDwF9DClIQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], @@ -441,10 +444,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.9.5.tgz", - "integrity": "sha512-HeGqmRJuyVg6/X6MpE2ur7GbymBPS8Np0S/vQFHDmocfORT+Zt76qu+69NUoxXzGqVP1pzaY6QIi0FJWLC3OPA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], @@ -454,10 +470,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", - "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -468,9 +497,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.9.5.tgz", - "integrity": "sha512-ezyFUOwldYpj7AbkwyW9AJ203peub81CaAIVvckdkyH8EvhEIoKzaMFJj0G4qYJ5sw3BpqhFrsCc30t54HV8vg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -481,9 +510,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.9.5.tgz", - "integrity": "sha512-aHSsMnUw+0UETB0Hlv7B/ZHOGY5bQdwMKJSzGfDfvyhnpmVxLMGnQPGNE9wgqkLUs3+gbG1Qx02S2LLfJ5GaRQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], @@ -494,9 +523,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.9.5.tgz", - "integrity": "sha512-AiqiLkb9KSf7Lj/o1U3SEP9Zn+5NuVKgFdRIZkvd4N0+bYrTOovVd0+LmYCPQGbocT4kvFyK+LXCDiXPBF3fyA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], @@ -507,9 +536,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.9.5.tgz", - "integrity": "sha512-1q+mykKE3Vot1kaFJIDoUFv5TuW+QQVaf2FmTT9krg86pQrGStOSJJ0Zil7CFagyxDuouTepzt5Y5TVzyajOdQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], @@ -520,9 +549,9 @@ ] }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/prop-types": { "version": "15.7.11", @@ -578,14 +607,14 @@ "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" }, "node_modules/@webreflection/idb-map": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.1.tgz", - "integrity": "sha512-lRCanqwR7tHHFohJHAMSMEZnoNPvgjcKr0f5e4y+lTJA+fctT61EZ+f5pT5/+8+wlSsMAvXjzfKRLT6o9aqxbA==" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.2.tgz", + "integrity": "sha512-VLBTx6EUYF/dPdLyyjWWKxQmTWnVXTT1YJekrJUmfGxBcqEVL0Ih2EQptNG/JezkTYgJ0uSTb0yAum/THltBvQ==" }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -603,6 +632,11 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/add-promise-listener": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/add-promise-listener/-/add-promise-listener-0.1.3.tgz", + "integrity": "sha512-hQ6IgGJ7NvvlPYbwdekhdVwPb4QzEptNZ5v7B4XRKz7FukUPDuF/v+R5EFHArWmhmq4d+xv0G4/B5bu2GSiz9Q==" + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -650,28 +684,32 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/array-includes": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz", - "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", "is-string": "^1.0.7" }, "engines": { @@ -681,6 +719,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -718,30 +776,34 @@ } }, "node_modules/array.prototype.tosorted": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.2.tgz", - "integrity": "sha512-HuQCHOlk1Weat5jzStICBCd83NxiIMwqDg/dHEsoefabn/hJRj5pVdWcPUSpRrwhwxZOsQassMpgN/xRYFBMIg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -751,20 +813,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asynciterator.prototype": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz", - "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - } - }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -784,35 +840,29 @@ "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dev": true, "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -852,17 +902,13 @@ } }, "node_modules/coincident": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", - "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-2.2.3.tgz", + "integrity": "sha512-yxLzgZqDSEPOczzNoM4T3gVyg955Kbk5snkaAkQw8xvh8zfxGuUtbaOaDNCaP3HxWeRZZjwbJi80hapAikpvoQ==", "dependencies": { - "@ungap/structured-clone": "^1.2.0", - "@ungap/with-resolvers": "^0.1.0", - "gc-hook": "^0.3.1", - "proxy-target": "^3.0.2" - }, - "optionalDependencies": { - "ws": "^8.16.0" + "gc-hook": "^0.4.1", + "js-proxy": "^0.5.1", + "sabayon": "^0.5.2" } }, "node_modules/color-convert": { @@ -915,13 +961,64 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -948,17 +1045,20 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-properties": { @@ -991,50 +1091,57 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -1043,37 +1150,73 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-iterator-helpers": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", - "integrity": "sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", + "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", "dev": true, "dependencies": { - "asynciterator.prototype": "^1.0.0", - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "define-properties": "^1.2.1", - "es-abstract": "^1.22.1", - "es-set-tostringtag": "^2.0.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.2.1", - "globalthis": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.4", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "iterator.prototype": "^1.1.2", - "safe-array-concat": "^1.0.1" + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.3", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-set-tostringtag": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "has-tostringtag": "^1.0.0", - "hasown": "^2.0.0" + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -1118,16 +1261,17 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -1173,43 +1317,35 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.33.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz", - "integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", "dev": true, "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flatmap": "^1.3.1", - "array.prototype.tosorted": "^1.1.1", + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.12", + "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.entries": "^1.1.6", - "object.fromentries": "^2.0.6", - "object.hasown": "^1.1.2", - "object.values": "^1.1.6", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.4", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.8" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react/node_modules/doctrine": { @@ -1224,18 +1360,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-react/node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -1281,28 +1405,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -1394,9 +1496,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.0.tgz", - "integrity": "sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -1445,9 +1547,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/for-each": { @@ -1520,33 +1622,38 @@ } }, "node_modules/gc-hook": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", - "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.4.1.tgz", + "integrity": "sha512-uiF+uUftDVLr+VRdudsdsT3/LQYnv2ntwhRH964O7xXDI57Smrek5olv75Wb8Nnz6U+7iVTRXsBlxKcsaDTJTQ==" }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dev": true, "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -1556,19 +1663,21 @@ } }, "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=12" + "node": "*" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -1602,12 +1711,13 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -1653,21 +1763,21 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "dev": true, "engines": { "node": ">= 0.4" @@ -1689,12 +1799,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -1704,9 +1814,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -1720,9 +1830,9 @@ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" }, "node_modules/ignore": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", - "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "engines": { "node": ">= 4" @@ -1757,6 +1867,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -1770,12 +1881,12 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -1784,14 +1895,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1840,21 +1953,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", - "dev": true, - "dependencies": { - "builtin-modules": "^3.3.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -1878,6 +1976,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -1942,10 +2055,13 @@ } }, "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1957,9 +2073,9 @@ "dev": true }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "engines": { "node": ">= 0.4" @@ -2018,21 +2134,27 @@ } }, "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2069,12 +2191,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -2084,10 +2206,13 @@ } }, "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2105,13 +2230,16 @@ } }, "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2130,9 +2258,9 @@ "dev": true }, "node_modules/iterator.prototype": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", - "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", + "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", "dev": true, "dependencies": { "define-properties": "^1.2.1", @@ -2140,8 +2268,25 @@ "has-symbols": "^1.0.3", "reflect.getprototypeof": "^1.0.4", "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-proxy": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/js-proxy/-/js-proxy-0.5.1.tgz", + "integrity": "sha512-G1AswnGndelrmZ2tuJi5NWlXo28BucJdgO8aKP5U1NkxWxPvgFA510Ku6at+1A17Kh2ja7A/r4RDtd9Hdr6sOw==", + "dependencies": { + "gc-hook": "^0.3.1", + "proxy-target": "^3.0.2" } }, + "node_modules/js-proxy/node_modules/gc-hook": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", + "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2267,26 +2412,26 @@ } }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/morphdom": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.3.tgz", - "integrity": "sha512-rvGK92GxSuPEZLY8D/JH07cG3BxyA+/F0Bxg32OoGAEFFhGWA3OqVpqPZlOgZTCR52clXrmz+z2pYSJ6gOig1w==" + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz", + "integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==" }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/natural-compare": { @@ -2304,10 +2449,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2340,28 +2488,29 @@ } }, "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" } }, "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2370,28 +2519,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.hasown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", - "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2517,24 +2653,33 @@ "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" }, "node_modules/polyscript": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.13.5.tgz", - "integrity": "sha512-PwXWnhLbOMtvZWFIN271JhaN7KnxESaMtv9Rcdrq1TKTCMnkz9idvYb3Od1iumBJlr49lLlwyUKeGb423rFR4w==", + "version": "0.16.3", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.16.3.tgz", + "integrity": "sha512-I3kHxt62FMRAX2iVl24iCEtG4UnUInMSbv/LnwevkmjOErLPAQtES4CNzU/fgKRpXYCqp0WWQaRvRYkJhpMIbA==", "dependencies": { "@ungap/structured-clone": "^1.2.0", "@ungap/with-resolvers": "^0.1.0", "@webreflection/fetch": "^0.1.5", - "@webreflection/idb-map": "^0.3.1", + "@webreflection/idb-map": "^0.3.2", "basic-devtools": "^0.1.6", "codedent": "^0.1.2", - "coincident": "^1.2.3", - "gc-hook": "^0.3.1", + "coincident": "^2.2.2", + "gc-hook": "^0.4.1", "html-escaper": "^3.0.3", "proxy-target": "^3.0.2", "sticky-module": "^0.1.1", "to-json-callback": "^0.1.1" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2545,9 +2690,9 @@ } }, "node_modules/prettier": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.3.tgz", - "integrity": "sha512-QNhUTBq+mqt1oH1dTfY3phOKNhcDdJkfttHI6u0kj7M2+c+7fmNKlgh2GhnHiqMcbxJ+a0j2igz/2jfl9QKLuw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -2638,15 +2783,16 @@ "dev": true }, "node_modules/reflect.getprototypeof": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz", - "integrity": "sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", "globalthis": "^1.0.3", "which-builtin-type": "^1.1.3" }, @@ -2658,14 +2804,15 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", + "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2713,6 +2860,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -2724,55 +2872,13 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.9.5.tgz", - "integrity": "sha512-E4vQW0H/mbNMw2yLSqJyjtkHY9dslf/p0zuT1xehNRqUTBOFMqEjguDvqhXr7N7r/4ttb2jr4T41d3dncmIgbQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "devOptional": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -2782,19 +2888,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.9.5", - "@rollup/rollup-android-arm64": "4.9.5", - "@rollup/rollup-darwin-arm64": "4.9.5", - "@rollup/rollup-darwin-x64": "4.9.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.9.5", - "@rollup/rollup-linux-arm64-gnu": "4.9.5", - "@rollup/rollup-linux-arm64-musl": "4.9.5", - "@rollup/rollup-linux-riscv64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-gnu": "4.9.5", - "@rollup/rollup-linux-x64-musl": "4.9.5", - "@rollup/rollup-win32-arm64-msvc": "4.9.5", - "@rollup/rollup-win32-ia32-msvc": "4.9.5", - "@rollup/rollup-win32-x64-msvc": "4.9.5", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -2821,14 +2930,22 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/sabayon": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/sabayon/-/sabayon-0.5.2.tgz", + "integrity": "sha512-7y8dwQFhInkUuI0JL1rAGLYeKz9LypgfzMOVO04v9pYvepzYJ6B7MhyoRiVIx2sGm7MhrD5h/SaHP6p+5Uc7Cw==", + "bin": { + "sabayon": "cli.cjs" + } + }, "node_modules/safe-array-concat": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -2840,13 +2957,13 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -2876,30 +2993,32 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2927,14 +3046,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2946,34 +3069,51 @@ "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" }, "node_modules/string.prototype.matchall": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", - "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "regexp.prototype.flags": "^1.5.0", - "set-function-name": "^2.0.0", - "side-channel": "^1.0.4" + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2983,28 +3123,31 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3069,9 +3212,9 @@ "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, "node_modules/type-check": { "version": "0.4.0", @@ -3103,29 +3246,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -3135,16 +3279,17 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -3154,23 +3299,29 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3235,13 +3386,13 @@ } }, "node_modules/which-builtin-type": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", - "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", + "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", "dev": true, "dependencies": { - "function.prototype.name": "^1.1.5", - "has-tostringtag": "^1.0.0", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.0.5", "is-finalizationregistry": "^1.0.2", @@ -3250,8 +3401,8 @@ "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.9" + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -3261,31 +3412,34 @@ } }, "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3300,27 +3454,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "optional": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/js/package.json b/src/js/package.json index 949b6cf9..ef6aae27 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -8,22 +8,22 @@ "check": "prettier --check . && eslint" }, "devDependencies": { - "@rollup/plugin-commonjs": "^25.0.7", - "@rollup/plugin-node-resolve": "^15.2.3", - "@rollup/plugin-replace": "^5.0.5", + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-node-resolve": "^15.3.0", + "@rollup/plugin-replace": "^6.0.1", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "eslint": "^8.38.0", "eslint-plugin-react": "^7.32.2", - "prettier": "^3.2.3", - "rollup": "^4.9.5", - "typescript": "^5.3.3" + "prettier": "^3.3.3", + "rollup": "^4.24.0", + "typescript": "^5.6.3" }, "dependencies": { - "@pyscript/core": "^0.4.48", + "@pyscript/core": "^0.6.7", "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^11.1.6", - "morphdom": "^2.7.3", - "tslib": "^2.6.2" + "@rollup/plugin-typescript": "^12.1.1", + "morphdom": "^2.7.4", + "tslib": "^2.8.0" } } diff --git a/src/js/src/index.tsx b/src/js/src/index.tsx index 23300874..51a387f3 100644 --- a/src/js/src/index.tsx +++ b/src/js/src/index.tsx @@ -1,6 +1,6 @@ import { ReactPyDjangoClient } from "./client"; import React from "react"; -import { render } from "react-dom"; +import ReactDOM from "react-dom"; import { Layout } from "@reactpy/client/src/components"; export function mountComponent( @@ -77,5 +77,5 @@ export function mountComponent( } // Start rendering the component - render(, client.mountElement); + ReactDOM.render(, client.mountElement); } diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json index b73afab7..f1b84b50 100644 --- a/src/js/tsconfig.json +++ b/src/js/tsconfig.json @@ -4,10 +4,10 @@ "module": "esnext", "moduleResolution": "node", "jsx": "react", - "allowSyntheticDefaultImports": true, + "allowSyntheticDefaultImports": true }, "paths": { "react": ["./node_modules/preact/compat/"], - "react-dom": ["./node_modules/preact/compat/"], - }, + "react-dom": ["./node_modules/preact/compat/"] + } } From e09fa17c6ab97504190a90603fad60eedfaf5085 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Tue, 22 Oct 2024 21:29:57 -0700 Subject: [PATCH 12/35] Cleanup PyScript renderer & use `Bun` for building JS (#254) - Switch to `Bun` for building JS - Minor cleanup to pyscript renderer - Revert pyscript version to 0.5 due to [bugs](https://github.com/pyscript/pyscript/issues/2228) --- .editorconfig | 4 + .github/workflows/publish-develop-docs.yml | 3 + .github/workflows/publish-py.yml | 3 + .github/workflows/publish-release-docs.yml | 3 + .github/workflows/test-docs.yml | 3 + .github/workflows/test-src.yml | 3 + .../pyscript-setup-local-interpreter.html | 1 + docs/src/about/code.md | 1 + docs/src/reference/template-tag.md | 14 + noxfile.py | 10 +- pyproject.toml | 2 +- requirements/test-env.txt | 1 - setup.py | 45 +- src/js/bun.lockb | Bin 0 -> 101426 bytes src/js/eslint.config.js | 1 + src/js/package-lock.json | 3470 ----------------- src/js/package.json | 20 +- src/js/rollup.config.mjs | 23 - src/js/tsconfig.json | 13 - src/reactpy_django/components.py | 1 - src/reactpy_django/pyscript/layout_handler.py | 23 +- src/reactpy_django/templatetags/reactpy.py | 4 +- tests/test_app/__init__.py | 62 +- 23 files changed, 135 insertions(+), 3575 deletions(-) create mode 100644 docs/examples/html/pyscript-setup-local-interpreter.html create mode 100644 src/js/bun.lockb create mode 100644 src/js/eslint.config.js delete mode 100644 src/js/package-lock.json delete mode 100644 src/js/rollup.config.mjs delete mode 100644 src/js/tsconfig.json diff --git a/.editorconfig b/.editorconfig index 356385d7..170d7ddb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,10 +14,14 @@ end_of_line = lf indent_size = 4 max_line_length = 120 +[*.yml] +indent_size = 4 + [*.md] indent_size = 4 [*.html] +indent_size = 4 max_line_length = off [*.js] diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index c2b62d95..53c5aa16 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -11,6 +11,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-py.yml index 72a04dae..6a86db98 100644 --- a/.github/workflows/publish-py.yml +++ b/.github/workflows/publish-py.yml @@ -12,6 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.github/workflows/publish-release-docs.yml b/.github/workflows/publish-release-docs.yml index 3f24d129..93df3e2a 100644 --- a/.github/workflows/publish-release-docs.yml +++ b/.github/workflows/publish-release-docs.yml @@ -11,6 +11,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 3062ebc1..95d131c4 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -17,6 +17,9 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - uses: actions/setup-python@v5 with: python-version: 3.x diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-src.yml index 328bd1c3..5eb2e67a 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-src.yml @@ -18,6 +18,9 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest - name: Use Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: diff --git a/docs/examples/html/pyscript-setup-local-interpreter.html b/docs/examples/html/pyscript-setup-local-interpreter.html new file mode 100644 index 00000000..8371fa94 --- /dev/null +++ b/docs/examples/html/pyscript-setup-local-interpreter.html @@ -0,0 +1 @@ +{% pyscript_setup config='{"interpreter":"/static/pyodide/pyodide.mjs"}' %} diff --git a/docs/src/about/code.md b/docs/src/about/code.md index 205c2c96..81e49c51 100644 --- a/docs/src/about/code.md +++ b/docs/src/about/code.md @@ -19,6 +19,7 @@ If you plan to make code changes to this repository, you will need to install the following dependencies first: - [Python 3.9+](https://www.python.org/downloads/) +- [Bun](https://bun.sh/) - [Git](https://git-scm.com/downloads) Once done, you should clone this repository: diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index e5c60a79..091b2ac8 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -391,3 +391,17 @@ You can optionally use this tag to configure the current PyScript environment. F ```python {% include "../../examples/python/pyscript-setup-config-object.py" %} ``` + +??? question "Can I use a local interpreter for PyScript?" + + Yes, you can set up a local interpreter by following PyScript's [standard documentation](https://docs.pyscript.net/latest/user-guide/offline/#local-pyodide-packages). + + To summarize, + + 1. Download the latest Pyodide bundle from the [Pyodide GitHub releases page](https://github.com/pyodide/pyodide/releases) (for example `pyodide-0.26.3.tar.bz2`). + 2. Extract the contents of the bundle to your project's static files. + 3. Configure your `#!jinja {% pyscript_setup %}` template tag to use `pyodide` as an interpreter. + + ```jinja linenums="0" + {% include "../../examples/html/pyscript-setup-local-interpreter.html" %} + ``` diff --git a/noxfile.py b/noxfile.py index 7c228143..8776de45 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,14 +31,14 @@ def test_python(session: Session) -> None: settings_files = glob(settings_glob) assert settings_files, f"No Django settings files found at '{settings_glob}'!" for settings_file in settings_files: - settings_module = settings_file.strip(".py").replace("/", ".").replace("\\", ".") + settings_module = ( + settings_file.strip(".py").replace("/", ".").replace("\\", ".") + ) session.run( "python", "manage.py", "test", *posargs, - "-v", - "2", "--settings", settings_module, ) @@ -62,8 +62,8 @@ def test_style(session: Session) -> None: def test_javascript(session: Session) -> None: install_requirements_file(session, "test-env") session.chdir(ROOT_DIR / "src" / "js") - session.run("python", "-m", "nodejs.npm", "install", external=True) - session.run("python", "-m", "nodejs.npm", "run", "check") + session.run("bun", "install", external=True) + session.run("bun", "run", "check", external=True) def install_requirements_file(session: Session, name: str) -> None: diff --git a/pyproject.toml b/pyproject.toml index 274a352e..99ff6917 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=42", "wheel", "nodejs-bin==18.4.0a4"] +requires = ["setuptools>=42", "wheel"] build-backend = "setuptools.build_meta" [tool.mypy] diff --git a/requirements/test-env.txt b/requirements/test-env.txt index 6f146715..fc1ba2ce 100644 --- a/requirements/test-env.txt +++ b/requirements/test-env.txt @@ -3,4 +3,3 @@ twisted channels[daphne]>=4.0.0 tblib whitenoise -nodejs-bin==18.4.0a4 diff --git a/setup.py b/setup.py index 76a91edf..a3388b35 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,18 @@ from __future__ import annotations, print_function import shutil +import subprocess import sys import traceback -from distutils import log +from logging import getLogger from pathlib import Path -from nodejs import npm from setuptools import find_namespace_packages, setup from setuptools.command.develop import develop from setuptools.command.sdist import sdist +log = getLogger(__name__) + # ----------------------------------------------------------------------------- # Basic Constants # ----------------------------------------------------------------------------- @@ -97,19 +99,44 @@ # ---------------------------------------------------------------------------- # Build Javascript # ---------------------------------------------------------------------------- +def copy_js_files(source_dir: Path, destination: Path) -> None: + if destination.exists(): + shutil.rmtree(destination) + destination.mkdir() + + for file in source_dir.iterdir(): + if file.is_file(): + shutil.copy(file, destination / file.name) + else: + copy_js_files(file, destination / file.name) + + def build_javascript_first(build_cls: type): class Command(build_cls): def run(self): log.info("Installing Javascript...") - result = npm.call(["install"], cwd=str(js_dir)) + result = subprocess.run( + ["bun", "install"], cwd=str(js_dir), check=True + ).returncode if result != 0: log.error(traceback.format_exc()) log.error("Failed to install Javascript") raise RuntimeError("Failed to install Javascript") log.info("Building Javascript...") - result = npm.call(["run", "build"], cwd=str(js_dir)) + result = subprocess.run( + [ + "bun", + "build", + "./src/index.tsx", + "--outfile", + str(static_dir / "client.js"), + "--minify", + ], + cwd=str(js_dir), + check=True, + ).returncode if result != 0: log.error(traceback.format_exc()) log.error("Failed to build Javascript") @@ -118,18 +145,12 @@ def run(self): log.info("Copying @pyscript/core distribution") pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist" pyscript_static_dir = static_dir / "pyscript" - if not pyscript_static_dir.exists(): - pyscript_static_dir.mkdir() - for file in pyscript_dist.iterdir(): - shutil.copy(file, pyscript_static_dir / file.name) + copy_js_files(pyscript_dist, pyscript_static_dir) log.info("Copying Morphdom distribution") morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist" morphdom_static_dir = static_dir / "morphdom" - if not morphdom_static_dir.exists(): - morphdom_static_dir.mkdir() - for file in morphdom_dist.iterdir(): - shutil.copy(file, morphdom_static_dir / file.name) + copy_js_files(morphdom_dist, morphdom_static_dir) log.info("Successfully built Javascript") super().run() diff --git a/src/js/bun.lockb b/src/js/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..3807b5711eaf5836a8f91e41e3a15c31dc0c1370 GIT binary patch literal 101426 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 literal 0 HcmV?d00001 diff --git a/src/js/eslint.config.js b/src/js/eslint.config.js new file mode 100644 index 00000000..27082ef3 --- /dev/null +++ b/src/js/eslint.config.js @@ -0,0 +1 @@ +export default [{}]; diff --git a/src/js/package-lock.json b/src/js/package-lock.json deleted file mode 100644 index 0bdd2ca5..00000000 --- a/src/js/package-lock.json +++ /dev/null @@ -1,3470 +0,0 @@ -{ - "name": "js", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@pyscript/core": "^0.6.7", - "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^12.1.1", - "morphdom": "^2.7.4", - "tslib": "^2.8.0" - }, - "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-replace": "^6.0.1", - "@types/react": "^18.2.48", - "@types/react-dom": "^18.2.18", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "prettier": "^3.3.3", - "rollup": "^4.24.0", - "typescript": "^5.6.3" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", - "dev": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pyscript/core": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.6.7.tgz", - "integrity": "sha512-Ij9nrIAp49IkavzTYYwoV4K12MzzVCvcfmkGeqc9nPhtwBxPWSDJ9LEJtfO9MzoMV3dog5FwHdUh2WbZbqqZfw==", - "dependencies": { - "@ungap/with-resolvers": "^0.1.0", - "@webreflection/idb-map": "^0.3.2", - "add-promise-listener": "^0.1.3", - "basic-devtools": "^0.1.6", - "polyscript": "^0.16.3", - "sabayon": "^0.5.2", - "sticky-module": "^0.1.1", - "to-json-callback": "^0.1.1", - "type-checked-collections": "^0.1.7" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@reactpy/client": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@reactpy/client/-/client-0.3.1.tgz", - "integrity": "sha512-mvFwAvmRMgo7lTjkhkEJzBep6HX/wfm5BaNbtEMOUzto7G/h+z1AmqlOMXLH37DSI0iwfmCuNwy07EJM0JWZ0g==", - "dependencies": { - "event-to-object": "^0.1.2", - "json-pointer": "^0.6.2" - }, - "peerDependencies": { - "react": ">=16 <18", - "react-dom": ">=16 <18" - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz", - "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/fdir": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", - "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", - "dev": true, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", - "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-replace": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.1.tgz", - "integrity": "sha512-2sPh9b73dj5IxuMmDAsQWVFT7mR+yoHweBaXG2W/R8vQ+IWZlnaI7BR7J6EguVQUp1hd8Z7XuozpDjEKQAAC2Q==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "magic-string": "^0.30.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-typescript": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-12.1.1.tgz", - "integrity": "sha512-t7O653DpfB5MbFrqPe/VcKFFkvRuFNp9qId3xq4Eth5xlyymzxNpye2z8Hrl0RIMuXTSr5GGcFpkdlMeacUiFQ==", - "dependencies": { - "@rollup/pluginutils": "^5.1.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.14.0||^3.0.0||^4.0.0", - "tslib": "*", - "typescript": ">=3.7.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - }, - "tslib": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", - "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", - "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", - "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", - "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", - "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", - "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", - "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", - "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", - "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", - "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", - "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", - "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", - "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", - "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", - "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", - "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.11", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", - "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", - "dev": true - }, - "node_modules/@types/react": { - "version": "18.2.48", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.48.tgz", - "integrity": "sha512-qboRCl6Ie70DQQG9hhNREz81jqC1cs9EVNcjQ1AU+jH6NFfSAhVVbrrY/+nSF+Bsk4AOwm9Qa61InvMCyV+H3w==", - "dev": true, - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.2.18", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz", - "integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==", - "dev": true, - "dependencies": { - "@types/react": "*" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true - }, - "node_modules/@types/scheduler": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", - "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", - "dev": true - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@ungap/with-resolvers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", - "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" - }, - "node_modules/@webreflection/fetch": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", - "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" - }, - "node_modules/@webreflection/idb-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@webreflection/idb-map/-/idb-map-0.3.2.tgz", - "integrity": "sha512-VLBTx6EUYF/dPdLyyjWWKxQmTWnVXTT1YJekrJUmfGxBcqEVL0Ih2EQptNG/JezkTYgJ0uSTb0yAum/THltBvQ==" - }, - "node_modules/acorn": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", - "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/add-promise-listener": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/add-promise-listener/-/add-promise-listener-0.1.3.tgz", - "integrity": "sha512-hQ6IgGJ7NvvlPYbwdekhdVwPb4QzEptNZ5v7B4XRKz7FukUPDuF/v+R5EFHArWmhmq4d+xv0G4/B5bu2GSiz9Q==" - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/basic-devtools": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", - "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/codedent": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", - "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", - "dependencies": { - "plain-tag": "^0.1.3" - } - }, - "node_modules/coincident": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/coincident/-/coincident-2.2.3.tgz", - "integrity": "sha512-yxLzgZqDSEPOczzNoM4T3gVyg955Kbk5snkaAkQw8xvh8zfxGuUtbaOaDNCaP3HxWeRZZjwbJi80hapAikpvoQ==", - "dependencies": { - "gc-hook": "^0.4.1", - "js-proxy": "^0.5.1", - "sabayon": "^0.5.2" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true - }, - "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", - "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", - "integrity": "sha512-/SurEfycdyssORP/E+bj4sEu1CWw4EmLDsHynHwSXQ7utgbrMRWW195pTrCjFgFCddf/UkYm3oqKPRq5i8bJbw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.4", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "iterator.prototype": "^1.1.3", - "safe-array-concat": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.2.4", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", - "dev": true, - "dependencies": { - "hasown": "^2.0.0" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", - "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.2", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.8", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.0", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react/node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/event-to-object": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/event-to-object/-/event-to-object-0.1.2.tgz", - "integrity": "sha512-+fUmp1XOCZiYomwe5Zxp4IlchuZZfdVdjFUk5MbgRT4M+V2TEWKc0jJwKLCX/nxlJ6xM5VUb/ylzERh7YDCRrg==", - "dependencies": { - "json-pointer": "^0.6.2" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gc-hook": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.4.1.tgz", - "integrity": "sha512-uiF+uUftDVLr+VRdudsdsT3/LQYnv2ntwhRH964O7xXDI57Smrek5olv75Wb8Nnz6U+7iVTRXsBlxKcsaDTJTQ==" - }, - "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", - "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", - "dev": true, - "dependencies": { - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dev": true, - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/iterator.prototype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", - "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", - "dev": true, - "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/js-proxy": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/js-proxy/-/js-proxy-0.5.1.tgz", - "integrity": "sha512-G1AswnGndelrmZ2tuJi5NWlXo28BucJdgO8aKP5U1NkxWxPvgFA510Ku6at+1A17Kh2ja7A/r4RDtd9Hdr6sOw==", - "dependencies": { - "gc-hook": "^0.3.1", - "proxy-target": "^3.0.2" - } - }, - "node_modules/js-proxy/node_modules/gc-hook": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", - "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true - }, - "node_modules/json-pointer": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", - "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", - "dependencies": { - "foreach": "^2.0.4" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/magic-string": { - "version": "0.30.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", - "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/morphdom": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.4.tgz", - "integrity": "sha512-ATTbWMgGa+FaMU3FhnFYB6WgulCqwf6opOll4CBzmVDTLvPMmUPrEv8CudmLPK0MESa64+6B89fWOxP3+YIlxQ==" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", - "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/plain-tag": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", - "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" - }, - "node_modules/polyscript": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.16.3.tgz", - "integrity": "sha512-I3kHxt62FMRAX2iVl24iCEtG4UnUInMSbv/LnwevkmjOErLPAQtES4CNzU/fgKRpXYCqp0WWQaRvRYkJhpMIbA==", - "dependencies": { - "@ungap/structured-clone": "^1.2.0", - "@ungap/with-resolvers": "^0.1.0", - "@webreflection/fetch": "^0.1.5", - "@webreflection/idb-map": "^0.3.2", - "basic-devtools": "^0.1.6", - "codedent": "^0.1.2", - "coincident": "^2.2.2", - "gc-hook": "^0.4.1", - "html-escaper": "^3.0.3", - "proxy-target": "^3.0.2", - "sticky-module": "^0.1.1", - "to-json-callback": "^0.1.1" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", - "dev": true, - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/proxy-target": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", - "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.24.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", - "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", - "devOptional": true, - "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.24.0", - "@rollup/rollup-android-arm64": "4.24.0", - "@rollup/rollup-darwin-arm64": "4.24.0", - "@rollup/rollup-darwin-x64": "4.24.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", - "@rollup/rollup-linux-arm-musleabihf": "4.24.0", - "@rollup/rollup-linux-arm64-gnu": "4.24.0", - "@rollup/rollup-linux-arm64-musl": "4.24.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", - "@rollup/rollup-linux-riscv64-gnu": "4.24.0", - "@rollup/rollup-linux-s390x-gnu": "4.24.0", - "@rollup/rollup-linux-x64-gnu": "4.24.0", - "@rollup/rollup-linux-x64-musl": "4.24.0", - "@rollup/rollup-win32-arm64-msvc": "4.24.0", - "@rollup/rollup-win32-ia32-msvc": "4.24.0", - "@rollup/rollup-win32-x64-msvc": "4.24.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sabayon": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/sabayon/-/sabayon-0.5.2.tgz", - "integrity": "sha512-7y8dwQFhInkUuI0JL1rAGLYeKz9LypgfzMOVO04v9pYvepzYJ6B7MhyoRiVIx2sGm7MhrD5h/SaHP6p+5Uc7Cw==", - "bin": { - "sabayon": "cli.cjs" - } - }, - "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.6", - "es-errors": "^1.3.0", - "is-regex": "^1.1.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sticky-module": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", - "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", - "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.7", - "regexp.prototype.flags": "^1.5.2", - "set-function-name": "^2.0.2", - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/to-json-callback": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", - "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" - }, - "node_modules/tslib": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", - "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-checked-collections": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", - "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", - "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", - "dev": true, - "dependencies": { - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", - "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dev": true, - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/src/js/package.json b/src/js/package.json index ef6aae27..7f6cc019 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -1,29 +1,19 @@ { - "description": "ReactPy-Django Client", - "main": "src/index.tsx", "type": "module", "scripts": { - "build": "rollup --config", "format": "prettier --write . && eslint --fix", "check": "prettier --check . && eslint" }, "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-node-resolve": "^15.3.0", - "@rollup/plugin-replace": "^6.0.1", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", - "eslint": "^8.38.0", - "eslint-plugin-react": "^7.32.2", - "prettier": "^3.3.3", - "rollup": "^4.24.0", - "typescript": "^5.6.3" + "eslint": "^9.13.0", + "eslint-plugin-react": "^7.37.1", + "prettier": "^3.3.3" }, "dependencies": { - "@pyscript/core": "^0.6.7", + "@pyscript/core": "^0.5", "@reactpy/client": "^0.3.1", - "@rollup/plugin-typescript": "^12.1.1", - "morphdom": "^2.7.4", - "tslib": "^2.8.0" + "morphdom": "^2.7.4" } } diff --git a/src/js/rollup.config.mjs b/src/js/rollup.config.mjs deleted file mode 100644 index 5a6de090..00000000 --- a/src/js/rollup.config.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import resolve from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; -import replace from "@rollup/plugin-replace"; -import typescript from "@rollup/plugin-typescript"; - -export default { - input: "src/index.tsx", - output: { - file: "../reactpy_django/static/reactpy_django/client.js", - format: "esm", - }, - plugins: [ - resolve(), - commonjs(), - replace({ - "process.env.NODE_ENV": JSON.stringify("production"), - }), - typescript(), - ], - onwarn: function (warning) { - console.warn(warning.message); - }, -}; diff --git a/src/js/tsconfig.json b/src/js/tsconfig.json deleted file mode 100644 index f1b84b50..00000000 --- a/src/js/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "esnext", - "moduleResolution": "node", - "jsx": "react", - "allowSyntheticDefaultImports": true - }, - "paths": { - "react": ["./node_modules/preact/compat/"], - "react-dom": ["./node_modules/preact/compat/"] - } -} diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 3794ba73..98981730 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -215,7 +215,6 @@ def _view_to_iframe( { "src": reverse("reactpy:view_to_iframe", args=[dotted_path]) + query_string, "style": {"border": "none"}, - "onload": 'javascript:(function(o){o.style.height=o.contentWindow.document.body.scrollHeight+"px";}(this));', "loading": "lazy", } | extra_props diff --git a/src/reactpy_django/pyscript/layout_handler.py b/src/reactpy_django/pyscript/layout_handler.py index da5bfb1b..479cb613 100644 --- a/src/reactpy_django/pyscript/layout_handler.py +++ b/src/reactpy_django/pyscript/layout_handler.py @@ -14,7 +14,7 @@ def __init__(self, uuid): self.uuid = uuid @staticmethod - def apply_update(update, root_model): + def update_model(update, root_model): """Apply an update ReactPy's internal DOM model.""" from jsonpointer import set_pointer @@ -23,21 +23,22 @@ def apply_update(update, root_model): else: root_model.update(update["model"]) - def render(self, layout, model): + def render_html(self, layout, model): """Submit ReactPy's internal DOM model into the HTML DOM.""" - import js from pyscript.js_modules import morphdom + import js + # Create a new container to render the layout into container = js.document.getElementById(f"pyscript-{self.uuid}") - temp_container = container.cloneNode(False) - self.build_element_tree(layout, temp_container, model) + temp_root_container = container.cloneNode(False) + self.build_element_tree(layout, temp_root_container, model) # Use morphdom to update the DOM - morphdom.default(container, temp_container) + morphdom.default(container, temp_root_container) # Remove the cloned container to prevent memory leaks - temp_container.remove() + temp_root_container.remove() def build_element_tree(self, layout, parent, model): """Recursively build an element tree, starting from the root component.""" @@ -131,8 +132,8 @@ async def run(self, workspace_function): self.delete_old_workspaces() root_model: dict = {} - async with Layout(workspace_function()) as layout: + async with Layout(workspace_function()) as root_layout: while True: - update = await layout.render() - self.apply_update(update, root_model) - self.render(layout, root_model) + update = await root_layout.render() + self.update_model(update, root_model) + self.render_html(root_layout, root_model) diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 1fdfa6af..2f34651a 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -226,11 +226,11 @@ def pyscript_setup( extra_py: Dependencies that need to be loaded on the page for \ your PyScript components. Each dependency must be contained \ within it's own string and written in Python requirements file syntax. - + Kwargs: extra_js: A JSON string or Python dictionary containing a vanilla \ JavaScript module URL and the `name: str` to access it within \ - `pyscript.js_modules.*`. + `pyscript.js_modules.*`. config: A JSON string or Python dictionary containing PyScript \ configuration values. """ diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py index 392e6066..27d5e41d 100644 --- a/tests/test_app/__init__.py +++ b/tests/test_app/__init__.py @@ -1,39 +1,59 @@ import shutil +import subprocess from pathlib import Path -from nodejs import npm - # Make sure the JS is always re-built before running the tests js_dir = Path(__file__).parent.parent.parent / "src" / "js" -assert npm.call(["install"], cwd=str(js_dir)) == 0 -assert npm.call(["run", "build"], cwd=str(js_dir)) == 0 +static_dir = ( + Path(__file__).parent.parent.parent + / "src" + / "reactpy_django" + / "static" + / "reactpy_django" +) +assert subprocess.run(["bun", "install"], cwd=str(js_dir), check=True).returncode == 0 +assert ( + subprocess.run( + ["bun", "build", "./src/index.tsx", "--outfile", str(static_dir / "client.js")], + cwd=str(js_dir), + check=True, + ).returncode + == 0 +) + + +# Make sure the test environment is always using the latest JS +def copy_js_files(source_dir: Path, destination: Path) -> None: + if destination.exists(): + shutil.rmtree(destination) + destination.mkdir() + + for file in source_dir.iterdir(): + if file.is_file(): + shutil.copy(file, destination / file.name) + else: + copy_js_files(file, destination / file.name) -# Make sure the current PyScript distribution is always available -pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist" -pyscript_static_dir = ( + +# Copy PyScript +copy_js_files( + js_dir / "node_modules" / "@pyscript" / "core" / "dist", Path(__file__).parent.parent.parent / "src" / "reactpy_django" / "static" / "reactpy_django" - / "pyscript" + / "pyscript", ) -if not pyscript_static_dir.exists(): - pyscript_static_dir.mkdir() -for file in pyscript_dist.iterdir(): - shutil.copy(file, pyscript_static_dir / file.name) - -# Make sure the current Morphdom distrubiton is always available -morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist" -morphdom_static_dir = ( + + +# Copy MorphDOM +copy_js_files( + js_dir / "node_modules" / "morphdom" / "dist", Path(__file__).parent.parent.parent / "src" / "reactpy_django" / "static" / "reactpy_django" - / "morphdom" + / "morphdom", ) -if not morphdom_static_dir.exists(): - morphdom_static_dir.mkdir() -for file in morphdom_dist.iterdir(): - shutil.copy(file, morphdom_static_dir / file.name) From 95f8abf4b84e0ce4b5303afbbab5dadc23d52f70 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Tue, 22 Oct 2024 21:56:02 -0700 Subject: [PATCH 13/35] 5.0.0 (#255) --- .github/workflows/test-docs.yml | 12 ++++++------ CHANGELOG.md | 7 ++++++- src/reactpy_django/__init__.py | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 95d131c4..66a5c942 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -23,12 +23,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - name: Check docs links - uses: umbrelladocs/action-linkspector@v1 - with: - github_token: ${{ secrets.github_token }} - reporter: github-pr-review - fail_on_error: true + # - name: Check docs links + # uses: umbrelladocs/action-linkspector@v1 + # with: + # github_token: ${{ secrets.github_token }} + # reporter: github-pr-review + # fail_on_error: false - name: Check docs build run: | pip install -r requirements/build-docs.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 6680348a..5342e003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,10 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] +- Nothing (yet)! + +## [5.0.0] - 2024-10-22 + ### Changed - Now using ReactPy-Router v1 for URL routing, which comes with a slightly different API than before. @@ -515,7 +519,8 @@ Don't forget to remove deprecated code on each major release! - Support for IDOM within the Django -[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.0.0...HEAD +[5.0.0]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...5.0.0 [4.0.0]: https://github.com/reactive-python/reactpy-django/compare/3.8.1...4.0.0 [3.8.1]: https://github.com/reactive-python/reactpy-django/compare/3.8.0...3.8.1 [3.8.0]: https://github.com/reactive-python/reactpy-django/compare/3.7.0...3.8.0 diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index 77b56743..806428de 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -13,7 +13,7 @@ ) from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_ROUTE -__version__ = "4.0.0" +__version__ = "5.0.0" __all__ = [ "REACTPY_WEBSOCKET_ROUTE", "html", From 5891b414665680748beb4bce34fe3a27c00449aa Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sun, 24 Nov 2024 20:39:19 -0800 Subject: [PATCH 14/35] Bump ReactPy to v1.1.0 (#258) --- ...lease-docs.yml => publish-latest-docs.yml} | 2 +- CHANGELOG.md | 27 ++++++------------ README.md | 2 +- docs/src/reference/components.md | 2 +- docs/src/reference/router.md | 4 +-- docs/src/reference/settings.md | 14 ++++++++- requirements/pkg-deps.txt | 2 +- setup.py | 6 ++-- src/js/bun.lockb | Bin 101426 -> 102513 bytes src/js/package.json | 2 +- src/reactpy_django/config.py | 6 +++- src/reactpy_django/decorators.py | 8 ++---- src/reactpy_django/hooks.py | 6 ++-- src/reactpy_django/http/views.py | 2 ++ src/reactpy_django/pyscript/layout_handler.py | 2 +- src/reactpy_django/utils.py | 2 +- 16 files changed, 46 insertions(+), 41 deletions(-) rename .github/workflows/{publish-release-docs.yml => publish-latest-docs.yml} (96%) 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 3807b5711eaf5836a8f91e41e3a15c31dc0c1370..142d0d8330711681d02299206f06f9d3b3333663 100644 GIT binary patch delta 19389 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 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__}" From 2abe5ce89e2e77579f1e364ddb148b569b98cb49 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sun, 24 Nov 2024 23:11:49 -0800 Subject: [PATCH 15/35] v5.1.0 (#259) --- CHANGELOG.md | 7 ++++++- src/reactpy_django/__init__.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4719dd02..82209551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] +- Nothing (yet)! + +## [5.1.0] - 2024-11-24 + ### Added - `settings.py:REACTPY_ASYNC_RENDERING` to enable asynchronous rendering of components. @@ -508,7 +512,8 @@ Don't forget to remove deprecated code on each major release! - Support for IDOM within the Django -[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.0.0...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.1.0...HEAD +[5.1.0]: https://github.com/reactive-python/reactpy-django/compare/5.0.0...5.1.0 [5.0.0]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...5.0.0 [4.0.0]: https://github.com/reactive-python/reactpy-django/compare/3.8.1...4.0.0 [3.8.1]: https://github.com/reactive-python/reactpy-django/compare/3.8.0...3.8.1 diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index 806428de..f3fb1545 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -13,7 +13,7 @@ ) from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_ROUTE -__version__ = "5.0.0" +__version__ = "5.1.0" __all__ = [ "REACTPY_WEBSOCKET_ROUTE", "html", From c5b5c81854c2dd0e59f325a0a708ae664c768c76 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Sun, 1 Dec 2024 01:07:41 -0800 Subject: [PATCH 16/35] Switch from Nox to Hatch (#260) --- .github/workflows/publish-develop-docs.yml | 8 +- .github/workflows/publish-latest-docs.yml | 8 +- .github/workflows/publish-py.yml | 8 +- .github/workflows/test-docs.yml | 24 +- .github/workflows/test-javascript.yml | 25 + .../{test-src.yml => test-python.yml} | 10 +- .linkspector.yml | 7 - CHANGELOG.md | 8 +- MANIFEST.in | 3 - docs/mkdocs.yml | 4 +- docs/src/about/code.md | 85 -- docs/src/about/contributing.md | 93 +++ docs/src/about/docs.md | 45 -- docs/src/dictionary.txt | 5 + docs/src/reference/settings.md | 2 +- noxfile.py | 73 -- pyproject.toml | 220 +++++- requirements.txt | 8 - requirements/build-docs.txt | 8 - requirements/build-pkg.txt | 3 - requirements/check-style.txt | 1 - requirements/check-types.txt | 3 - requirements/dev-env.txt | 3 - requirements/pkg-deps.txt | 8 - requirements/test-env.txt | 5 - requirements/test-run.txt | 1 - setup.py | 182 ----- src/build_scripts/copy_dir.py | 31 + src/js/eslint.config.js | 1 - src/js/eslint.config.mjs | 43 ++ src/reactpy_django/clean.py | 9 +- src/reactpy_django/config.py | 4 +- .../templates/reactpy/component.html | 28 +- .../templates/reactpy/pyscript_setup.html | 2 +- src/reactpy_django/templatetags/reactpy.py | 14 +- tests/test_app/apps.py | 8 - tests/test_app/middleware.py | 31 + tests/test_app/prerender/components.py | 9 +- tests/test_app/settings_multi_db.py | 45 +- tests/test_app/settings_single_db.py | 26 +- tests/test_app/tests/__init__.py | 1 - tests/test_app/tests/conftest.py | 19 + tests/test_app/tests/test_components.py | 724 ++++++++---------- tests/test_app/tests/test_database.py | 1 + tests/test_app/tests/test_regex.py | 17 +- tests/test_app/tests/utils.py | 92 +++ 46 files changed, 985 insertions(+), 970 deletions(-) create mode 100644 .github/workflows/test-javascript.yml rename .github/workflows/{test-src.yml => test-python.yml} (66%) delete mode 100644 .linkspector.yml delete mode 100644 MANIFEST.in delete mode 100644 docs/src/about/code.md create mode 100644 docs/src/about/contributing.md delete mode 100644 docs/src/about/docs.md delete mode 100644 noxfile.py delete mode 100644 requirements.txt delete mode 100644 requirements/build-docs.txt delete mode 100644 requirements/build-pkg.txt delete mode 100644 requirements/check-style.txt delete mode 100644 requirements/check-types.txt delete mode 100644 requirements/dev-env.txt delete mode 100644 requirements/pkg-deps.txt delete mode 100644 requirements/test-env.txt delete mode 100644 requirements/test-run.txt delete mode 100644 setup.py create mode 100644 src/build_scripts/copy_dir.py delete mode 100644 src/js/eslint.config.js create mode 100644 src/js/eslint.config.mjs create mode 100644 tests/test_app/middleware.py create mode 100644 tests/test_app/tests/conftest.py create mode 100644 tests/test_app/tests/utils.py diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index 53c5aa16..11a7fa23 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -5,7 +5,7 @@ on: branches: - main jobs: - deploy: + publish-develop-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,12 +17,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install -r requirements/build-docs.txt + - name: Install dependencies + run: pip install --upgrade pip hatch uv - name: Publish Develop Docs run: | git config user.name github-actions git config user.email github-actions@github.com - cd docs - mike deploy --push develop + hatch run docs:deploy_develop concurrency: group: publish-docs diff --git a/.github/workflows/publish-latest-docs.yml b/.github/workflows/publish-latest-docs.yml index bc7409f0..697b10da 100644 --- a/.github/workflows/publish-latest-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -5,7 +5,7 @@ on: types: [published] jobs: - deploy: + publish-latest-docs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -17,12 +17,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - - run: pip install -r requirements/build-docs.txt + - name: Install dependencies + run: pip install --upgrade pip hatch uv - name: Publish ${{ github.event.release.name }} Docs run: | git config user.name github-actions git config user.email github-actions@github.com - cd docs - mike deploy --push --update-aliases ${{ github.event.release.name }} latest + hatch run docs:deploy_latest ${{ github.ref_name }} concurrency: group: publish-docs diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-py.yml index 6a86db98..f72cc55d 100644 --- a/.github/workflows/publish-py.yml +++ b/.github/workflows/publish-py.yml @@ -8,7 +8,7 @@ on: types: [published] jobs: - release-package: + publish-python: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -20,13 +20,11 @@ jobs: with: python-version: "3.x" - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements/build-pkg.txt + run: pip install --upgrade pip hatch uv - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python -m build --sdist --wheel --outdir dist . + hatch build --clean twine upload dist/* diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 66a5c942..08bfadd7 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -7,8 +7,6 @@ on: pull_request: branches: - main - schedule: - - cron: "0 0 * * *" jobs: docs: @@ -23,20 +21,12 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.x - # - name: Check docs links - # uses: umbrelladocs/action-linkspector@v1 - # with: - # github_token: ${{ secrets.github_token }} - # reporter: github-pr-review - # fail_on_error: false + - name: Install Python Dependencies + run: pip install --upgrade pip hatch uv + # DISABLED DUE TO DJANGO DOCS CONSTANTLY THROWING 429 ERRORS + # - name: Check documentation links + # run: hatch run docs:linkcheck - name: Check docs build - run: | - pip install -r requirements/build-docs.txt - cd docs - mkdocs build --strict + run: hatch run docs:build - name: Check docs examples - run: | - pip install -r requirements/check-types.txt - pip install -r requirements/check-style.txt - mypy --show-error-codes docs/examples/python/ - ruff check docs/examples/python/ + run: hatch run docs:check_examples diff --git a/.github/workflows/test-javascript.yml b/.github/workflows/test-javascript.yml new file mode 100644 index 00000000..d5b9db1d --- /dev/null +++ b/.github/workflows/test-javascript.yml @@ -0,0 +1,25 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + javascript: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install --upgrade pip hatch uv + - name: Run Tests + run: hatch run javascript:check diff --git a/.github/workflows/test-src.yml b/.github/workflows/test-python.yml similarity index 66% rename from .github/workflows/test-src.yml rename to .github/workflows/test-python.yml index 5eb2e67a..9fe700b8 100644 --- a/.github/workflows/test-src.yml +++ b/.github/workflows/test-python.yml @@ -11,7 +11,7 @@ on: - cron: "0 0 * * *" jobs: - source: + python: runs-on: ubuntu-latest strategy: matrix: @@ -26,6 +26,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install Python Dependencies - run: pip install -r requirements/test-run.txt - - name: Run Tests - run: nox -t test + run: pip install --upgrade pip hatch uv + - name: Run Single DB Tests + run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_single_db -v + - name: Run Multi-DB Tests + run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_multi_db -v diff --git a/.linkspector.yml b/.linkspector.yml deleted file mode 100644 index 6c0747e7..00000000 --- a/.linkspector.yml +++ /dev/null @@ -1,7 +0,0 @@ -dirs: - - ./docs -files: - - README.md - - CHANGELOG.md -useGitIgnore: true -modifiedFilesOnly: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 82209551..ec8d38ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,13 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] -- Nothing (yet)! +### Fixed + +- Fixed regression in v5.1.0 where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. + +### Changed + +- Set upper limit on ReactPy version to `<2.0.0`. ## [5.1.0] - 2024-11-24 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index ddcb7f8d..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include src/reactpy_django/py.typed -recursive-include src/reactpy_django/static * -recursive-include src/reactpy_django/templates *.html diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index c1b5922f..100b669b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -16,9 +16,7 @@ nav: - Management Commands: reference/management-commands.md - About: - Changelog: about/changelog.md - - Contributor Guide: - - Code: about/code.md - - Docs: about/docs.md + - Contributor Guide: about/contributing.md - Community: - GitHub Discussions: https://github.com/reactive-python/reactpy-django/discussions - Discord: https://discord.gg/uNb5P4hA9X diff --git a/docs/src/about/code.md b/docs/src/about/code.md deleted file mode 100644 index 81e49c51..00000000 --- a/docs/src/about/code.md +++ /dev/null @@ -1,85 +0,0 @@ -## Overview - -

- - You will need to set up a Python environment to develop ReactPy-Django. - -

- -!!! abstract "Note" - - Looking to contribute features that are not Django specific? - - Everything within the `reactpy-django` repository must be specific to Django integration. Check out the [ReactPy Core documentation](https://reactpy.dev/docs/about/contributor-guide.html) to contribute general features such as components, hooks, and events. - ---- - -## Creating an environment - -If you plan to make code changes to this repository, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Bun](https://bun.sh/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -``` - -Then, by running the command below you can install the dependencies needed to run the ReactPy-Django development environment. - -```bash linenums="0" -pip install -r requirements.txt --upgrade --verbose -``` - -!!! warning "Pitfall" - - Some of our development dependencies require a C++ compiler, which is not installed by default on Windows. If you receive errors related to this during installation, follow the instructions in your console errors. - - Additionally, be aware that ReactPy-Django's JavaScript bundle is built within the following scenarios: - - 1. When `pip install` is run on the `reactpy-django` package. - 2. Every time `python manage.py ...` or `nox ...` is run - -## Running the full test suite - -!!! abstract "Note" - - This repository uses [Nox](https://nox.thea.codes/en/stable/) to run tests. For a full test of available scripts run `nox -l`. - -By running the command below you can run the full test suite: - -```bash linenums="0" -nox -t test -``` - -Or, if you want to run the tests in the background: - -```bash linenums="0" -nox -t test -- --headless -``` - -## Running Django tests - -If you want to only run our Django tests in your current environment, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py test -``` - -## Running Django test web server - -If you want to manually run the Django test application, you can use the following command: - -```bash linenums="0" -cd tests -python manage.py runserver -``` - -## Creating a pull request - -{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md new file mode 100644 index 00000000..ecb0131b --- /dev/null +++ b/docs/src/about/contributing.md @@ -0,0 +1,93 @@ +## Overview + +

+ + You will need to set up a Python environment to develop ReactPy-Django. + +

+ +!!! abstract "Note" + + Looking to contribute features that are not Django specific? + + Everything within the `reactpy-django` repository must be specific to Django integration. Check out the [ReactPy Core documentation](https://reactpy.dev/docs/about/contributor-guide.html) to contribute general features such as components, hooks, and events. + +--- + +## Creating a development environment + +If you plan to make code changes to this repository, you will need to install the following dependencies first: + +- [Git](https://git-scm.com/downloads) +- [Python 3.9+](https://www.python.org/downloads/) +- [Hatch](https://hatch.pypa.io/latest/) +- [Bun](https://bun.sh/) + +Once you finish installing these dependencies, you can clone this repository: + +```bash linenums="0" +git clone https://github.com/reactive-python/reactpy-django.git +cd reactpy-django +``` + +## Executing test environment commands + +By utilizing `hatch`, the following commands are available to manage the development environment. + +### Tests + +| Command | Description | +| --- | --- | +| `hatch test` | Run Python tests using the current environment's Python version | +| `hatch test --all` | Run tests using all compatible Python versions | +| `hatch test --python 3.9` | Run tests using a specific Python version | +| `hatch test --include "django=5.1"` | Run tests using a specific Django version | +| `hatch test -k test_object_in_templatetag` | Run only a specific test | +| `hatch test --ds test_app.settings_multi_db` | Run tests with a specific Django settings file | +| `hatch run django:runserver` | Manually run the Django development server without running tests | + +??? question "What other arguments are available to me?" + + The `hatch test` command is a wrapper for `pytest`. Hatch "intercepts" a handful of arguments, which can be previewed by typing `hatch test --help`. + + Any additional arguments in the `test` command are directly passed on to pytest. See the [pytest documentation](https://docs.pytest.org/en/stable/reference/reference.html#command-line-flags) for what additional arguments are available. + +### Linting and Formatting + +| Command | Description | +| --- | --- | +| `hatch fmt` | Run all linters and formatters | +| `hatch fmt --check` | Run all linters and formatters, but do not save fixes to the disk | +| `hatch fmt --linter` | Run only linters | +| `hatch fmt --formatter` | Run only formatters | +| `hatch run javascript:check` | Run the JavaScript linter/formatter | +| `hatch run javascript:fix` | Run the JavaScript linter/formatter and write fixes to disk | + +??? tip "Configure your IDE for linting" + + This repository uses `hatch fmt` for linting and formatting, which is a [modestly customized](https://hatch.pypa.io/latest/config/internal/static-analysis/#default-settings) version of [`ruff`](https://github.com/astral-sh/ruff). + + You can install `ruff` as a plugin to your preferred code editor to create a similar environment. + +### Documentation + +| Command | Description | +| --- | --- | +| `hatch run docs:serve` | Start the [`mkdocs`](https://www.mkdocs.org/) server to view documentation locally | +| `hatch run docs:build` | Build the documentation | +| `hatch run docs:linkcheck` | Check for broken links in the documentation | +| `hatch run docs:check_examples` | Run linter on code examples in the documentation | + +### Environment Management + +| Command | Description | +| --- | --- | +| `hatch build --clean` | Build the package from source | +| `hatch env prune` | Delete all virtual environments created by `hatch` | +| `hatch python install 3.12` | Install a specific Python version to your system | + +??? tip "Check out Hatch for all available commands!" + + This documentation only covers commonly used commands. + + You can type `hatch --help` to see all available commands. diff --git a/docs/src/about/docs.md b/docs/src/about/docs.md deleted file mode 100644 index 712570ec..00000000 --- a/docs/src/about/docs.md +++ /dev/null @@ -1,45 +0,0 @@ -## Overview - -

- -You will need to set up a Python environment to create, test, and preview docs changes. - -

- ---- - -## Modifying Docs - -If you plan to make changes to this documentation, you will need to install the following dependencies first: - -- [Python 3.9+](https://www.python.org/downloads/) -- [Git](https://git-scm.com/downloads) - -Once done, you should clone this repository: - -```bash linenums="0" -git clone https://github.com/reactive-python/reactpy-django.git -cd reactpy-django -``` - -Then, by running the command below you can: - -- Install an editable version of the documentation -- Self-host a test server for the documentation - -```bash linenums="0" -pip install -r requirements.txt --upgrade -``` - -Finally, to verify that everything is working properly, you can manually run the docs preview web server. - -```bash linenums="0" -cd docs -mkdocs serve -``` - -Navigate to [`http://127.0.0.1:8000`](http://127.0.0.1:8000) to view a preview of the documentation. - -## GitHub Pull Request - -{% include-markdown "../../includes/pr.md" %} diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 14aa7a61..1b4ce080 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -43,3 +43,8 @@ unstyled WebSocket WebSockets whitespace +pytest +linter +linters +linting +formatters diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index e65dd203..23760919 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -117,7 +117,7 @@ We recommend using [`redis`](https://docs.djangoproject.com/en/stable/topics/cac Configures whether ReactPy components are rendered in a dedicated thread. -This allows the web server to process other traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). +This allows the web server to process other traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://github.com/pgjones/hypercorn) and [`uvicorn`](https://www.uvicorn.org/). This setting is incompatible with [`daphne`](https://github.com/django/daphne). diff --git a/noxfile.py b/noxfile.py deleted file mode 100644 index 8776de45..00000000 --- a/noxfile.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -from glob import glob -from pathlib import Path - -from nox import Session, session - -ROOT_DIR = Path(__file__).parent - - -@session(tags=["test"]) -def test_python(session: Session) -> None: - """Run the Python-based test suite""" - install_requirements_file(session, "test-env") - session.install(".[all]") - session.chdir(ROOT_DIR / "tests") - session.env["REACTPY_DEBUG_MODE"] = "1" - - posargs = session.posargs[:] - if "--headless" in posargs: - posargs.remove("--headless") - session.env["PLAYWRIGHT_HEADLESS"] = "1" - - if "--no-debug-mode" not in posargs: - posargs.append("--debug-mode") - - session.run("playwright", "install", "chromium") - - # Run tests for each settings file (tests/test_app/settings_*.py) - settings_glob = "test_app/settings_*.py" - settings_files = glob(settings_glob) - assert settings_files, f"No Django settings files found at '{settings_glob}'!" - for settings_file in settings_files: - settings_module = ( - settings_file.strip(".py").replace("/", ".").replace("\\", ".") - ) - session.run( - "python", - "manage.py", - "test", - *posargs, - "--settings", - settings_module, - ) - - -@session(tags=["test"]) -def test_types(session: Session) -> None: - install_requirements_file(session, "check-types") - install_requirements_file(session, "pkg-deps") - session.run("mypy", "--show-error-codes", "src/reactpy_django", "tests/test_app") - - -@session(tags=["test"]) -def test_style(session: Session) -> None: - """Check that style guidelines are being followed""" - install_requirements_file(session, "check-style") - session.run("ruff", "check", ".") - - -@session(tags=["test"]) -def test_javascript(session: Session) -> None: - install_requirements_file(session, "test-env") - session.chdir(ROOT_DIR / "src" / "js") - session.run("bun", "install", external=True) - session.run("bun", "run", "check", external=True) - - -def install_requirements_file(session: Session, name: str) -> None: - session.install("--upgrade", "pip", "setuptools", "wheel") - file_path = ROOT_DIR / "requirements" / f"{name}.txt" - assert file_path.exists(), f"requirements file {file_path} does not exist" - session.install("-r", str(file_path)) diff --git a/pyproject.toml b/pyproject.toml index 99ff6917..44f920a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,21 +1,215 @@ [build-system] -requires = ["setuptools>=42", "wheel"] -build-backend = "setuptools.build_meta" +build-backend = "hatchling.build" +requires = ["hatchling", "hatch-build-scripts"] -[tool.mypy] -exclude = ['migrations/.*'] -ignore_missing_imports = true -warn_unused_configs = true -warn_redundant_casts = true -warn_unused_ignores = true -check_untyped_defs = true +############################## +# >>> Hatch Build Config <<< # +############################## -[tool.ruff.lint.isort] -known-first-party = ["src", "tests"] +[project] +name = "reactpy_django" +description = "It's React, but in Python. Now with Django integration." +readme = "README.md" +keywords = [ + "React", + "ReactJS", + "ReactPy", + "components", + "asgi", + "django", + "http", + "server", + "reactive", + "interactive", +] +license = "MIT" +authors = [{ name = "Mark Bakhit", email = "archiethemonger@gmail.com" }] +requires-python = ">=3.9" +classifiers = [ + "Framework :: Django", + "Framework :: Django :: 4.0", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "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", +] +dependencies = [ + "channels>=4.0.0", + "django>=4.2.0", + "reactpy>=1.1.0, <2.0.0", + "reactpy-router>=1.0.3, <2.0.0", + "dill>=0.3.5", + "orjson>=3.6.0", + "nest_asyncio>=1.5.0", + "typing_extensions", +] +dynamic = ["version"] +urls.Changelog = "https://reactive-python.github.io/reactpy-django/latest/about/changelog/" +urls.Documentation = "https://reactive-python.github.io/reactpy-django/" +urls.Source = "https://github.com/reactive-python/reactpy-django" -[tool.ruff.lint] -ignore = ["E501"] +[tool.hatch.version] +path = "src/reactpy_django/__init__.py" + +[tool.hatch.build.targets.sdist] +include = ["/src"] +artifacts = ["/src/reactpy_django/static/"] + +[tool.hatch.build.targets.wheel] +artifacts = ["/src/reactpy_django/static/"] + +[tool.hatch.metadata] +license-files = { paths = ["LICENSE.md"] } + +[tool.hatch.envs.default] +installer = "uv" + +[[tool.hatch.build.hooks.build-scripts.scripts]] +commands = [ + "bun install --cwd src/js", + "bun build src/js/src/index.tsx --outfile src/reactpy_django/static/reactpy_django/client.js --minify", + 'cd src/build_scripts && python copy_dir.py "src/js/node_modules/@pyscript/core/dist" "src/reactpy_django/static/reactpy_django/pyscript"', + 'cd src/build_scripts && python copy_dir.py "src/js/node_modules/morphdom/dist" "src/reactpy_django/static/reactpy_django/morphdom"', +] +artifacts = [] + +############################# +# >>> Hatch Test Runner <<< # +############################# + +[tool.hatch.envs.hatch-test] +extra-dependencies = [ + "pytest-sugar", + "pytest-django", + "playwright", + "channels[daphne]>=4.0.0", + "twisted", + "tblib", + "servestatic", +] +matrix-name-format = "{variable}-{value}" + +# Django 4.2 +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.9", "3.10", "3.11", "3.12"] +django = ["4.2"] + +# Django 5.0 +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.10", "3.11", "3.12"] +django = ["5.0"] + +# Django 5.1 +[[tool.hatch.envs.hatch-test.matrix]] +python = ["3.10", "3.11", "3.12", "3.13"] +django = ["5.1"] + +[tool.hatch.envs.hatch-test.overrides] +matrix.django.dependencies = [ + { if = [ + "4.2", + ], value = "django~=4.2" }, + { if = [ + "5.0", + ], value = "django~=5.0" }, + { if = [ + "5.1", + ], value = "django~=5.1" }, +] + +[tool.pytest.ini_options] +addopts = """\ + --strict-config + --strict-markers + --reuse-db + """ +django_find_project = false +DJANGO_SETTINGS_MODULE = "test_app.settings_single_db" +pythonpath = [".", "tests/"] + +################################ +# >>> Hatch Django Scripts <<< # +################################ + +[tool.hatch.envs.django] +extra-dependencies = ["channels[daphne]>=4.0.0", "twisted", "servestatic"] + +[tool.hatch.envs.django.scripts] +runserver = [ + "cd tests && python manage.py migrate --noinput", + "cd tests && python manage.py runserver", +] + +####################################### +# >>> Hatch Documentation Scripts <<< # +####################################### + +[tool.hatch.envs.docs] +template = "docs" +extra-dependencies = [ + "mkdocs", + "mkdocs-git-revision-date-localized-plugin", + "mkdocs-material==9.4.0", + "mkdocs-include-markdown-plugin", + "mkdocs-spellcheck[all]", + "mkdocs-git-authors-plugin", + "mkdocs-minify-plugin", + "mike", + "ruff", + "django-stubs", + "linkcheckmd", +] + +[tool.hatch.envs.docs.scripts] +serve = ["cd docs && mkdocs serve"] +build = ["cd docs && mkdocs build --strict"] +linkcheck = [ + "linkcheckMarkdown docs/ -v -r --method head", + "linkcheckMarkdown README.md -v -r", + "linkcheckMarkdown CHANGELOG.md -v -r", +] +deploy_latest = ["cd docs && mike deploy --push --update-aliases {args} latest"] +deploy_develop = ["cd docs && mike deploy --push develop"] +check_examples = ["ruff check docs/examples/python"] + +############################ +# >>> Hatch JS Scripts <<< # +############################ + +[tool.hatch.envs.javascript] +detached = true + +[tool.hatch.envs.javascript.scripts] +check = ["cd src/js && bun install", "cd src/js && bun run check"] +fix = ["cd src/js && bun install", "cd src/js && bun run format"] + +######################### +# >>> Generic Tools <<< # +######################### [tool.ruff] extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", ".nox/*", "build/*"] line-length = 120 +format.preview = true +lint.extend-ignore = [ + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "ARG004", # Unused static method argument + "FBT001", # Boolean-typed positional argument in function definition + "FBT002", # Boolean default positional argument in function definition + "PLR2004", # Magic value used in comparison + "SIM115", # Use context handler for opening files + "SLF001", # Private member accessed + "E501", # Line too long + "PLC0415", # `import` should be at the top-level of a file +] +lint.preview = true +lint.isort.known-first-party = ["src", "tests"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 63e3d68e..00000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ --r requirements/build-docs.txt --r requirements/build-pkg.txt --r requirements/check-style.txt --r requirements/check-types.txt --r requirements/dev-env.txt --r requirements/pkg-deps.txt --r requirements/test-env.txt --r requirements/test-run.txt diff --git a/requirements/build-docs.txt b/requirements/build-docs.txt deleted file mode 100644 index 846a7ba3..00000000 --- a/requirements/build-docs.txt +++ /dev/null @@ -1,8 +0,0 @@ -mkdocs -mkdocs-git-revision-date-localized-plugin -mkdocs-material==9.4.0 -mkdocs-include-markdown-plugin -mkdocs-spellcheck[all] -mkdocs-git-authors-plugin -mkdocs-minify-plugin -mike diff --git a/requirements/build-pkg.txt b/requirements/build-pkg.txt deleted file mode 100644 index 82f40eaf..00000000 --- a/requirements/build-pkg.txt +++ /dev/null @@ -1,3 +0,0 @@ -twine -wheel -build diff --git a/requirements/check-style.txt b/requirements/check-style.txt deleted file mode 100644 index af3ee576..00000000 --- a/requirements/check-style.txt +++ /dev/null @@ -1 +0,0 @@ -ruff diff --git a/requirements/check-types.txt b/requirements/check-types.txt deleted file mode 100644 index c962b716..00000000 --- a/requirements/check-types.txt +++ /dev/null @@ -1,3 +0,0 @@ -mypy -django-stubs[compatible-mypy] -channels-redis diff --git a/requirements/dev-env.txt b/requirements/dev-env.txt deleted file mode 100644 index 05940702..00000000 --- a/requirements/dev-env.txt +++ /dev/null @@ -1,3 +0,0 @@ -twine -wheel --r ./test-run.txt diff --git a/requirements/pkg-deps.txt b/requirements/pkg-deps.txt deleted file mode 100644 index 61182ef9..00000000 --- a/requirements/pkg-deps.txt +++ /dev/null @@ -1,8 +0,0 @@ -channels >=4.0.0 -django >=4.2.0 -reactpy >=1.1.0, <1.2.0 -reactpy-router >=1.0.0, <2.0.0 -dill >=0.3.5 -orjson >=3.6.0 -nest_asyncio >=1.5.0 -typing_extensions diff --git a/requirements/test-env.txt b/requirements/test-env.txt deleted file mode 100644 index fc1ba2ce..00000000 --- a/requirements/test-env.txt +++ /dev/null @@ -1,5 +0,0 @@ -playwright -twisted -channels[daphne]>=4.0.0 -tblib -whitenoise diff --git a/requirements/test-run.txt b/requirements/test-run.txt deleted file mode 100644 index 816817c6..00000000 --- a/requirements/test-run.txt +++ /dev/null @@ -1 +0,0 @@ -nox diff --git a/setup.py b/setup.py deleted file mode 100644 index f0c2f22d..00000000 --- a/setup.py +++ /dev/null @@ -1,182 +0,0 @@ -from __future__ import annotations, print_function - -import shutil -import subprocess -import sys -import traceback -from logging import getLogger -from pathlib import Path - -from setuptools import find_namespace_packages, setup -from setuptools.command.develop import develop -from setuptools.command.sdist import sdist - -# ----------------------------------------------------------------------------- -# Basic Constants -# ----------------------------------------------------------------------------- -name = "reactpy_django" -root_dir = Path(__file__).parent -src_dir = root_dir / "src" -js_dir = src_dir / "js" -package_dir = src_dir / name -static_dir = package_dir / "static" / name -log = getLogger(__name__) - - -# ----------------------------------------------------------------------------- -# Package Definition -# ----------------------------------------------------------------------------- -package = { - "name": name, - "python_requires": ">=3.9", - "packages": find_namespace_packages(src_dir), - "package_dir": {"": "src"}, - "description": "It's React, but in Python. Now with Django integration.", - "author": "Mark Bakhit", - "author_email": "archiethemonger@gmail.com", - "url": "https://github.com/reactive-python/reactpy-django", - "license": "MIT", - "platforms": "Linux, Mac OS X, Windows", - "keywords": [ - "interactive", - "reactive", - "widgets", - "DOM", - "React", - "ReactJS", - "ReactPy", - ], - "include_package_data": True, - "zip_safe": False, - "classifiers": [ - "Framework :: Django", - "Framework :: Django :: 4.0", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Operating System :: OS Independent", - "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", - ], -} - - -# ----------------------------------------------------------------------------- -# Library Version -# ----------------------------------------------------------------------------- -for line in (package_dir / "__init__.py").read_text().split("\n"): - if line.startswith("__version__ = "): - package["version"] = eval(line.split("=", 1)[1]) - break -else: - print(f"No version found in {package_dir}/__init__.py") - sys.exit(1) - - -# ----------------------------------------------------------------------------- -# Requirements -# ----------------------------------------------------------------------------- -requirements: list[str] = [] -with (root_dir / "requirements" / "pkg-deps.txt").open() as f: - requirements.extend(line for line in map(str.strip, f) if not line.startswith("#")) -package["install_requires"] = requirements - - -# ----------------------------------------------------------------------------- -# Library Description -# ----------------------------------------------------------------------------- -with (root_dir / "README.md").open() as f: - long_description = f.read() - -package["long_description"] = long_description -package["long_description_content_type"] = "text/markdown" - - -# ---------------------------------------------------------------------------- -# Build Javascript -# ---------------------------------------------------------------------------- -def copy_js_files(source_dir: Path, destination: Path) -> None: - if destination.exists(): - shutil.rmtree(destination) - destination.mkdir() - - for file in source_dir.iterdir(): - if file.is_file(): - shutil.copy(file, destination / file.name) - else: - copy_js_files(file, destination / file.name) - - -def build_javascript_first(build_cls: type): - class Command(build_cls): - def run(self): - - log.info("Installing Javascript...") - result = subprocess.run( - ["bun", "install"], cwd=str(js_dir), check=True - ).returncode - if result != 0: - log.error(traceback.format_exc()) - log.error("Failed to install Javascript") - raise RuntimeError("Failed to install Javascript") - - log.info("Building Javascript...") - result = subprocess.run( - [ - "bun", - "build", - "./src/index.tsx", - "--outfile", - str(static_dir / "client.js"), - "--minify", - ], - cwd=str(js_dir), - check=True, - ).returncode - if result != 0: - log.error(traceback.format_exc()) - log.error("Failed to build Javascript") - raise RuntimeError("Failed to build Javascript") - - log.info("Copying @pyscript/core distribution") - pyscript_dist = js_dir / "node_modules" / "@pyscript" / "core" / "dist" - pyscript_static_dir = static_dir / "pyscript" - copy_js_files(pyscript_dist, pyscript_static_dir) - - log.info("Copying Morphdom distribution") - morphdom_dist = js_dir / "node_modules" / "morphdom" / "dist" - morphdom_static_dir = static_dir / "morphdom" - copy_js_files(morphdom_dist, morphdom_static_dir) - - log.info("Successfully built Javascript") - super().run() - - return Command - - -package["cmdclass"] = { - "sdist": build_javascript_first(sdist), - "develop": build_javascript_first(develop), -} - -if sys.version_info < (3, 10, 6): - from distutils.command.build import build - - package["cmdclass"]["build"] = build_javascript_first(build) -else: - from setuptools.command.build_py import build_py - - package["cmdclass"]["build_py"] = build_javascript_first(build_py) - - -# ----------------------------------------------------------------------------- -# Installation -# ----------------------------------------------------------------------------- -if __name__ == "__main__": - setup(**package) diff --git a/src/build_scripts/copy_dir.py b/src/build_scripts/copy_dir.py new file mode 100644 index 00000000..1f446f83 --- /dev/null +++ b/src/build_scripts/copy_dir.py @@ -0,0 +1,31 @@ +import shutil +import sys +from pathlib import Path + + +def copy_files(source: Path, destination: Path) -> None: + if destination.exists(): + shutil.rmtree(destination) + destination.mkdir() + + for file in source.iterdir(): + if file.is_file(): + shutil.copy(file, destination / file.name) + else: + copy_files(file, destination / file.name) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python copy_dir.py ") + sys.exit(1) + + root_dir = Path(__file__).parent.parent.parent + src = Path(root_dir / sys.argv[1]) + dest = Path(root_dir / sys.argv[2]) + + if not src.exists(): + print(f"Source directory {src} does not exist") + sys.exit(1) + + copy_files(src, dest) diff --git a/src/js/eslint.config.js b/src/js/eslint.config.js deleted file mode 100644 index 27082ef3..00000000 --- a/src/js/eslint.config.js +++ /dev/null @@ -1 +0,0 @@ -export default [{}]; diff --git a/src/js/eslint.config.mjs b/src/js/eslint.config.mjs new file mode 100644 index 00000000..320e9f8b --- /dev/null +++ b/src/js/eslint.config.mjs @@ -0,0 +1,43 @@ +import react from "eslint-plugin-react"; +import globals from "globals"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + ...compat.extends("eslint:recommended", "plugin:react/recommended"), + { + plugins: { + react, + }, + + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + + ecmaVersion: "latest", + sourceType: "module", + }, + + settings: { + react: { + version: "18.2.0", + }, + }, + + rules: { + "react/prop-types": "off", + }, + }, +]; diff --git a/src/reactpy_django/clean.py b/src/reactpy_django/clean.py index 93df7be6..1ec327ee 100644 --- a/src/reactpy_django/clean.py +++ b/src/reactpy_django/clean.py @@ -49,7 +49,8 @@ def clean_sessions(verbosity: int = 1): """Deletes expired component sessions from the database. As a performance optimization, this is only run once every REACTPY_SESSION_MAX_AGE seconds. """ - from reactpy_django.config import REACTPY_DEBUG_MODE, REACTPY_SESSION_MAX_AGE + + from reactpy_django.config import DJANGO_DEBUG, REACTPY_SESSION_MAX_AGE from reactpy_django.models import ComponentSession if verbosity >= 2: @@ -66,7 +67,7 @@ def clean_sessions(verbosity: int = 1): session_objects.delete() - if REACTPY_DEBUG_MODE or verbosity >= 2: + if DJANGO_DEBUG or verbosity >= 2: inspect_clean_duration(start_time, "component sessions", verbosity) @@ -78,7 +79,7 @@ def clean_user_data(verbosity: int = 1): However, we can't use Django to enforce this relationship since ReactPy can be configured to use any database. """ - from reactpy_django.config import REACTPY_DEBUG_MODE + from reactpy_django.config import DJANGO_DEBUG from reactpy_django.models import UserDataModel if verbosity >= 2: @@ -102,7 +103,7 @@ def clean_user_data(verbosity: int = 1): user_data_objects.delete() - if REACTPY_DEBUG_MODE or verbosity >= 2: + if DJANGO_DEBUG or verbosity >= 2: inspect_clean_duration(start_time, "user data", verbosity) diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index e74299e3..090980a5 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -18,13 +18,13 @@ from reactpy_django.utils import import_dotted_path # Non-configurable values -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")) +DJANGO_DEBUG = settings.DEBUG # Snapshot of Django's DEBUG setting +_REACTPY_DEBUG_MODE.set_current(settings.DEBUG) _REACTPY_ASYNC_RENDERING.set_current( getattr(settings, "REACTPY_ASYNC_RENDERING", _REACTPY_ASYNC_RENDERING.current) ) diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html index 6b4ecc16..7e3746f5 100644 --- a/src/reactpy_django/templates/reactpy/component.html +++ b/src/reactpy_django/templates/reactpy/component.html @@ -1,6 +1,6 @@ {% load static %} -{% if reactpy_failure and reactpy_debug_mode %} +{% if reactpy_failure and django_debug %}
{% firstof reactpy_error "UnknownError" %}: "{% firstof reactpy_dotted_path "UnknownPath" %}"
{% endif %} @@ -10,18 +10,18 @@ {% if reactpy_prerender_html %}
{{reactpy_prerender_html|safe}}
{% endif %} {% if reactpy_offline_html %}{% endif %} {% endif %} diff --git a/src/reactpy_django/templates/reactpy/pyscript_setup.html b/src/reactpy_django/templates/reactpy/pyscript_setup.html index e258cf08..547a672a 100644 --- a/src/reactpy_django/templates/reactpy/pyscript_setup.html +++ b/src/reactpy_django/templates/reactpy/pyscript_setup.html @@ -1,7 +1,7 @@ {% load static %} -{% if not reactpy_debug_mode %} +{% if not django_debug %} {% endif %} diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 2f34651a..1f419049 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -74,6 +74,8 @@ def component( """ + from reactpy_django.config import DJANGO_DEBUG + request: HttpRequest | None = context.get("request") perceived_host = (request.get_host() if request else "").strip("/") host = ( @@ -93,7 +95,7 @@ def component( _offline_html = "" # Validate the host - if host and reactpy_config.REACTPY_DEBUG_MODE: + if host and DJANGO_DEBUG: try: validate_host(host) except InvalidHostError as e: @@ -108,7 +110,7 @@ def component( return failure_context(dotted_path, ComponentDoesNotExistError(msg)) # Validate the component args & kwargs - if is_local and reactpy_config.REACTPY_DEBUG_MODE: + if is_local and DJANGO_DEBUG: try: validate_component_args(user_component, *args, **kwargs) except ComponentParamError as e: @@ -234,17 +236,21 @@ def pyscript_setup( config: A JSON string or Python dictionary containing PyScript \ configuration values. """ + from reactpy_django.config import DJANGO_DEBUG + return { "pyscript_config": extend_pyscript_config(extra_py, extra_js, config), "pyscript_layout_handler": PYSCRIPT_LAYOUT_HANDLER, - "reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE, + "django_debug": DJANGO_DEBUG, } def failure_context(dotted_path: str, error: Exception): + from reactpy_django.config import DJANGO_DEBUG + return { "reactpy_failure": True, - "reactpy_debug_mode": reactpy_config.REACTPY_DEBUG_MODE, + "django_debug": DJANGO_DEBUG, "reactpy_dotted_path": dotted_path, "reactpy_error": type(error).__name__, } diff --git a/tests/test_app/apps.py b/tests/test_app/apps.py index 2bef8446..c5ec0d60 100644 --- a/tests/test_app/apps.py +++ b/tests/test_app/apps.py @@ -1,4 +1,3 @@ -import contextlib import sys from django.apps import AppConfig @@ -11,8 +10,6 @@ class TestAppConfig(AppConfig): name = "test_app" def ready(self): - from django.contrib.auth.models import User - register_iframe("test_app.views.view_to_iframe_sync_func") register_iframe(views.view_to_iframe_async_func) register_iframe(views.ViewToIframeSyncClass) @@ -22,8 +19,3 @@ def ready(self): if "test" in sys.argv: return - - with contextlib.suppress(Exception): - User.objects.create_superuser( - username="admin", email="admin@example.com", password="password" - ) diff --git a/tests/test_app/middleware.py b/tests/test_app/middleware.py new file mode 100644 index 00000000..0927a100 --- /dev/null +++ b/tests/test_app/middleware.py @@ -0,0 +1,31 @@ +import contextlib + +from asgiref.sync import iscoroutinefunction, markcoroutinefunction + + +class AutoCreateAdminMiddleware: + async_capable = True + sync_capable = True + + def __init__(self, get_response): + from django.contrib.auth.models import User + + # One-time configuration and initialization. + self.get_response = get_response + with contextlib.suppress(Exception): + User.objects.create_superuser( + username="admin", email="admin@example.com", password="password" + ) + + if iscoroutinefunction(self.get_response): + markcoroutinefunction(self) + + def __call__(self, request): + if iscoroutinefunction(self.get_response): + + async def async_call(): + return await self.get_response(request) + + return async_call() + + return self.get_response(request) diff --git a/tests/test_app/prerender/components.py b/tests/test_app/prerender/components.py index dd312195..7a2b29b4 100644 --- a/tests/test_app/prerender/components.py +++ b/tests/test_app/prerender/components.py @@ -1,14 +1,19 @@ from time import sleep -import reactpy_django from reactpy import component, html +import reactpy_django + +SLEEP_TIME = 0.25 + @component def prerender_string(): scope = reactpy_django.hooks.use_scope() - sleep(0.5) + if scope.get("type") != "http": + sleep(SLEEP_TIME) + return ( "prerender_string: Fully Rendered" if scope.get("type") == "websocket" diff --git a/tests/test_app/settings_multi_db.py b/tests/test_app/settings_multi_db.py index 65e37415..fb390e28 100644 --- a/tests/test_app/settings_multi_db.py +++ b/tests/test_app/settings_multi_db.py @@ -12,9 +12,9 @@ SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" # Run in production mode when using a real web server -DEBUG = all( - not sys.argv[0].endswith(webserver_name) - for webserver_name in {"hypercorn", "uvicorn", "daphne"} +DEBUG = not any( + sys.argv[0].endswith(webserver_name) + for webserver_name in ["hypercorn", "uvicorn", "daphne"] ) ALLOWED_HOSTS = ["*"] @@ -32,7 +32,8 @@ ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", + "servestatic.middleware.ServeStaticMiddleware", + "test_app.middleware.AutoCreateAdminMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -66,37 +67,26 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - # Changing NAME is needed due to a bug related to `manage.py test` - "NAME": os.path.join( - BASE_DIR, - f"test_{DB_NAME}.sqlite3" if "test" in sys.argv else f"{DB_NAME}.sqlite3", - ), + "NAME": os.path.join(BASE_DIR, f"{DB_NAME}.sqlite3"), "TEST": { - "NAME": os.path.join(BASE_DIR, f"test_{DB_NAME}.sqlite3"), + "NAME": os.path.join(BASE_DIR, f"{DB_NAME}.sqlite3"), "OPTIONS": {"timeout": 20}, "DEPENDENCIES": [], }, "OPTIONS": {"timeout": 20}, }, -} -if "test" in sys.argv: - DATABASES["reactpy"] = { + "reactpy": { "ENGINE": "django.db.backends.sqlite3", - # Changing NAME is needed due to a bug related to `manage.py test` - "NAME": os.path.join( - BASE_DIR, - f"test_{DB_NAME}_2.sqlite3" - if "test" in sys.argv - else f"{DB_NAME}_2.sqlite3", - ), + "NAME": os.path.join(BASE_DIR, f"{DB_NAME}_2.sqlite3"), "TEST": { - "NAME": os.path.join(BASE_DIR, f"test_{DB_NAME}_2.sqlite3"), + "NAME": os.path.join(BASE_DIR, f"{DB_NAME}_2.sqlite3"), "OPTIONS": {"timeout": 20}, "DEPENDENCIES": [], }, "OPTIONS": {"timeout": 20}, - } - REACTPY_DATABASE = "reactpy" + }, +} +REACTPY_DATABASE = "reactpy" DATABASE_ROUTERS = ["reactpy_django.database.Router"] # Cache @@ -121,7 +111,6 @@ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True -USE_L10N = True USE_TZ = True # Default primary key field type @@ -139,9 +128,7 @@ ] # Logging -LOG_LEVEL = "WARNING" -if DEBUG and ("test" not in sys.argv): - LOG_LEVEL = "DEBUG" +LOG_LEVEL = "DEBUG" LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -159,4 +146,6 @@ CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} # ReactPy-Django Settings -REACTPY_BACKHAUL_THREAD = "test" not in sys.argv and "runserver" not in sys.argv +REACTPY_BACKHAUL_THREAD = any( + sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn"] +) diff --git a/tests/test_app/settings_single_db.py b/tests/test_app/settings_single_db.py index 2550c8d1..e5f8969a 100644 --- a/tests/test_app/settings_single_db.py +++ b/tests/test_app/settings_single_db.py @@ -12,9 +12,9 @@ SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" # Run in production mode when using a real web server -DEBUG = all( - not sys.argv[0].endswith(webserver_name) - for webserver_name in {"hypercorn", "uvicorn", "daphne"} +DEBUG = not any( + sys.argv[0].endswith(webserver_name) + for webserver_name in ["hypercorn", "uvicorn", "daphne"] ) ALLOWED_HOSTS = ["*"] @@ -32,7 +32,8 @@ ] MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", + "servestatic.middleware.ServeStaticMiddleware", + "test_app.middleware.AutoCreateAdminMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -66,13 +67,9 @@ DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", - # Changing NAME is needed due to a bug related to `manage.py test` - "NAME": os.path.join( - BASE_DIR, - f"test_{DB_NAME}.sqlite3" if "test" in sys.argv else f"{DB_NAME}.sqlite3", - ), + "NAME": os.path.join(BASE_DIR, f"{DB_NAME}.sqlite3"), "TEST": { - "NAME": os.path.join(BASE_DIR, f"test_{DB_NAME}.sqlite3"), + "NAME": os.path.join(BASE_DIR, f"{DB_NAME}.sqlite3"), "OPTIONS": {"timeout": 20}, "DEPENDENCIES": [], }, @@ -102,7 +99,6 @@ LANGUAGE_CODE = "en-us" TIME_ZONE = "UTC" USE_I18N = True -USE_L10N = True USE_TZ = True # Default primary key field type @@ -120,9 +116,7 @@ ] # Logging -LOG_LEVEL = "WARNING" -if DEBUG and ("test" not in sys.argv): - LOG_LEVEL = "DEBUG" +LOG_LEVEL = "DEBUG" LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -140,4 +134,6 @@ CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} # ReactPy-Django Settings -REACTPY_BACKHAUL_THREAD = "test" not in sys.argv and "runserver" not in sys.argv +REACTPY_BACKHAUL_THREAD = any( + sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn"] +) diff --git a/tests/test_app/tests/__init__.py b/tests/test_app/tests/__init__.py index fff5a11e..e69de29b 100644 --- a/tests/test_app/tests/__init__.py +++ b/tests/test_app/tests/__init__.py @@ -1 +0,0 @@ -from . import * # noqa: F401, F403 diff --git a/tests/test_app/tests/conftest.py b/tests/test_app/tests/conftest.py new file mode 100644 index 00000000..89c7fad9 --- /dev/null +++ b/tests/test_app/tests/conftest.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import pytest + +os.chdir(Path(__file__).parent.parent.parent) + + +@pytest.fixture(autouse=True) +def enable_db_access_for_all_tests(db): + pass + + +@pytest.fixture(autouse=True, scope="session") +def install_playwright(): + subprocess.run(["playwright", "install", "chromium"], check=True) diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index f3726a4c..c4848ccf 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -1,111 +1,25 @@ -import asyncio import os import socket -import sys -from functools import partial from time import sleep -from channels.testing import ChannelsLiveServerTestCase -from channels.testing.live import make_application -from django.core.exceptions import ImproperlyConfigured -from django.core.management import call_command -from django.db import connections -from django.test.utils import modify_settings -from playwright.sync_api import TimeoutError, sync_playwright +from playwright.sync_api import TimeoutError from reactpy_django.models import ComponentSession from reactpy_django.utils import strtobool -GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False") -CLICK_DELAY = 250 if strtobool(GITHUB_ACTIONS) else 25 # Delay in miliseconds. +from .utils import GITHUB_ACTIONS, PlaywrightTestCase +CLICK_DELAY = 250 if strtobool(GITHUB_ACTIONS) else 25 # Delay in miliseconds. -class ComponentTests(ChannelsLiveServerTestCase): - from django.db import DEFAULT_DB_ALIAS - from reactpy_django import config +class GenericComponentTests(PlaywrightTestCase): databases = {"default"} @classmethod def setUpClass(cls): - # Repurposed from ChannelsLiveServerTestCase._pre_setup - for connection in connections.all(): - if cls._is_in_memory_db(cls, connection): - raise ImproperlyConfigured( - "ChannelLiveServerTestCase can not be used with in memory databases" - ) - cls._live_server_modified_settings = modify_settings( - ALLOWED_HOSTS={"append": cls.host} - ) - cls._live_server_modified_settings.enable() - get_application = partial( - make_application, - static_wrapper=cls.static_wrapper if cls.serve_static else None, - ) - cls._server_process = cls.ProtocolServerProcess(cls.host, get_application) - cls._server_process.start() - cls._server_process.ready.wait() - cls._port = cls._server_process.port.value - - # Open the second server process, used for testing custom hosts - cls._server_process2 = cls.ProtocolServerProcess(cls.host, get_application) - cls._server_process2.start() - cls._server_process2.ready.wait() - cls._port2 = cls._server_process2.port.value - - # Open the third server process, used for testing offline fallback - cls._server_process3 = cls.ProtocolServerProcess(cls.host, get_application) - cls._server_process3.start() - cls._server_process3.ready.wait() - cls._port3 = cls._server_process3.port.value - - # Open a Playwright browser window - if sys.platform == "win32": - asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) - cls.playwright = sync_playwright().start() - headless = strtobool(os.environ.get("PLAYWRIGHT_HEADLESS", GITHUB_ACTIONS)) - cls.browser = cls.playwright.chromium.launch(headless=bool(headless)) - cls.page = cls.browser.new_page() - cls.page.set_default_timeout(5000) - - @classmethod - def tearDownClass(cls): - from reactpy_django import config - - # Close the Playwright browser - cls.playwright.stop() - - # Close the other server processes - cls._server_process.terminate() - cls._server_process.join() - cls._server_process2.terminate() - cls._server_process2.join() - cls._server_process3.terminate() - cls._server_process3.join() - - # Repurposed from ChannelsLiveServerTestCase._post_teardown - cls._live_server_modified_settings.disable() - for db_name in {"default", config.REACTPY_DATABASE}: - call_command( - "flush", - verbosity=0, - interactive=False, - database=db_name, - reset_sequences=False, - ) - - def _pre_setup(self): - """Handled manually in `setUpClass` to speed things up.""" - - def _post_teardown(self): - """Handled manually in `tearDownClass` to prevent TransactionTestCase from doing - database flushing. This is needed to prevent a `SynchronousOnlyOperation` from - occuring due to a bug within `ChannelsLiveServerTestCase`.""" - - def setUp(self): - if self.page.url == "about:blank": - self.page.goto(self.live_server_url) + super().setUpClass() + cls.page.goto(f"http://{cls.host}:{cls._port}") def test_hello_world(self): self.page.wait_for_selector("#hello-world") @@ -297,149 +211,6 @@ def test_component_session_missing(self): os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE") self.assertFalse(query_exists) - def test_custom_host(self): - """Make sure that the component is rendered by a separate server.""" - new_page = self.browser.new_page() - new_page.goto(f"{self.live_server_url}/port/{self._port2}/") - try: - elem = new_page.locator(".custom_host-0") - elem.wait_for() - self.assertIn( - f"Server Port: {self._port2}", - elem.text_content(), - ) - finally: - new_page.close() - - def test_custom_host_wrong_port(self): - """Make sure that other ports are not rendering components.""" - new_page = self.browser.new_page() - try: - tmp_sock = socket.socket() - tmp_sock.bind((self._server_process.host, 0)) - random_port = tmp_sock.getsockname()[1] - new_page.goto(f"{self.live_server_url}/port/{random_port}/") - with self.assertRaises(TimeoutError): - new_page.locator(".custom_host").wait_for(timeout=1000) - finally: - new_page.close() - - def test_host_roundrobin(self): - """Verify if round-robin host selection is working.""" - new_page = self.browser.new_page() - new_page.goto(f"{self.live_server_url}/roundrobin/{self._port}/{self._port2}/8") - try: - elem0 = new_page.locator(".custom_host-0") - elem1 = new_page.locator(".custom_host-1") - elem2 = new_page.locator(".custom_host-2") - elem3 = new_page.locator(".custom_host-3") - - elem0.wait_for() - elem1.wait_for() - elem2.wait_for() - elem3.wait_for() - - current_ports = { - elem0.get_attribute("data-port"), - elem1.get_attribute("data-port"), - elem2.get_attribute("data-port"), - elem3.get_attribute("data-port"), - } - correct_ports = { - str(self._port), - str(self._port2), - } - - # There should only be two ports in the set - self.assertEqual(current_ports, correct_ports) - self.assertEqual(len(current_ports), 2) - finally: - new_page.close() - - def test_prerender(self): - """Verify if round-robin host selection is working.""" - new_page = self.browser.new_page() - new_page.goto(f"{self.live_server_url}/prerender/") - try: - string = new_page.locator("#prerender_string") - vdom = new_page.locator("#prerender_vdom") - component = new_page.locator("#prerender_component") - use_root_id_http = new_page.locator("#use-root-id-http") - use_root_id_ws = new_page.locator("#use-root-id-ws") - use_user_http = new_page.locator("#use-user-http[data-success=True]") - use_user_ws = new_page.locator("#use-user-ws[data-success=true]") - - string.wait_for() - vdom.wait_for() - component.wait_for() - use_root_id_http.wait_for() - use_user_http.wait_for() - - # Check if the prerender occurred - self.assertEqual( - string.all_inner_texts(), ["prerender_string: Prerendered"] - ) - self.assertEqual(vdom.all_inner_texts(), ["prerender_vdom: Prerendered"]) - self.assertEqual( - component.all_inner_texts(), ["prerender_component: Prerendered"] - ) - root_id_value = use_root_id_http.get_attribute("data-value") - self.assertEqual(len(root_id_value), 36) - - # Check if the full render occurred - sleep(1) - self.assertEqual( - string.all_inner_texts(), ["prerender_string: Fully Rendered"] - ) - self.assertEqual(vdom.all_inner_texts(), ["prerender_vdom: Fully Rendered"]) - self.assertEqual( - component.all_inner_texts(), ["prerender_component: Fully Rendered"] - ) - use_root_id_ws.wait_for() - use_user_ws.wait_for() - self.assertEqual(use_root_id_ws.get_attribute("data-value"), root_id_value) - - finally: - new_page.close() - - def test_component_errors(self): - new_page = self.browser.new_page() - new_page.goto(f"{self.live_server_url}/errors/") - try: - # ComponentDoesNotExistError - broken_component = new_page.locator("#component_does_not_exist_error") - broken_component.wait_for() - self.assertIn( - "ComponentDoesNotExistError:", broken_component.text_content() - ) - - # ComponentParamError - broken_component = new_page.locator("#component_param_error") - broken_component.wait_for() - self.assertIn("ComponentParamError:", broken_component.text_content()) - - # InvalidHostError - broken_component = new_page.locator("#invalid_host_error") - broken_component.wait_for() - self.assertIn("InvalidHostError:", broken_component.text_content()) - - # SynchronousOnlyOperation - broken_component = new_page.locator("#broken_postprocessor_query pre") - broken_component.wait_for() - self.assertIn("SynchronousOnlyOperation:", broken_component.text_content()) - - # ViewNotRegisteredError - broken_component = new_page.locator("#view_to_iframe_not_registered pre") - broken_component.wait_for() - self.assertIn("ViewNotRegisteredError:", broken_component.text_content()) - - # DecoratorParamError - broken_component = new_page.locator("#incorrect_user_passes_test_decorator") - broken_component.wait_for() - self.assertIn("DecoratorParamError:", broken_component.text_content()) - finally: - new_page.close() - def test_use_user_data(self): text_input = self.page.wait_for_selector("#use-user-data input") login_1 = self.page.wait_for_selector("#use-user-data .login-1") @@ -536,170 +307,333 @@ def test_use_user_data_with_default(self): user_data_div.text_content(), ) + +class PrerenderTests(PlaywrightTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.page.goto(f"http://{cls.host}:{cls._port}/prerender/") + + def test_prerender(self): + """Verify if round-robin host selection is working.""" + string = self.page.locator("#prerender_string") + vdom = self.page.locator("#prerender_vdom") + component = self.page.locator("#prerender_component") + use_root_id_http = self.page.locator("#use-root-id-http") + use_root_id_ws = self.page.locator("#use-root-id-ws") + use_user_http = self.page.locator("#use-user-http[data-success=True]") + use_user_ws = self.page.locator("#use-user-ws[data-success=true]") + + # Check if the prerender occurred properly + string.wait_for() + vdom.wait_for() + component.wait_for() + use_root_id_http.wait_for() + use_user_http.wait_for() + self.assertEqual(string.all_inner_texts(), ["prerender_string: Prerendered"]) + self.assertEqual(vdom.all_inner_texts(), ["prerender_vdom: Prerendered"]) + self.assertEqual( + component.all_inner_texts(), ["prerender_component: Prerendered"] + ) + root_id_value = use_root_id_http.get_attribute("data-value") + self.assertEqual(len(root_id_value), 36) + + # Check if the full render occurred + sleep(2) + self.assertEqual(string.all_inner_texts(), ["prerender_string: Fully Rendered"]) + self.assertEqual(vdom.all_inner_texts(), ["prerender_vdom: Fully Rendered"]) + self.assertEqual( + component.all_inner_texts(), ["prerender_component: Fully Rendered"] + ) + use_root_id_ws.wait_for() + use_user_ws.wait_for() + self.assertEqual(use_root_id_ws.get_attribute("data-value"), root_id_value) + + +class ErrorTests(PlaywrightTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.page.goto(f"http://{cls.host}:{cls._port}/errors/") + + def test_component_does_not_exist_error(self): + broken_component = self.page.locator("#component_does_not_exist_error") + broken_component.wait_for() + self.assertIn("ComponentDoesNotExistError:", broken_component.text_content()) + + def test_component_param_error(self): + broken_component = self.page.locator("#component_param_error") + broken_component.wait_for() + self.assertIn("ComponentParamError:", broken_component.text_content()) + + def test_invalid_host_error(self): + broken_component = self.page.locator("#invalid_host_error") + broken_component.wait_for() + self.assertIn("InvalidHostError:", broken_component.text_content()) + + def test_synchronous_only_operation_error(self): + broken_component = self.page.locator("#broken_postprocessor_query pre") + broken_component.wait_for() + self.assertIn("SynchronousOnlyOperation:", broken_component.text_content()) + + def test_view_not_registered_error(self): + broken_component = self.page.locator("#view_to_iframe_not_registered pre") + broken_component.wait_for() + self.assertIn("ViewNotRegisteredError:", broken_component.text_content()) + + def test_decorator_param_error(self): + broken_component = self.page.locator("#incorrect_user_passes_test_decorator") + broken_component.wait_for() + self.assertIn("DecoratorParamError:", broken_component.text_content()) + + +class UrlRouterTests(PlaywrightTestCase): + def test_url_router(self): - new_page = self.browser.new_page() - try: - new_page.goto(f"{self.live_server_url}/router/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/subroute/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/subroute/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("subroute/", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/unspecified/123/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/unspecified/123/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/unspecified//", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/integer/123/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/integer/123/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/integer//", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/path/abc/123/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/path/abc/123/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/path//", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/slug/abc-123/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/slug/abc-123/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/slug//", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/string/abc/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/string/abc/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/string//", string.text_content()) - - new_page.goto( - f"{self.live_server_url}/router/uuid/123e4567-e89b-12d3-a456-426614174000/" - ) - path = new_page.wait_for_selector("#router-path") - self.assertIn( - "/router/uuid/123e4567-e89b-12d3-a456-426614174000/", - path.get_attribute("data-path"), - ) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/uuid//", string.text_content()) + self.page.goto(f"{self.live_server_url}/router/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/", string.text_content()) + + def test_url_router_subroute(self): + self.page.goto(f"{self.live_server_url}/router/subroute/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/subroute/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("subroute/", string.text_content()) + + def test_url_unspecified(self): + self.page.goto(f"{self.live_server_url}/router/unspecified/123/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/unspecified/123/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/unspecified//", string.text_content()) + + def test_url_router_integer(self): + self.page.goto(f"{self.live_server_url}/router/integer/123/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/integer/123/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/integer//", string.text_content()) + + def test_url_router_path(self): + self.page.goto(f"{self.live_server_url}/router/path/abc/123/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/path/abc/123/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/path//", string.text_content()) + + def test_url_router_slug(self): + self.page.goto(f"{self.live_server_url}/router/slug/abc-123/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/slug/abc-123/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/slug//", string.text_content()) + + def test_url_router_string(self): + self.page.goto(f"{self.live_server_url}/router/string/abc/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/string/abc/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/string//", string.text_content()) + + def test_url_router_uuid(self): + self.page.goto( + f"{self.live_server_url}/router/uuid/123e4567-e89b-12d3-a456-426614174000/" + ) + path = self.page.wait_for_selector("#router-path") + self.assertIn( + "/router/uuid/123e4567-e89b-12d3-a456-426614174000/", + path.get_attribute("data-path"), + ) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/uuid//", string.text_content()) - new_page.goto( - f"{self.live_server_url}/router/any/adslkjgklasdjhfah/6789543256/" - ) - path = new_page.wait_for_selector("#router-path") - self.assertIn( - "/router/any/adslkjgklasdjhfah/6789543256/", - path.get_attribute("data-path"), - ) - string = new_page.query_selector("#router-string") - self.assertEqual("/router/any/", string.text_content()) - - new_page.goto(f"{self.live_server_url}/router/two/123/abc/") - path = new_page.wait_for_selector("#router-path") - self.assertIn("/router/two/123/abc/", path.get_attribute("data-path")) - string = new_page.query_selector("#router-string") - self.assertEqual( - "/router/two///", string.text_content() - ) + def test_url_router_any(self): + self.page.goto( + f"{self.live_server_url}/router/any/adslkjgklasdjhfah/6789543256/" + ) + path = self.page.wait_for_selector("#router-path") + self.assertIn( + "/router/any/adslkjgklasdjhfah/6789543256/", + path.get_attribute("data-path"), + ) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/any/", string.text_content()) - finally: - new_page.close() + def test_url_router_int_and_string(self): + self.page.goto(f"{self.live_server_url}/router/two/123/abc/") + path = self.page.wait_for_selector("#router-path") + self.assertIn("/router/two/123/abc/", path.get_attribute("data-path")) + string = self.page.query_selector("#router-string") + self.assertEqual("/router/two///", string.text_content()) - def test_offline_components(self): - new_page = self.browser.new_page() - try: - server3_url = self.live_server_url.replace( - str(self._port), str(self._port3) - ) - new_page.goto(f"{server3_url}/offline/") - new_page.wait_for_selector("div:not([hidden]) > #online") - self.assertIsNotNone(new_page.query_selector("div[hidden] > #offline")) - self._server_process3.terminate() - self._server_process3.join() - new_page.wait_for_selector("div:not([hidden]) > #offline") - self.assertIsNotNone(new_page.query_selector("div[hidden] > #online")) - finally: - new_page.close() +class ChannelLayersTests(PlaywrightTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.page.goto(f"http://{cls.host}:{cls._port}/channel-layers/") def test_channel_layer_components(self): - new_page = self.browser.new_page() - try: - new_page.goto(f"{self.live_server_url}/channel-layers/") - sender = new_page.wait_for_selector("#sender") - sender.type("test", delay=CLICK_DELAY) - sender.press("Enter", delay=CLICK_DELAY) - receiver = new_page.wait_for_selector("#receiver[data-message='test']") - self.assertIsNotNone(receiver) - - sender = new_page.wait_for_selector("#group-sender") - sender.type("1234", delay=CLICK_DELAY) - sender.press("Enter", delay=CLICK_DELAY) - receiver_1 = new_page.wait_for_selector( - "#group-receiver-1[data-message='1234']" - ) - receiver_2 = new_page.wait_for_selector( - "#group-receiver-2[data-message='1234']" - ) - receiver_3 = new_page.wait_for_selector( - "#group-receiver-3[data-message='1234']" - ) - self.assertIsNotNone(receiver_1) - self.assertIsNotNone(receiver_2) - self.assertIsNotNone(receiver_3) - - finally: - new_page.close() - - def test_pyscript_components(self): - new_page = self.browser.new_page() - try: - new_page.goto(f"{self.live_server_url}/pyscript/") - new_page.wait_for_selector("#hello-world-loading") - new_page.wait_for_selector("#hello-world") - new_page.wait_for_selector("#custom-root") - new_page.wait_for_selector("#multifile-parent") - new_page.wait_for_selector("#multifile-child") - - new_page.wait_for_selector("#counter") - new_page.wait_for_selector("#counter pre[data-value='0']") - new_page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#counter pre[data-value='1']") - new_page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#counter pre[data-value='2']") - new_page.wait_for_selector("#counter .minus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#counter pre[data-value='1']") - - new_page.wait_for_selector("#parent") - new_page.wait_for_selector("#child") - new_page.wait_for_selector("#child pre[data-value='0']") - new_page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#child pre[data-value='1']") - new_page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#child pre[data-value='2']") - new_page.wait_for_selector("#child .minus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#child pre[data-value='1']") - - new_page.wait_for_selector("#parent-toggle") - new_page.wait_for_selector("#parent-toggle button").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#parent-toggle") - new_page.wait_for_selector("#parent-toggle pre[data-value='0']") - new_page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#parent-toggle pre[data-value='1']") - new_page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#parent-toggle pre[data-value='2']") - new_page.wait_for_selector("#parent-toggle .minus").click(delay=CLICK_DELAY) - new_page.wait_for_selector("#parent-toggle pre[data-value='1']") - - new_page.wait_for_selector("#moment[data-success=true]") - finally: - new_page.close() + sender = self.page.wait_for_selector("#sender") + sender.type("test", delay=CLICK_DELAY) + sender.press("Enter", delay=CLICK_DELAY) + receiver = self.page.wait_for_selector("#receiver[data-message='test']") + self.assertIsNotNone(receiver) + + sender = self.page.wait_for_selector("#group-sender") + sender.type("1234", delay=CLICK_DELAY) + sender.press("Enter", delay=CLICK_DELAY) + receiver_1 = self.page.wait_for_selector( + "#group-receiver-1[data-message='1234']" + ) + receiver_2 = self.page.wait_for_selector( + "#group-receiver-2[data-message='1234']" + ) + receiver_3 = self.page.wait_for_selector( + "#group-receiver-3[data-message='1234']" + ) + self.assertIsNotNone(receiver_1) + self.assertIsNotNone(receiver_2) + self.assertIsNotNone(receiver_3) + + +class PyscriptTests(PlaywrightTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.page.goto(f"http://{cls.host}:{cls._port}/pyscript/") + + def test_0_hello_world(self): + self.page.wait_for_selector("#hello-world-loading") + self.page.wait_for_selector("#hello-world") + + def test_1_custom_root(self): + self.page.wait_for_selector("#custom-root") + + def test_1_multifile(self): + self.page.wait_for_selector("#multifile-parent") + self.page.wait_for_selector("#multifile-child") + + def test_1_counter(self): + self.page.wait_for_selector("#counter") + self.page.wait_for_selector("#counter pre[data-value='0']") + self.page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#counter pre[data-value='1']") + self.page.wait_for_selector("#counter .plus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#counter pre[data-value='2']") + self.page.wait_for_selector("#counter .minus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#counter pre[data-value='1']") + + def test_1_server_side_parent(self): + self.page.wait_for_selector("#parent") + self.page.wait_for_selector("#child") + self.page.wait_for_selector("#child pre[data-value='0']") + self.page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#child pre[data-value='1']") + self.page.wait_for_selector("#child .plus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#child pre[data-value='2']") + self.page.wait_for_selector("#child .minus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#child pre[data-value='1']") + + def test_1_server_side_parent_with_toggle(self): + self.page.wait_for_selector("#parent-toggle") + self.page.wait_for_selector("#parent-toggle button").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#parent-toggle") + self.page.wait_for_selector("#parent-toggle pre[data-value='0']") + self.page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#parent-toggle pre[data-value='1']") + self.page.wait_for_selector("#parent-toggle .plus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#parent-toggle pre[data-value='2']") + self.page.wait_for_selector("#parent-toggle .minus").click(delay=CLICK_DELAY) + self.page.wait_for_selector("#parent-toggle pre[data-value='1']") + + def test_1_javascript_module_execution_within_pyscript(self): + self.page.wait_for_selector("#moment[data-success=true]") + + +class DistributedComputingTests(PlaywrightTestCase): + + @classmethod + def setUpServer(cls): + super().setUpServer() + cls._server_process2 = cls.ProtocolServerProcess(cls.host, cls.get_application) + cls._server_process2.start() + cls._server_process2.ready.wait() + cls._port2 = cls._server_process2.port.value + + @classmethod + def tearDownServer(cls): + super().tearDownServer() + cls._server_process2.terminate() + cls._server_process2.join() + + def test_host_roundrobin(self): + """Verify if round-robin host selection is working.""" + self.page.goto( + f"{self.live_server_url}/roundrobin/{self._port}/{self._port2}/8" + ) + elem0 = self.page.locator(".custom_host-0") + elem1 = self.page.locator(".custom_host-1") + elem2 = self.page.locator(".custom_host-2") + elem3 = self.page.locator(".custom_host-3") + + elem0.wait_for() + elem1.wait_for() + elem2.wait_for() + elem3.wait_for() + + current_ports = { + elem0.get_attribute("data-port"), + elem1.get_attribute("data-port"), + elem2.get_attribute("data-port"), + elem3.get_attribute("data-port"), + } + correct_ports = { + str(self._port), + str(self._port2), + } + + # There should only be two ports in the set + self.assertEqual(current_ports, correct_ports) + self.assertEqual(len(current_ports), 2) + + def test_custom_host(self): + """Make sure that the component is rendered by a separate server.""" + self.page.goto(f"{self.live_server_url}/port/{self._port2}/") + elem = self.page.locator(".custom_host-0") + elem.wait_for() + self.assertIn( + f"Server Port: {self._port2}", + elem.text_content(), + ) + + def test_custom_host_wrong_port(self): + """Make sure that other ports are not rendering components.""" + tmp_sock = socket.socket() + tmp_sock.bind((self._server_process.host, 0)) + random_port = tmp_sock.getsockname()[1] + self.page.goto(f"{self.live_server_url}/port/{random_port}/") + with self.assertRaises(TimeoutError): + self.page.locator(".custom_host").wait_for(timeout=1000) + + +class OfflineTests(PlaywrightTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.page.goto(f"http://{cls.host}:{cls._port}/offline/") + + def test_offline_components(self): + self.page.wait_for_selector("div:not([hidden]) > #online") + self.assertIsNotNone(self.page.query_selector("div[hidden] > #offline")) + self._server_process.terminate() + self._server_process.join() + self.page.wait_for_selector("div:not([hidden]) > #offline") + self.assertIsNotNone(self.page.query_selector("div[hidden] > #online")) diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index 6daa516f..83e34ccb 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -4,6 +4,7 @@ import dill as pickle from django.test import TransactionTestCase + from reactpy_django import clean from reactpy_django.models import ComponentSession, UserDataModel from reactpy_django.types import ComponentParams diff --git a/tests/test_app/tests/test_regex.py b/tests/test_app/tests/test_regex.py index bf567413..5c3ec95a 100644 --- a/tests/test_app/tests/test_regex.py +++ b/tests/test_app/tests/test_regex.py @@ -1,6 +1,7 @@ import re from django.test import TestCase + from reactpy_django.utils import COMMENT_REGEX, COMPONENT_REGEX @@ -21,10 +22,10 @@ def test_component_regex(self): ) self.assertRegex( r"""{% - component - "my.component" - class="my_thing" - attr="attribute" + component + "my.component" + class="my_thing" + attr="attribute" %}""", # noqa: W291 COMPONENT_REGEX, @@ -84,10 +85,10 @@ def test_comment_regex(self): COMMENT_REGEX, ) self.assertRegex( - r"""""", # noqa: W291 COMMENT_REGEX, ) @@ -138,8 +139,8 @@ def test_comment_regex(self): COMMENT_REGEX.sub( "", r"""""", # noqa: W291 ), "", diff --git a/tests/test_app/tests/utils.py b/tests/test_app/tests/utils.py new file mode 100644 index 00000000..fe32d97d --- /dev/null +++ b/tests/test_app/tests/utils.py @@ -0,0 +1,92 @@ +import asyncio +import os +import sys +from functools import partial + +from channels.testing import ChannelsLiveServerTestCase +from channels.testing.live import make_application +from django.core.exceptions import ImproperlyConfigured +from django.core.management import call_command +from django.db import connections +from django.test.utils import modify_settings +from playwright.sync_api import sync_playwright + +from reactpy_django.utils import strtobool + +GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS", "False") + + +class PlaywrightTestCase(ChannelsLiveServerTestCase): + + from reactpy_django import config + + databases = {"default"} + + @classmethod + def setUpClass(cls): + # Repurposed from ChannelsLiveServerTestCase._pre_setup + for connection in connections.all(): + if cls._is_in_memory_db(cls, connection): + raise ImproperlyConfigured( + "ChannelLiveServerTestCase can not be used with in memory databases" + ) + cls._live_server_modified_settings = modify_settings( + ALLOWED_HOSTS={"append": cls.host} + ) + cls._live_server_modified_settings.enable() + cls.get_application = partial( + make_application, + static_wrapper=cls.static_wrapper if cls.serve_static else None, + ) + cls.setUpServer() + + # Open a Playwright browser window + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy()) + cls.playwright = sync_playwright().start() + headless = strtobool(os.environ.get("PLAYWRIGHT_HEADLESS", GITHUB_ACTIONS)) + cls.browser = cls.playwright.chromium.launch(headless=bool(headless)) + cls.page = cls.browser.new_page() + cls.page.set_default_timeout(10000) + + @classmethod + def setUpServer(cls): + cls._server_process = cls.ProtocolServerProcess(cls.host, cls.get_application) + cls._server_process.start() + cls._server_process.ready.wait() + cls._port = cls._server_process.port.value + + @classmethod + def tearDownClass(cls): + from reactpy_django import config + + # Close the Playwright browser + cls.playwright.stop() + + # Close the other server processes + cls.tearDownServer() + + # Repurposed from ChannelsLiveServerTestCase._post_teardown + cls._live_server_modified_settings.disable() + # Using set to prevent duplicates + for db_name in {"default", config.REACTPY_DATABASE}: + call_command( + "flush", + verbosity=0, + interactive=False, + database=db_name, + reset_sequences=False, + ) + + @classmethod + def tearDownServer(cls): + cls._server_process.terminate() + cls._server_process.join() + + def _pre_setup(self): + """Handled manually in `setUpClass` to speed things up.""" + + def _post_teardown(self): + """Handled manually in `tearDownClass` to prevent TransactionTestCase from doing + database flushing. This is needed to prevent a `SynchronousOnlyOperation` from + occurring due to a bug within `ChannelsLiveServerTestCase`.""" From a85164cfaba95b3515d1ba2a8ea9c2396e1d61bf Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 2 Dec 2024 01:32:55 -0800 Subject: [PATCH 17/35] Replace `mypy` and `black` with `ruff` (#262) --- .github/workflows/test-docs.yml | 2 +- .github/workflows/test-python.yml | 15 ++ CHANGELOG.md | 3 + ...component.html => pyscript_component.html} | 0 ...ject.html => pyscript_initial_object.html} | 0 ...ring.html => pyscript_initial_string.html} | 0 ...import.html => pyscript_local_import.html} | 0 ...iles.html => pyscript_multiple_files.html} | 0 ...{pyscript-root.html => pyscript_root.html} | 0 ...yscript-setup.html => pyscript_setup.html} | 0 ...html => pyscript_setup_config_object.html} | 0 ...html => pyscript_setup_config_string.html} | 0 ....html => pyscript_setup_dependencies.html} | 0 ...ml => pyscript_setup_extra_js_object.html} | 0 ...ml => pyscript_setup_extra_js_string.html} | 0 ... => pyscript_setup_local_interpreter.html} | 0 ...r-parent.html => pyscript_ssr_parent.html} | 0 .../{pyscript-tag.html => pyscript_tag.html} | 0 .../{configure-asgi.py => configure_asgi.py} | 11 +- ...leware.py => configure_asgi_middleware.py} | 11 +- ...-app.py => configure_channels_asgi_app.py} | 0 ...py => configure_channels_installed_app.py} | 0 ...ed-apps.py => configure_installed_apps.py} | 0 .../{configure-urls.py => configure_urls.py} | 0 .../python/{django-css.py => django_css.py} | 1 + ...al-link.py => django_css_external_link.py} | 4 +- ...local-link.py => django_css_local_link.py} | 0 .../python/{django-js.py => django_js.py} | 1 + ...al-script.py => django_js_local_script.py} | 0 ...e-script.py => django_js_remote_script.py} | 0 ...essor.py => django_query_postprocessor.py} | 3 +- .../{django-router.py => django_router.py} | 3 +- docs/examples/python/example/__init__.py | 3 + docs/examples/python/example/components.py | 6 + docs/examples/python/example/views.py | 9 +- .../python/{example/urls.py => first_urls.py} | 1 + docs/examples/python/first_view.py | 5 + .../python/hello_world_app_config_cbv.py | 4 +- .../python/hello_world_app_config_fbv.py | 4 +- ...dide-js-module.py => pyodide_js_module.py} | 5 +- ...y => pyscript_component_initial_object.py} | 1 + ...y => pyscript_component_initial_string.py} | 1 + ...pyscript_component_multiple_files_root.py} | 1 + ...ent-root.py => pyscript_component_root.py} | 1 + ...hello-world.py => pyscript_hello_world.py} | 0 ...l-object.py => pyscript_initial_object.py} | 0 ...cal-import.py => pyscript_local_import.py} | 0 ...ld.py => pyscript_multiple_files_child.py} | 0 ...oot.py => pyscript_multiple_files_root.py} | 5 +- .../{pyscript-root.py => pyscript_root.py} | 0 ...ect.py => pyscript_setup_config_object.py} | 0 ...t.py => pyscript_setup_extra_js_object.py} | 0 ...ipt-ssr-child.py => pyscript_ssr_child.py} | 0 ...t-ssr-parent.py => pyscript_ssr_parent.py} | 1 + .../{pyscript-tag.py => pyscript_tag.py} | 1 + ...ter-component.py => register_component.py} | 1 + ...-kwargs.py => template_tag_args_kwargs.py} | 0 ...g-bad-view.py => template_tag_bad_view.py} | 0 .../{example/models.py => todo_item_model.py} | 0 ...-channel-layer.py => use_channel_layer.py} | 1 + ...er-group.py => use_channel_layer_group.py} | 1 + ...y => use_channel_layer_signal_receiver.py} | 1 + ....py => use_channel_layer_signal_sender.py} | 0 .../{use-connection.py => use_connection.py} | 1 + .../{use-location.py => use_location.py} | 1 + .../{use-mutation.py => use_mutation.py} | 3 +- ...-kwargs.py => use_mutation_args_kwargs.py} | 6 +- ...fetch.py => use_mutation_query_refetch.py} | 7 +- ...utation-reset.py => use_mutation_reset.py} | 3 +- ...ve.py => use_mutation_thread_sensitive.py} | 2 +- .../python/{use-origin.py => use_origin.py} | 1 + .../python/{use-query.py => use_query.py} | 7 +- .../{use-query-args.py => use_query_args.py} | 1 + ...e.py => use_query_postprocessor_change.py} | 2 +- ....py => use_query_postprocessor_disable.py} | 2 +- ...s.py => use_query_postprocessor_kwargs.py} | 3 +- ...itive.py => use_query_thread_sensitive.py} | 2 +- .../python/{use-root-id.py => use_root_id.py} | 1 + .../python/{use-scope.py => use_scope.py} | 1 + .../python/{use-user.py => use_user.py} | 1 + .../{use-user-data.py => use_user_data.py} | 1 + ...-defaults.py => use_user_data_defaults.py} | 1 + ...ser-passes-test.py => user_passes_test.py} | 1 + ...=> user_passes_test_component_fallback.py} | 1 + ...k.py => user_passes_test_vdom_fallback.py} | 1 + docs/examples/python/views.py | 7 - docs/examples/python/vtc.py | 4 +- .../python/{vtc-args.py => vtc_args.py} | 4 +- .../python/{vtc-cbv.py => vtc_cbv.py} | 4 +- ...trict-parsing.py => vtc_strict_parsing.py} | 4 +- .../{vtc-transforms.py => vtc_transforms.py} | 8 +- docs/examples/python/vti.py | 4 +- .../python/{vti-args.py => vti_args.py} | 4 +- .../python/{vti-cbv.py => vti_cbv.py} | 4 +- ...{vti-extra-props.py => vti_extra_props.py} | 8 +- docs/overrides/home.html | 12 +- .../add_interactivity.py} | 7 +- .../add_interactivity_demo.html} | 0 .../code_block.html} | 0 .../create_user_interfaces.py} | 1 + .../create_user_interfaces_demo.html} | 0 .../write_components_with_python.py} | 1 + .../write_components_with_python_demo.html} | 0 docs/src/about/contributing.md | 2 +- .../learn/add-reactpy-to-a-django-project.md | 12 +- docs/src/learn/your-first-component.md | 6 +- docs/src/reference/components.md | 56 ++--- docs/src/reference/decorators.md | 6 +- docs/src/reference/hooks.md | 62 ++--- docs/src/reference/html.md | 6 +- docs/src/reference/router.md | 2 +- docs/src/reference/settings.md | 4 +- docs/src/reference/template-tag.md | 54 ++-- docs/src/reference/utils.md | 10 +- pyproject.toml | 10 +- src/build_scripts/copy_dir.py | 6 +- src/reactpy_django/__init__.py | 6 +- src/reactpy_django/checks.py | 98 ++------ src/reactpy_django/clean.py | 28 +-- src/reactpy_django/components.py | 42 ++-- src/reactpy_django/config.py | 30 +-- src/reactpy_django/database.py | 9 +- src/reactpy_django/decorators.py | 9 +- src/reactpy_django/exceptions.py | 27 +- src/reactpy_django/hooks.py | 115 ++++----- src/reactpy_django/http/urls.py | 2 +- src/reactpy_django/http/views.py | 34 +-- .../management/commands/clean_reactpy.py | 5 +- src/reactpy_django/models.py | 12 +- .../pyscript/component_template.py | 1 + src/reactpy_django/pyscript/layout_handler.py | 55 ++-- src/reactpy_django/router/converters.py | 3 +- src/reactpy_django/router/resolvers.py | 6 +- src/reactpy_django/templatetags/reactpy.py | 38 ++- src/reactpy_django/types.py | 4 +- src/reactpy_django/utils.py | 175 ++++++------- src/reactpy_django/websocket/consumer.py | 45 ++-- src/reactpy_django/websocket/paths.py | 3 +- tests/manage.py | 8 +- tests/test_app/__init__.py | 22 +- tests/test_app/admin.py | 3 +- tests/test_app/asgi.py | 11 +- tests/test_app/channel_layers/components.py | 7 +- tests/test_app/components.py | 236 ++++++------------ tests/test_app/middleware.py | 4 +- tests/test_app/migrations/0001_initial.py | 2 +- tests/test_app/models.py | 34 ++- tests/test_app/offline/components.py | 3 +- tests/test_app/performance/components.py | 29 +-- tests/test_app/performance/urls.py | 1 - tests/test_app/prerender/components.py | 14 +- tests/test_app/pyscript/components/child.py | 8 +- tests/test_app/pyscript/components/counter.py | 8 +- .../pyscript/components/multifile_parent.py | 1 + .../pyscript/components/server_side.py | 5 +- tests/test_app/settings_multi_db.py | 14 +- tests/test_app/settings_single_db.py | 18 +- tests/test_app/templates/channel_layers.html | 6 +- .../tests/js/button-from-js-module.js | 30 +-- tests/test_app/tests/test_components.py | 220 ++++++---------- tests/test_app/tests/test_database.py | 31 ++- tests/test_app/tests/test_regex.py | 187 +++++--------- tests/test_app/tests/utils.py | 13 +- tests/test_app/views.py | 18 +- 164 files changed, 915 insertions(+), 1228 deletions(-) rename docs/examples/html/{pyscript-component.html => pyscript_component.html} (100%) rename docs/examples/html/{pyscript-initial-object.html => pyscript_initial_object.html} (100%) rename docs/examples/html/{pyscript-initial-string.html => pyscript_initial_string.html} (100%) rename docs/examples/html/{pyscript-local-import.html => pyscript_local_import.html} (100%) rename docs/examples/html/{pyscript-multiple-files.html => pyscript_multiple_files.html} (100%) rename docs/examples/html/{pyscript-root.html => pyscript_root.html} (100%) rename docs/examples/html/{pyscript-setup.html => pyscript_setup.html} (100%) rename docs/examples/html/{pyscript-setup-config-object.html => pyscript_setup_config_object.html} (100%) rename docs/examples/html/{pyscript-setup-config-string.html => pyscript_setup_config_string.html} (100%) rename docs/examples/html/{pyscript-setup-dependencies.html => pyscript_setup_dependencies.html} (100%) rename docs/examples/html/{pyscript-setup-extra-js-object.html => pyscript_setup_extra_js_object.html} (100%) rename docs/examples/html/{pyscript-setup-extra-js-string.html => pyscript_setup_extra_js_string.html} (100%) rename docs/examples/html/{pyscript-setup-local-interpreter.html => pyscript_setup_local_interpreter.html} (100%) rename docs/examples/html/{pyscript-ssr-parent.html => pyscript_ssr_parent.html} (100%) rename docs/examples/html/{pyscript-tag.html => pyscript_tag.html} (100%) rename docs/examples/python/{configure-asgi.py => configure_asgi.py} (77%) rename docs/examples/python/{configure-asgi-middleware.py => configure_asgi_middleware.py} (60%) rename docs/examples/python/{configure-channels-asgi-app.py => configure_channels_asgi_app.py} (100%) rename docs/examples/python/{configure-channels-installed-app.py => configure_channels_installed_app.py} (100%) rename docs/examples/python/{configure-installed-apps.py => configure_installed_apps.py} (100%) rename docs/examples/python/{configure-urls.py => configure_urls.py} (100%) rename docs/examples/python/{django-css.py => django_css.py} (99%) rename docs/examples/python/{django-css-external-link.py => django_css_external_link.py} (53%) rename docs/examples/python/{django-css-local-link.py => django_css_local_link.py} (100%) rename docs/examples/python/{django-js.py => django_js.py} (99%) rename docs/examples/python/{django-js-local-script.py => django_js_local_script.py} (100%) rename docs/examples/python/{django-js-remote-script.py => django_js_remote_script.py} (100%) rename docs/examples/python/{django-query-postprocessor.py => django_query_postprocessor.py} (99%) rename docs/examples/python/{django-router.py => django_router.py} (99%) create mode 100644 docs/examples/python/example/components.py rename docs/examples/python/{example/urls.py => first_urls.py} (99%) create mode 100644 docs/examples/python/first_view.py rename docs/examples/python/{pyodide-js-module.py => pyodide_js_module.py} (58%) rename docs/examples/python/{pyscript-component-initial-object.py => pyscript_component_initial_object.py} (99%) rename docs/examples/python/{pyscript-component-initial-string.py => pyscript_component_initial_string.py} (99%) rename docs/examples/python/{pyscript-component-multiple-files-root.py => pyscript_component_multiple_files_root.py} (99%) rename docs/examples/python/{pyscript-component-root.py => pyscript_component_root.py} (99%) rename docs/examples/python/{pyscript-hello-world.py => pyscript_hello_world.py} (100%) rename docs/examples/python/{pyscript-initial-object.py => pyscript_initial_object.py} (100%) rename docs/examples/python/{pyscript-local-import.py => pyscript_local_import.py} (100%) rename docs/examples/python/{pyscript-multiple-files-child.py => pyscript_multiple_files_child.py} (100%) rename docs/examples/python/{pyscript-multiple-files-root.py => pyscript_multiple_files_root.py} (60%) rename docs/examples/python/{pyscript-root.py => pyscript_root.py} (100%) rename docs/examples/python/{pyscript-setup-config-object.py => pyscript_setup_config_object.py} (100%) rename docs/examples/python/{pyscript-setup-extra-js-object.py => pyscript_setup_extra_js_object.py} (100%) rename docs/examples/python/{pyscript-ssr-child.py => pyscript_ssr_child.py} (100%) rename docs/examples/python/{pyscript-ssr-parent.py => pyscript_ssr_parent.py} (99%) rename docs/examples/python/{pyscript-tag.py => pyscript_tag.py} (99%) rename docs/examples/python/{register-component.py => register_component.py} (99%) rename docs/examples/python/{template-tag-args-kwargs.py => template_tag_args_kwargs.py} (100%) rename docs/examples/python/{template-tag-bad-view.py => template_tag_bad_view.py} (100%) rename docs/examples/python/{example/models.py => todo_item_model.py} (100%) rename docs/examples/python/{use-channel-layer.py => use_channel_layer.py} (99%) rename docs/examples/python/{use-channel-layer-group.py => use_channel_layer_group.py} (99%) rename docs/examples/python/{use-channel-layer-signal-receiver.py => use_channel_layer_signal_receiver.py} (99%) rename docs/examples/python/{use-channel-layer-signal-sender.py => use_channel_layer_signal_sender.py} (100%) rename docs/examples/python/{use-connection.py => use_connection.py} (99%) rename docs/examples/python/{use-location.py => use_location.py} (99%) rename docs/examples/python/{use-mutation.py => use_mutation.py} (99%) rename docs/examples/python/{use-mutation-args-kwargs.py => use_mutation_args_kwargs.py} (71%) rename docs/examples/python/{use-mutation-query-refetch.py => use_mutation_query_refetch.py} (90%) rename docs/examples/python/{use-mutation-reset.py => use_mutation_reset.py} (99%) rename docs/examples/python/{use-mutation-thread-sensitive.py => use_mutation_thread_sensitive.py} (98%) rename docs/examples/python/{use-origin.py => use_origin.py} (99%) rename docs/examples/python/{use-query.py => use_query.py} (82%) rename docs/examples/python/{use-query-args.py => use_query_args.py} (99%) rename docs/examples/python/{use-query-postprocessor-change.py => use_query_postprocessor_change.py} (98%) rename docs/examples/python/{use-query-postprocessor-disable.py => use_query_postprocessor_disable.py} (97%) rename docs/examples/python/{use-query-postprocessor-kwargs.py => use_query_postprocessor_kwargs.py} (99%) rename docs/examples/python/{use-query-thread-sensitive.py => use_query_thread_sensitive.py} (97%) rename docs/examples/python/{use-root-id.py => use_root_id.py} (99%) rename docs/examples/python/{use-scope.py => use_scope.py} (99%) rename docs/examples/python/{use-user.py => use_user.py} (99%) rename docs/examples/python/{use-user-data.py => use_user_data.py} (99%) rename docs/examples/python/{use-user-data-defaults.py => use_user_data_defaults.py} (99%) rename docs/examples/python/{user-passes-test.py => user_passes_test.py} (99%) rename docs/examples/python/{user-passes-test-component-fallback.py => user_passes_test_component_fallback.py} (99%) rename docs/examples/python/{user-passes-test-vdom-fallback.py => user_passes_test_vdom_fallback.py} (99%) delete mode 100644 docs/examples/python/views.py rename docs/examples/python/{vtc-args.py => vtc_args.py} (95%) rename docs/examples/python/{vtc-cbv.py => vtc_cbv.py} (90%) rename docs/examples/python/{vtc-strict-parsing.py => vtc_strict_parsing.py} (90%) rename docs/examples/python/{vtc-transforms.py => vtc_transforms.py} (75%) rename docs/examples/python/{vti-args.py => vti_args.py} (93%) rename docs/examples/python/{vti-cbv.py => vti_cbv.py} (90%) rename docs/examples/python/{vti-extra-props.py => vti_extra_props.py} (60%) rename docs/overrides/{home-code-examples/add-interactivity.py => homepage_examples/add_interactivity.py} (74%) rename docs/overrides/{home-code-examples/add-interactivity-demo.html => homepage_examples/add_interactivity_demo.html} (100%) rename docs/overrides/{home-code-examples/code-block.html => homepage_examples/code_block.html} (100%) rename docs/overrides/{home-code-examples/create-user-interfaces.py => homepage_examples/create_user_interfaces.py} (94%) rename docs/overrides/{home-code-examples/create-user-interfaces-demo.html => homepage_examples/create_user_interfaces_demo.html} (100%) rename docs/overrides/{home-code-examples/write-components-with-python.py => homepage_examples/write_components_with_python.py} (94%) rename docs/overrides/{home-code-examples/write-components-with-python-demo.html => homepage_examples/write_components_with_python_demo.html} (100%) diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 08bfadd7..0babadbc 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -29,4 +29,4 @@ jobs: - name: Check docs build run: hatch run docs:build - name: Check docs examples - run: hatch run docs:check_examples + run: hatch fmt docs --check diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 9fe700b8..8faca864 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -31,3 +31,18 @@ jobs: run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_single_db -v - name: Run Multi-DB Tests run: hatch test --python ${{ matrix.python-version }} --ds=test_app.settings_multi_db -v + + python-formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - name: Install Python Dependencies + run: pip install --upgrade pip hatch uv + - name: Check Python formatting + run: hatch fmt src tests --check diff --git a/CHANGELOG.md b/CHANGELOG.md index ec8d38ff..399d3668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ Don't forget to remove deprecated code on each major release! ### Changed - Set upper limit on ReactPy version to `<2.0.0`. +- ReactPy web modules are now streamed in chunks. +- ReactPy web modules are now streamed using asynchronous file reading to improve performance. +- Performed refactoring to utilize `ruff` as this repository's linter. ## [5.1.0] - 2024-11-24 diff --git a/docs/examples/html/pyscript-component.html b/docs/examples/html/pyscript_component.html similarity index 100% rename from docs/examples/html/pyscript-component.html rename to docs/examples/html/pyscript_component.html diff --git a/docs/examples/html/pyscript-initial-object.html b/docs/examples/html/pyscript_initial_object.html similarity index 100% rename from docs/examples/html/pyscript-initial-object.html rename to docs/examples/html/pyscript_initial_object.html diff --git a/docs/examples/html/pyscript-initial-string.html b/docs/examples/html/pyscript_initial_string.html similarity index 100% rename from docs/examples/html/pyscript-initial-string.html rename to docs/examples/html/pyscript_initial_string.html diff --git a/docs/examples/html/pyscript-local-import.html b/docs/examples/html/pyscript_local_import.html similarity index 100% rename from docs/examples/html/pyscript-local-import.html rename to docs/examples/html/pyscript_local_import.html diff --git a/docs/examples/html/pyscript-multiple-files.html b/docs/examples/html/pyscript_multiple_files.html similarity index 100% rename from docs/examples/html/pyscript-multiple-files.html rename to docs/examples/html/pyscript_multiple_files.html diff --git a/docs/examples/html/pyscript-root.html b/docs/examples/html/pyscript_root.html similarity index 100% rename from docs/examples/html/pyscript-root.html rename to docs/examples/html/pyscript_root.html diff --git a/docs/examples/html/pyscript-setup.html b/docs/examples/html/pyscript_setup.html similarity index 100% rename from docs/examples/html/pyscript-setup.html rename to docs/examples/html/pyscript_setup.html diff --git a/docs/examples/html/pyscript-setup-config-object.html b/docs/examples/html/pyscript_setup_config_object.html similarity index 100% rename from docs/examples/html/pyscript-setup-config-object.html rename to docs/examples/html/pyscript_setup_config_object.html diff --git a/docs/examples/html/pyscript-setup-config-string.html b/docs/examples/html/pyscript_setup_config_string.html similarity index 100% rename from docs/examples/html/pyscript-setup-config-string.html rename to docs/examples/html/pyscript_setup_config_string.html diff --git a/docs/examples/html/pyscript-setup-dependencies.html b/docs/examples/html/pyscript_setup_dependencies.html similarity index 100% rename from docs/examples/html/pyscript-setup-dependencies.html rename to docs/examples/html/pyscript_setup_dependencies.html diff --git a/docs/examples/html/pyscript-setup-extra-js-object.html b/docs/examples/html/pyscript_setup_extra_js_object.html similarity index 100% rename from docs/examples/html/pyscript-setup-extra-js-object.html rename to docs/examples/html/pyscript_setup_extra_js_object.html diff --git a/docs/examples/html/pyscript-setup-extra-js-string.html b/docs/examples/html/pyscript_setup_extra_js_string.html similarity index 100% rename from docs/examples/html/pyscript-setup-extra-js-string.html rename to docs/examples/html/pyscript_setup_extra_js_string.html diff --git a/docs/examples/html/pyscript-setup-local-interpreter.html b/docs/examples/html/pyscript_setup_local_interpreter.html similarity index 100% rename from docs/examples/html/pyscript-setup-local-interpreter.html rename to docs/examples/html/pyscript_setup_local_interpreter.html diff --git a/docs/examples/html/pyscript-ssr-parent.html b/docs/examples/html/pyscript_ssr_parent.html similarity index 100% rename from docs/examples/html/pyscript-ssr-parent.html rename to docs/examples/html/pyscript_ssr_parent.html diff --git a/docs/examples/html/pyscript-tag.html b/docs/examples/html/pyscript_tag.html similarity index 100% rename from docs/examples/html/pyscript-tag.html rename to docs/examples/html/pyscript_tag.html diff --git a/docs/examples/python/configure-asgi.py b/docs/examples/python/configure_asgi.py similarity index 77% rename from docs/examples/python/configure-asgi.py rename to docs/examples/python/configure_asgi.py index 8081d747..8feb0ec2 100644 --- a/docs/examples/python/configure-asgi.py +++ b/docs/examples/python/configure_asgi.py @@ -10,11 +10,10 @@ from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 + from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402 -application = ProtocolTypeRouter( - { - "http": django_asgi_app, - "websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]), - } -) +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]), +}) diff --git a/docs/examples/python/configure-asgi-middleware.py b/docs/examples/python/configure_asgi_middleware.py similarity index 60% rename from docs/examples/python/configure-asgi-middleware.py rename to docs/examples/python/configure_asgi_middleware.py index 6df35a39..0c5a7214 100644 --- a/docs/examples/python/configure-asgi-middleware.py +++ b/docs/examples/python/configure_asgi_middleware.py @@ -1,5 +1,6 @@ # Broken load order, only used for linting from channels.routing import ProtocolTypeRouter, URLRouter + from reactpy_django import REACTPY_WEBSOCKET_ROUTE django_asgi_app = "" @@ -8,9 +9,7 @@ # start from channels.auth import AuthMiddlewareStack # noqa: E402 -application = ProtocolTypeRouter( - { - "http": django_asgi_app, - "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), - } -) +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), +}) diff --git a/docs/examples/python/configure-channels-asgi-app.py b/docs/examples/python/configure_channels_asgi_app.py similarity index 100% rename from docs/examples/python/configure-channels-asgi-app.py rename to docs/examples/python/configure_channels_asgi_app.py diff --git a/docs/examples/python/configure-channels-installed-app.py b/docs/examples/python/configure_channels_installed_app.py similarity index 100% rename from docs/examples/python/configure-channels-installed-app.py rename to docs/examples/python/configure_channels_installed_app.py diff --git a/docs/examples/python/configure-installed-apps.py b/docs/examples/python/configure_installed_apps.py similarity index 100% rename from docs/examples/python/configure-installed-apps.py rename to docs/examples/python/configure_installed_apps.py diff --git a/docs/examples/python/configure-urls.py b/docs/examples/python/configure_urls.py similarity index 100% rename from docs/examples/python/configure-urls.py rename to docs/examples/python/configure_urls.py diff --git a/docs/examples/python/django-css.py b/docs/examples/python/django_css.py similarity index 99% rename from docs/examples/python/django-css.py rename to docs/examples/python/django_css.py index aeb4addb..c7f60881 100644 --- a/docs/examples/python/django-css.py +++ b/docs/examples/python/django_css.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import django_css diff --git a/docs/examples/python/django-css-external-link.py b/docs/examples/python/django_css_external_link.py similarity index 53% rename from docs/examples/python/django-css-external-link.py rename to docs/examples/python/django_css_external_link.py index ac1d0fba..28eb3fca 100644 --- a/docs/examples/python/django-css-external-link.py +++ b/docs/examples/python/django_css_external_link.py @@ -4,8 +4,6 @@ @component def my_component(): return html.div( - html.link( - {"rel": "stylesheet", "href": "https://example.com/external-styles.css"} - ), + html.link({"rel": "stylesheet", "href": "https://example.com/external-styles.css"}), html.button("My Button!"), ) diff --git a/docs/examples/python/django-css-local-link.py b/docs/examples/python/django_css_local_link.py similarity index 100% rename from docs/examples/python/django-css-local-link.py rename to docs/examples/python/django_css_local_link.py diff --git a/docs/examples/python/django-js.py b/docs/examples/python/django_js.py similarity index 99% rename from docs/examples/python/django-js.py rename to docs/examples/python/django_js.py index b4af014c..37868184 100644 --- a/docs/examples/python/django-js.py +++ b/docs/examples/python/django_js.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import django_js diff --git a/docs/examples/python/django-js-local-script.py b/docs/examples/python/django_js_local_script.py similarity index 100% rename from docs/examples/python/django-js-local-script.py rename to docs/examples/python/django_js_local_script.py diff --git a/docs/examples/python/django-js-remote-script.py b/docs/examples/python/django_js_remote_script.py similarity index 100% rename from docs/examples/python/django-js-remote-script.py rename to docs/examples/python/django_js_remote_script.py diff --git a/docs/examples/python/django-query-postprocessor.py b/docs/examples/python/django_query_postprocessor.py similarity index 99% rename from docs/examples/python/django-query-postprocessor.py rename to docs/examples/python/django_query_postprocessor.py index da33c362..7bdc870c 100644 --- a/docs/examples/python/django-query-postprocessor.py +++ b/docs/examples/python/django_query_postprocessor.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component + +from example.models import TodoItem from reactpy_django.hooks import use_query from reactpy_django.utils import django_query_postprocessor diff --git a/docs/examples/python/django-router.py b/docs/examples/python/django_router.py similarity index 99% rename from docs/examples/python/django-router.py rename to docs/examples/python/django_router.py index 5c845967..e37ae0a8 100644 --- a/docs/examples/python/django-router.py +++ b/docs/examples/python/django_router.py @@ -1,7 +1,8 @@ from reactpy import component, html -from reactpy_django.router import django_router from reactpy_router import route +from reactpy_django.router import django_router + @component def my_component(): diff --git a/docs/examples/python/example/__init__.py b/docs/examples/python/example/__init__.py index e69de29b..c32d6329 100644 --- a/docs/examples/python/example/__init__.py +++ b/docs/examples/python/example/__init__.py @@ -0,0 +1,3 @@ +"""This module exists only to satisfy type checkers. + +Do not use the files in this module as examples within the docs.""" diff --git a/docs/examples/python/example/components.py b/docs/examples/python/example/components.py new file mode 100644 index 00000000..ec301524 --- /dev/null +++ b/docs/examples/python/example/components.py @@ -0,0 +1,6 @@ +"""This module exists only to satisfy type checkers. + +Do not use the files in this module as examples within the docs.""" + + +def child_component(): ... diff --git a/docs/examples/python/example/views.py b/docs/examples/python/example/views.py index 23e21130..49bfeb8e 100644 --- a/docs/examples/python/example/views.py +++ b/docs/examples/python/example/views.py @@ -1,5 +1,8 @@ -from django.shortcuts import render +"""This module exists only to satisfy type checkers. +Do not use the files in this module as examples within the docs.""" -def index(request): - return render(request, "my_template.html") +from python.hello_world_cbv import HelloWorld +from python.hello_world_fbv import hello_world + +__all__ = ["HelloWorld", "hello_world"] diff --git a/docs/examples/python/example/urls.py b/docs/examples/python/first_urls.py similarity index 99% rename from docs/examples/python/example/urls.py rename to docs/examples/python/first_urls.py index 74f72806..a0f1d72f 100644 --- a/docs/examples/python/example/urls.py +++ b/docs/examples/python/first_urls.py @@ -1,4 +1,5 @@ from django.urls import path + from example import views urlpatterns = [ diff --git a/docs/examples/python/first_view.py b/docs/examples/python/first_view.py new file mode 100644 index 00000000..23e21130 --- /dev/null +++ b/docs/examples/python/first_view.py @@ -0,0 +1,5 @@ +from django.shortcuts import render + + +def index(request): + return render(request, "my_template.html") diff --git a/docs/examples/python/hello_world_app_config_cbv.py b/docs/examples/python/hello_world_app_config_cbv.py index ec448117..c0852da8 100644 --- a/docs/examples/python/hello_world_app_config_cbv.py +++ b/docs/examples/python/hello_world_app_config_cbv.py @@ -1,7 +1,7 @@ from django.apps import AppConfig -from reactpy_django.utils import register_iframe -from . import views +from example import views +from reactpy_django.utils import register_iframe class ExampleAppConfig(AppConfig): diff --git a/docs/examples/python/hello_world_app_config_fbv.py b/docs/examples/python/hello_world_app_config_fbv.py index c23c6919..47a71cde 100644 --- a/docs/examples/python/hello_world_app_config_fbv.py +++ b/docs/examples/python/hello_world_app_config_fbv.py @@ -1,7 +1,7 @@ from django.apps import AppConfig -from reactpy_django.utils import register_iframe -from . import views +from example import views +from reactpy_django.utils import register_iframe class ExampleAppConfig(AppConfig): diff --git a/docs/examples/python/pyodide-js-module.py b/docs/examples/python/pyodide_js_module.py similarity index 58% rename from docs/examples/python/pyodide-js-module.py rename to docs/examples/python/pyodide_js_module.py index a96ef65b..864936dc 100644 --- a/docs/examples/python/pyodide-js-module.py +++ b/docs/examples/python/pyodide_js_module.py @@ -4,8 +4,7 @@ @component def root(): - - def onClick(event): + def on_click(event): js.document.title = "New window title" - return html.button({"onClick": onClick}, "Click Me!") + return html.button({"onClick": on_click}, "Click Me!") diff --git a/docs/examples/python/pyscript-component-initial-object.py b/docs/examples/python/pyscript_component_initial_object.py similarity index 99% rename from docs/examples/python/pyscript-component-initial-object.py rename to docs/examples/python/pyscript_component_initial_object.py index 222a568b..d84328a4 100644 --- a/docs/examples/python/pyscript-component-initial-object.py +++ b/docs/examples/python/pyscript_component_initial_object.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-component-initial-string.py b/docs/examples/python/pyscript_component_initial_string.py similarity index 99% rename from docs/examples/python/pyscript-component-initial-string.py rename to docs/examples/python/pyscript_component_initial_string.py index 664b9f9b..bb8f9d17 100644 --- a/docs/examples/python/pyscript-component-initial-string.py +++ b/docs/examples/python/pyscript_component_initial_string.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-component-multiple-files-root.py b/docs/examples/python/pyscript_component_multiple_files_root.py similarity index 99% rename from docs/examples/python/pyscript-component-multiple-files-root.py rename to docs/examples/python/pyscript_component_multiple_files_root.py index 776b26b2..fd826137 100644 --- a/docs/examples/python/pyscript-component-multiple-files-root.py +++ b/docs/examples/python/pyscript_component_multiple_files_root.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-component-root.py b/docs/examples/python/pyscript_component_root.py similarity index 99% rename from docs/examples/python/pyscript-component-root.py rename to docs/examples/python/pyscript_component_root.py index 9880b740..3d795247 100644 --- a/docs/examples/python/pyscript-component-root.py +++ b/docs/examples/python/pyscript_component_root.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-hello-world.py b/docs/examples/python/pyscript_hello_world.py similarity index 100% rename from docs/examples/python/pyscript-hello-world.py rename to docs/examples/python/pyscript_hello_world.py diff --git a/docs/examples/python/pyscript-initial-object.py b/docs/examples/python/pyscript_initial_object.py similarity index 100% rename from docs/examples/python/pyscript-initial-object.py rename to docs/examples/python/pyscript_initial_object.py diff --git a/docs/examples/python/pyscript-local-import.py b/docs/examples/python/pyscript_local_import.py similarity index 100% rename from docs/examples/python/pyscript-local-import.py rename to docs/examples/python/pyscript_local_import.py diff --git a/docs/examples/python/pyscript-multiple-files-child.py b/docs/examples/python/pyscript_multiple_files_child.py similarity index 100% rename from docs/examples/python/pyscript-multiple-files-child.py rename to docs/examples/python/pyscript_multiple_files_child.py diff --git a/docs/examples/python/pyscript-multiple-files-root.py b/docs/examples/python/pyscript_multiple_files_root.py similarity index 60% rename from docs/examples/python/pyscript-multiple-files-root.py rename to docs/examples/python/pyscript_multiple_files_root.py index dc17e7ad..9ae8e549 100644 --- a/docs/examples/python/pyscript-multiple-files-root.py +++ b/docs/examples/python/pyscript_multiple_files_root.py @@ -1,9 +1,6 @@ -from typing import TYPE_CHECKING - from reactpy import component, html -if TYPE_CHECKING: - from .child import child_component +from example.components import child_component @component diff --git a/docs/examples/python/pyscript-root.py b/docs/examples/python/pyscript_root.py similarity index 100% rename from docs/examples/python/pyscript-root.py rename to docs/examples/python/pyscript_root.py diff --git a/docs/examples/python/pyscript-setup-config-object.py b/docs/examples/python/pyscript_setup_config_object.py similarity index 100% rename from docs/examples/python/pyscript-setup-config-object.py rename to docs/examples/python/pyscript_setup_config_object.py diff --git a/docs/examples/python/pyscript-setup-extra-js-object.py b/docs/examples/python/pyscript_setup_extra_js_object.py similarity index 100% rename from docs/examples/python/pyscript-setup-extra-js-object.py rename to docs/examples/python/pyscript_setup_extra_js_object.py diff --git a/docs/examples/python/pyscript-ssr-child.py b/docs/examples/python/pyscript_ssr_child.py similarity index 100% rename from docs/examples/python/pyscript-ssr-child.py rename to docs/examples/python/pyscript_ssr_child.py diff --git a/docs/examples/python/pyscript-ssr-parent.py b/docs/examples/python/pyscript_ssr_parent.py similarity index 99% rename from docs/examples/python/pyscript-ssr-parent.py rename to docs/examples/python/pyscript_ssr_parent.py index b51aa110..524cdc52 100644 --- a/docs/examples/python/pyscript-ssr-parent.py +++ b/docs/examples/python/pyscript_ssr_parent.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.components import pyscript_component diff --git a/docs/examples/python/pyscript-tag.py b/docs/examples/python/pyscript_tag.py similarity index 99% rename from docs/examples/python/pyscript-tag.py rename to docs/examples/python/pyscript_tag.py index 6455e9da..a038b267 100644 --- a/docs/examples/python/pyscript-tag.py +++ b/docs/examples/python/pyscript_tag.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.html import pyscript example_source_code = """ diff --git a/docs/examples/python/register-component.py b/docs/examples/python/register_component.py similarity index 99% rename from docs/examples/python/register-component.py rename to docs/examples/python/register_component.py index cbdbf789..6d7d3831 100644 --- a/docs/examples/python/register-component.py +++ b/docs/examples/python/register_component.py @@ -1,4 +1,5 @@ from django.apps import AppConfig + from reactpy_django.utils import register_component diff --git a/docs/examples/python/template-tag-args-kwargs.py b/docs/examples/python/template_tag_args_kwargs.py similarity index 100% rename from docs/examples/python/template-tag-args-kwargs.py rename to docs/examples/python/template_tag_args_kwargs.py diff --git a/docs/examples/python/template-tag-bad-view.py b/docs/examples/python/template_tag_bad_view.py similarity index 100% rename from docs/examples/python/template-tag-bad-view.py rename to docs/examples/python/template_tag_bad_view.py diff --git a/docs/examples/python/example/models.py b/docs/examples/python/todo_item_model.py similarity index 100% rename from docs/examples/python/example/models.py rename to docs/examples/python/todo_item_model.py diff --git a/docs/examples/python/use-channel-layer.py b/docs/examples/python/use_channel_layer.py similarity index 99% rename from docs/examples/python/use-channel-layer.py rename to docs/examples/python/use_channel_layer.py index 83a66f19..f504c978 100644 --- a/docs/examples/python/use-channel-layer.py +++ b/docs/examples/python/use_channel_layer.py @@ -1,4 +1,5 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer diff --git a/docs/examples/python/use-channel-layer-group.py b/docs/examples/python/use_channel_layer_group.py similarity index 99% rename from docs/examples/python/use-channel-layer-group.py rename to docs/examples/python/use_channel_layer_group.py index bcbabee6..4e6aaa83 100644 --- a/docs/examples/python/use-channel-layer-group.py +++ b/docs/examples/python/use_channel_layer_group.py @@ -1,4 +1,5 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer diff --git a/docs/examples/python/use-channel-layer-signal-receiver.py b/docs/examples/python/use_channel_layer_signal_receiver.py similarity index 99% rename from docs/examples/python/use-channel-layer-signal-receiver.py rename to docs/examples/python/use_channel_layer_signal_receiver.py index 57a92321..bd8c47f9 100644 --- a/docs/examples/python/use-channel-layer-signal-receiver.py +++ b/docs/examples/python/use_channel_layer_signal_receiver.py @@ -1,4 +1,5 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer diff --git a/docs/examples/python/use-channel-layer-signal-sender.py b/docs/examples/python/use_channel_layer_signal_sender.py similarity index 100% rename from docs/examples/python/use-channel-layer-signal-sender.py rename to docs/examples/python/use_channel_layer_signal_sender.py diff --git a/docs/examples/python/use-connection.py b/docs/examples/python/use_connection.py similarity index 99% rename from docs/examples/python/use-connection.py rename to docs/examples/python/use_connection.py index 1ea0fdb6..a15cd39b 100644 --- a/docs/examples/python/use-connection.py +++ b/docs/examples/python/use_connection.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_connection diff --git a/docs/examples/python/use-location.py b/docs/examples/python/use_location.py similarity index 99% rename from docs/examples/python/use-location.py rename to docs/examples/python/use_location.py index d7afcbac..454da7f6 100644 --- a/docs/examples/python/use-location.py +++ b/docs/examples/python/use_location.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_location diff --git a/docs/examples/python/use-mutation.py b/docs/examples/python/use_mutation.py similarity index 99% rename from docs/examples/python/use-mutation.py rename to docs/examples/python/use_mutation.py index 1bc69312..dcfabb3e 100644 --- a/docs/examples/python/use-mutation.py +++ b/docs/examples/python/use_mutation.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_mutation diff --git a/docs/examples/python/use-mutation-args-kwargs.py b/docs/examples/python/use_mutation_args_kwargs.py similarity index 71% rename from docs/examples/python/use-mutation-args-kwargs.py rename to docs/examples/python/use_mutation_args_kwargs.py index f9889777..9a4b1e0a 100644 --- a/docs/examples/python/use-mutation-args-kwargs.py +++ b/docs/examples/python/use_mutation_args_kwargs.py @@ -1,9 +1,9 @@ from reactpy import component + from reactpy_django.hooks import use_mutation -def example_mutation(value: int, other_value: bool = False): - ... +def example_mutation(value: int, other_value: bool = False): ... @component @@ -11,5 +11,3 @@ def my_component(): mutation = use_mutation(example_mutation) mutation(123, other_value=True) - - ... diff --git a/docs/examples/python/use-mutation-query-refetch.py b/docs/examples/python/use_mutation_query_refetch.py similarity index 90% rename from docs/examples/python/use-mutation-query-refetch.py rename to docs/examples/python/use_mutation_query_refetch.py index 227ab1a7..40d4100a 100644 --- a/docs/examples/python/use-mutation-query-refetch.py +++ b/docs/examples/python/use_mutation_query_refetch.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_mutation, use_query @@ -26,9 +27,7 @@ def submit_event(event): elif item_query.error or not item_query.data: rendered_items = html.h2("Error when loading!") else: - rendered_items = html.ul( - html.li(item.text, key=item.pk) for item in item_query.data - ) + rendered_items = html.ul(html.li(item.text, key=item.pk) for item in item_query.data) # Handle all possible mutation states if item_mutation.loading: diff --git a/docs/examples/python/use-mutation-reset.py b/docs/examples/python/use_mutation_reset.py similarity index 99% rename from docs/examples/python/use-mutation-reset.py rename to docs/examples/python/use_mutation_reset.py index 8eb1e042..0b68d8b9 100644 --- a/docs/examples/python/use-mutation-reset.py +++ b/docs/examples/python/use_mutation_reset.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_mutation diff --git a/docs/examples/python/use-mutation-thread-sensitive.py b/docs/examples/python/use_mutation_thread_sensitive.py similarity index 98% rename from docs/examples/python/use-mutation-thread-sensitive.py rename to docs/examples/python/use_mutation_thread_sensitive.py index 85046dc0..762b0819 100644 --- a/docs/examples/python/use-mutation-thread-sensitive.py +++ b/docs/examples/python/use_mutation_thread_sensitive.py @@ -1,10 +1,10 @@ from reactpy import component, html + from reactpy_django.hooks import use_mutation def execute_thread_safe_mutation(text): """This is an example mutation function that does some thread-safe operation.""" - pass @component diff --git a/docs/examples/python/use-origin.py b/docs/examples/python/use_origin.py similarity index 99% rename from docs/examples/python/use-origin.py rename to docs/examples/python/use_origin.py index e8763bbf..f0713db9 100644 --- a/docs/examples/python/use-origin.py +++ b/docs/examples/python/use_origin.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_origin diff --git a/docs/examples/python/use-query.py b/docs/examples/python/use_query.py similarity index 82% rename from docs/examples/python/use-query.py rename to docs/examples/python/use_query.py index 5688765b..9cadbd25 100644 --- a/docs/examples/python/use-query.py +++ b/docs/examples/python/use_query.py @@ -1,6 +1,7 @@ from channels.db import database_sync_to_async -from example.models import TodoItem from reactpy import component, html + +from example.models import TodoItem from reactpy_django.hooks import use_query @@ -17,8 +18,6 @@ def todo_list(): elif item_query.error or not item_query.data: rendered_items = html.h2("Error when loading!") else: - rendered_items = html.ul( - [html.li(item.text, key=item.pk) for item in item_query.data] - ) + rendered_items = html.ul([html.li(item.text, key=item.pk) for item in item_query.data]) return html.div("Rendered items: ", rendered_items) diff --git a/docs/examples/python/use-query-args.py b/docs/examples/python/use_query_args.py similarity index 99% rename from docs/examples/python/use-query-args.py rename to docs/examples/python/use_query_args.py index 8deb549a..a37f7277 100644 --- a/docs/examples/python/use-query-args.py +++ b/docs/examples/python/use_query_args.py @@ -1,4 +1,5 @@ from reactpy import component + from reactpy_django.hooks import use_query diff --git a/docs/examples/python/use-query-postprocessor-change.py b/docs/examples/python/use_query_postprocessor_change.py similarity index 98% rename from docs/examples/python/use-query-postprocessor-change.py rename to docs/examples/python/use_query_postprocessor_change.py index 5685956d..2faba050 100644 --- a/docs/examples/python/use-query-postprocessor-change.py +++ b/docs/examples/python/use_query_postprocessor_change.py @@ -1,4 +1,5 @@ from reactpy import component + from reactpy_django.hooks import use_query @@ -11,7 +12,6 @@ def my_postprocessor(data, example_kwarg=True): def execute_io_intensive_operation(): """This is an example query function that does something IO intensive.""" - pass @component diff --git a/docs/examples/python/use-query-postprocessor-disable.py b/docs/examples/python/use_query_postprocessor_disable.py similarity index 97% rename from docs/examples/python/use-query-postprocessor-disable.py rename to docs/examples/python/use_query_postprocessor_disable.py index e9541924..a22f7a96 100644 --- a/docs/examples/python/use-query-postprocessor-disable.py +++ b/docs/examples/python/use_query_postprocessor_disable.py @@ -1,10 +1,10 @@ from reactpy import component + from reactpy_django.hooks import use_query def execute_io_intensive_operation(): """This is an example query function that does something IO intensive.""" - pass @component diff --git a/docs/examples/python/use-query-postprocessor-kwargs.py b/docs/examples/python/use_query_postprocessor_kwargs.py similarity index 99% rename from docs/examples/python/use-query-postprocessor-kwargs.py rename to docs/examples/python/use_query_postprocessor_kwargs.py index 4ed108af..18ba2999 100644 --- a/docs/examples/python/use-query-postprocessor-kwargs.py +++ b/docs/examples/python/use_query_postprocessor_kwargs.py @@ -1,5 +1,6 @@ -from example.models import TodoItem from reactpy import component + +from example.models import TodoItem from reactpy_django.hooks import use_query diff --git a/docs/examples/python/use-query-thread-sensitive.py b/docs/examples/python/use_query_thread_sensitive.py similarity index 97% rename from docs/examples/python/use-query-thread-sensitive.py rename to docs/examples/python/use_query_thread_sensitive.py index d657be6b..9b929e3a 100644 --- a/docs/examples/python/use-query-thread-sensitive.py +++ b/docs/examples/python/use_query_thread_sensitive.py @@ -1,10 +1,10 @@ from reactpy import component + from reactpy_django.hooks import use_query def execute_thread_safe_operation(): """This is an example query function that does some thread-safe operation.""" - pass @component diff --git a/docs/examples/python/use-root-id.py b/docs/examples/python/use_root_id.py similarity index 99% rename from docs/examples/python/use-root-id.py rename to docs/examples/python/use_root_id.py index f2088cc4..246a8da1 100644 --- a/docs/examples/python/use-root-id.py +++ b/docs/examples/python/use_root_id.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_root_id diff --git a/docs/examples/python/use-scope.py b/docs/examples/python/use_scope.py similarity index 99% rename from docs/examples/python/use-scope.py rename to docs/examples/python/use_scope.py index 2e6f5961..2bd8f483 100644 --- a/docs/examples/python/use-scope.py +++ b/docs/examples/python/use_scope.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_scope diff --git a/docs/examples/python/use-user.py b/docs/examples/python/use_user.py similarity index 99% rename from docs/examples/python/use-user.py rename to docs/examples/python/use_user.py index 641bbeee..597e9f67 100644 --- a/docs/examples/python/use-user.py +++ b/docs/examples/python/use_user.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_user diff --git a/docs/examples/python/use-user-data.py b/docs/examples/python/use_user_data.py similarity index 99% rename from docs/examples/python/use-user-data.py rename to docs/examples/python/use_user_data.py index bc0ffaff..2c998db0 100644 --- a/docs/examples/python/use-user-data.py +++ b/docs/examples/python/use_user_data.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_user_data diff --git a/docs/examples/python/use-user-data-defaults.py b/docs/examples/python/use_user_data_defaults.py similarity index 99% rename from docs/examples/python/use-user-data-defaults.py rename to docs/examples/python/use_user_data_defaults.py index 7a1380bc..2c066ad7 100644 --- a/docs/examples/python/use-user-data-defaults.py +++ b/docs/examples/python/use_user_data_defaults.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.hooks import use_user_data diff --git a/docs/examples/python/user-passes-test.py b/docs/examples/python/user_passes_test.py similarity index 99% rename from docs/examples/python/user-passes-test.py rename to docs/examples/python/user_passes_test.py index 201ad831..37160c1b 100644 --- a/docs/examples/python/user-passes-test.py +++ b/docs/examples/python/user_passes_test.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.decorators import user_passes_test diff --git a/docs/examples/python/user-passes-test-component-fallback.py b/docs/examples/python/user_passes_test_component_fallback.py similarity index 99% rename from docs/examples/python/user-passes-test-component-fallback.py rename to docs/examples/python/user_passes_test_component_fallback.py index 9fb71ea7..b18330d1 100644 --- a/docs/examples/python/user-passes-test-component-fallback.py +++ b/docs/examples/python/user_passes_test_component_fallback.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.decorators import user_passes_test diff --git a/docs/examples/python/user-passes-test-vdom-fallback.py b/docs/examples/python/user_passes_test_vdom_fallback.py similarity index 99% rename from docs/examples/python/user-passes-test-vdom-fallback.py rename to docs/examples/python/user_passes_test_vdom_fallback.py index 5d5c54f4..9dd1ad65 100644 --- a/docs/examples/python/user-passes-test-vdom-fallback.py +++ b/docs/examples/python/user_passes_test_vdom_fallback.py @@ -1,4 +1,5 @@ from reactpy import component, html + from reactpy_django.decorators import user_passes_test diff --git a/docs/examples/python/views.py b/docs/examples/python/views.py deleted file mode 100644 index 60ebc945..00000000 --- a/docs/examples/python/views.py +++ /dev/null @@ -1,7 +0,0 @@ -from .hello_world_cbv import HelloWorld -from .hello_world_fbv import hello_world - -__all__ = [ - "HelloWorld", - "hello_world", -] diff --git a/docs/examples/python/vtc.py b/docs/examples/python/vtc.py index 194d35cc..84c7aeb2 100644 --- a/docs/examples/python/vtc.py +++ b/docs/examples/python/vtc.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.hello_world) diff --git a/docs/examples/python/vtc-args.py b/docs/examples/python/vtc_args.py similarity index 95% rename from docs/examples/python/vtc-args.py rename to docs/examples/python/vtc_args.py index edc0fbb2..9ce081b5 100644 --- a/docs/examples/python/vtc-args.py +++ b/docs/examples/python/vtc_args.py @@ -1,8 +1,8 @@ from django.http import HttpRequest from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.hello_world) diff --git a/docs/examples/python/vtc-cbv.py b/docs/examples/python/vtc_cbv.py similarity index 90% rename from docs/examples/python/vtc-cbv.py rename to docs/examples/python/vtc_cbv.py index 47509b75..38e40efe 100644 --- a/docs/examples/python/vtc-cbv.py +++ b/docs/examples/python/vtc_cbv.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.HelloWorld.as_view()) diff --git a/docs/examples/python/vtc-strict-parsing.py b/docs/examples/python/vtc_strict_parsing.py similarity index 90% rename from docs/examples/python/vtc-strict-parsing.py rename to docs/examples/python/vtc_strict_parsing.py index 194d35cc..84c7aeb2 100644 --- a/docs/examples/python/vtc-strict-parsing.py +++ b/docs/examples/python/vtc_strict_parsing.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component hello_world_component = view_to_component(views.hello_world) diff --git a/docs/examples/python/vtc-transforms.py b/docs/examples/python/vtc_transforms.py similarity index 75% rename from docs/examples/python/vtc-transforms.py rename to docs/examples/python/vtc_transforms.py index adbf9ea1..b8402481 100644 --- a/docs/examples/python/vtc-transforms.py +++ b/docs/examples/python/vtc_transforms.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_component -from . import views +from example import views +from reactpy_django.components import view_to_component def example_transform(vdom): @@ -10,9 +10,7 @@ def example_transform(vdom): vdom["children"][0] = "Farewell World!" -hello_world_component = view_to_component( - views.hello_world, transforms=[example_transform] -) +hello_world_component = view_to_component(views.hello_world, transforms=[example_transform]) @component diff --git a/docs/examples/python/vti.py b/docs/examples/python/vti.py index c8ff6796..207e5bc5 100644 --- a/docs/examples/python/vti.py +++ b/docs/examples/python/vti.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe hello_world_iframe = view_to_iframe(views.hello_world) diff --git a/docs/examples/python/vti-args.py b/docs/examples/python/vti_args.py similarity index 93% rename from docs/examples/python/vti-args.py rename to docs/examples/python/vti_args.py index f5013ecd..a26c3d3a 100644 --- a/docs/examples/python/vti-args.py +++ b/docs/examples/python/vti_args.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe hello_world_iframe = view_to_iframe( views.hello_world, diff --git a/docs/examples/python/vti-cbv.py b/docs/examples/python/vti_cbv.py similarity index 90% rename from docs/examples/python/vti-cbv.py rename to docs/examples/python/vti_cbv.py index 4e1f1b44..63f182ae 100644 --- a/docs/examples/python/vti-cbv.py +++ b/docs/examples/python/vti_cbv.py @@ -1,7 +1,7 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe hello_world_iframe = view_to_iframe(views.HelloWorld.as_view()) diff --git a/docs/examples/python/vti-extra-props.py b/docs/examples/python/vti_extra_props.py similarity index 60% rename from docs/examples/python/vti-extra-props.py rename to docs/examples/python/vti_extra_props.py index 655ad541..09846a1c 100644 --- a/docs/examples/python/vti-extra-props.py +++ b/docs/examples/python/vti_extra_props.py @@ -1,11 +1,9 @@ from reactpy import component, html -from reactpy_django.components import view_to_iframe -from . import views +from example import views +from reactpy_django.components import view_to_iframe -hello_world_iframe = view_to_iframe( - views.hello_world, extra_props={"title": "Hello World!"} -) +hello_world_iframe = view_to_iframe(views.hello_world, extra_props={"title": "Hello World!"}) @component diff --git a/docs/overrides/home.html b/docs/overrides/home.html index 67e31441..93d5ca29 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -73,9 +73,9 @@

Create user interfaces from components

{% with image="create-user-interfaces.png", class="pop-left" %} - {% include "home-code-examples/code-block.html" %} + {% include "homepage_examples/code_block.html" %} {% endwith %} - {% include "home-code-examples/create-user-interfaces-demo.html" %} + {% include "homepage_examples/create_user_interfaces_demo.html" %}

Whether you work on your own or with thousands of other developers, using React feels the same. It is @@ -94,9 +94,9 @@

Write components with pure Python code

{% with image="write-components-with-python.png", class="pop-left" %} - {% include "home-code-examples/code-block.html" %} + {% include "homepage_examples/code_block.html" %} {% endwith %} - {% include "home-code-examples/write-components-with-python-demo.html" %} + {% include "homepage_examples/write_components_with_python_demo.html" %}
@@ -110,9 +110,9 @@

Add interactivity wherever you need it

{% with image="add-interactivity.png" %} - {% include "home-code-examples/code-block.html" %} + {% include "homepage_examples/code_block.html" %} {% endwith %} - {% include "home-code-examples/add-interactivity-demo.html" %} + {% include "homepage_examples/add_interactivity_demo.html" %}

You don't have to build your whole page in ReactPy. Add React to your existing HTML page, and render diff --git a/docs/overrides/home-code-examples/add-interactivity.py b/docs/overrides/homepage_examples/add_interactivity.py similarity index 74% rename from docs/overrides/home-code-examples/add-interactivity.py rename to docs/overrides/homepage_examples/add_interactivity.py index f29ba3c8..9a7bf76f 100644 --- a/docs/overrides/home-code-examples/add-interactivity.py +++ b/docs/overrides/homepage_examples/add_interactivity.py @@ -1,8 +1,9 @@ -# pylint: disable=assignment-from-no-return, unnecessary-lambda +# ruff: noqa: INP001 from reactpy import component, html, use_state -def filter_videos(*_, **__): ... +def filter_videos(*_, **__): + return [] def search_input(*_, **__): ... @@ -18,7 +19,7 @@ def searchable_video_list(videos): return html._( search_input( - {"onChange": lambda new_text: set_search_text(new_text)}, + {"onChange": lambda event: set_search_text(event["target"]["value"])}, value=search_text, ), video_list( diff --git a/docs/overrides/home-code-examples/add-interactivity-demo.html b/docs/overrides/homepage_examples/add_interactivity_demo.html similarity index 100% rename from docs/overrides/home-code-examples/add-interactivity-demo.html rename to docs/overrides/homepage_examples/add_interactivity_demo.html diff --git a/docs/overrides/home-code-examples/code-block.html b/docs/overrides/homepage_examples/code_block.html similarity index 100% rename from docs/overrides/home-code-examples/code-block.html rename to docs/overrides/homepage_examples/code_block.html diff --git a/docs/overrides/home-code-examples/create-user-interfaces.py b/docs/overrides/homepage_examples/create_user_interfaces.py similarity index 94% rename from docs/overrides/home-code-examples/create-user-interfaces.py rename to docs/overrides/homepage_examples/create_user_interfaces.py index 873b9d88..7878aa6b 100644 --- a/docs/overrides/home-code-examples/create-user-interfaces.py +++ b/docs/overrides/homepage_examples/create_user_interfaces.py @@ -1,3 +1,4 @@ +# ruff: noqa: INP001 from reactpy import component, html diff --git a/docs/overrides/home-code-examples/create-user-interfaces-demo.html b/docs/overrides/homepage_examples/create_user_interfaces_demo.html similarity index 100% rename from docs/overrides/home-code-examples/create-user-interfaces-demo.html rename to docs/overrides/homepage_examples/create_user_interfaces_demo.html diff --git a/docs/overrides/home-code-examples/write-components-with-python.py b/docs/overrides/homepage_examples/write_components_with_python.py similarity index 94% rename from docs/overrides/home-code-examples/write-components-with-python.py rename to docs/overrides/homepage_examples/write_components_with_python.py index 47e28b68..5993046c 100644 --- a/docs/overrides/home-code-examples/write-components-with-python.py +++ b/docs/overrides/homepage_examples/write_components_with_python.py @@ -1,3 +1,4 @@ +# ruff: noqa: INP001 from reactpy import component, html diff --git a/docs/overrides/home-code-examples/write-components-with-python-demo.html b/docs/overrides/homepage_examples/write_components_with_python_demo.html similarity index 100% rename from docs/overrides/home-code-examples/write-components-with-python-demo.html rename to docs/overrides/homepage_examples/write_components_with_python_demo.html diff --git a/docs/src/about/contributing.md b/docs/src/about/contributing.md index ecb0131b..59f4f989 100644 --- a/docs/src/about/contributing.md +++ b/docs/src/about/contributing.md @@ -76,7 +76,7 @@ By utilizing `hatch`, the following commands are available to manage the develop | `hatch run docs:serve` | Start the [`mkdocs`](https://www.mkdocs.org/) server to view documentation locally | | `hatch run docs:build` | Build the documentation | | `hatch run docs:linkcheck` | Check for broken links in the documentation | -| `hatch run docs:check_examples` | Run linter on code examples in the documentation | +| `hatch fmt docs --check` | Run linter on code examples in the documentation | ### Environment Management diff --git a/docs/src/learn/add-reactpy-to-a-django-project.md b/docs/src/learn/add-reactpy-to-a-django-project.md index 0bf919e2..407fe61d 100644 --- a/docs/src/learn/add-reactpy-to-a-django-project.md +++ b/docs/src/learn/add-reactpy-to-a-django-project.md @@ -29,7 +29,7 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject === "settings.py" ```python - {% include "../../examples/python/configure-installed-apps.py" %} + {% include "../../examples/python/configure_installed_apps.py" %} ``` ??? warning "Enable ASGI and Django Channels (Required)" @@ -42,13 +42,13 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject 2. Add `#!python "daphne"` to `#!python INSTALLED_APPS`. ```python linenums="0" - {% include "../../examples/python/configure-channels-installed-app.py" %} + {% include "../../examples/python/configure_channels_installed_app.py" %} ``` 3. Set your `#!python ASGI_APPLICATION` variable. ```python linenums="0" - {% include "../../examples/python/configure-channels-asgi-app.py" %} + {% include "../../examples/python/configure_channels_asgi_app.py" %} ``` ??? info "Configure ReactPy settings (Optional)" @@ -64,7 +64,7 @@ Add ReactPy HTTP paths to your `#!python urlpatterns` in your [`urls.py`](https: === "urls.py" ```python - {% include "../../examples/python/configure-urls.py" %} + {% include "../../examples/python/configure_urls.py" %} ``` ## Step 4: Configure `asgi.py` @@ -74,7 +74,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [` === "asgi.py" ```python - {% include "../../examples/python/configure-asgi.py" %} + {% include "../../examples/python/configure_asgi.py" %} ``` ??? info "Add `#!python AuthMiddlewareStack` (Optional)" @@ -88,7 +88,7 @@ Register ReactPy's WebSocket using `#!python REACTPY_WEBSOCKET_ROUTE` in your [` In these situations will need to ensure you are using `#!python AuthMiddlewareStack`. ```python linenums="0" - {% include "../../examples/python/configure-asgi-middleware.py" start="# start" %} + {% include "../../examples/python/configure_asgi_middleware.py" start="# start" %} ``` ??? question "Where is my `asgi.py`?" diff --git a/docs/src/learn/your-first-component.md b/docs/src/learn/your-first-component.md index 85af4109..11e29798 100644 --- a/docs/src/learn/your-first-component.md +++ b/docs/src/learn/your-first-component.md @@ -8,7 +8,7 @@ Components are one of the core concepts of ReactPy. They are the foundation upon !!! abstract "Note" - If you have reached this point, you should have already [installed ReactPy-Django](../learn/add-reactpy-to-a-django-project.md) through the previous steps. + If you have reached this point, you should have already [installed ReactPy-Django](./add-reactpy-to-a-django-project.md) through the previous steps. --- @@ -87,7 +87,7 @@ Within your **Django app**'s `views.py` file, you will need to [create a view fu === "views.py" ```python - {% include "../../examples/python/example/views.py" %} + {% include "../../examples/python/first_view.py" %} ``` We will add this new view into your [`urls.py`](https://docs.djangoproject.com/en/stable/intro/tutorial01/#write-your-first-view) and define what URL it should be accessible at. @@ -95,7 +95,7 @@ We will add this new view into your [`urls.py`](https://docs.djangoproject.com/e === "urls.py" ```python - {% include "../../examples/python/example/urls.py" %} + {% include "../../examples/python/first_urls.py" %} ``` ??? question "Which urls.py do I add my views to?" diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index 7c60ca68..4186af42 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -12,26 +12,26 @@ We supply some pre-designed that components can be used to help simplify develop This allows you to embedded any number of client-side PyScript components within traditional ReactPy components. -{% include-markdown "../reference/template-tag.md" start="" end="" %} +{% include-markdown "./template-tag.md" start="" end="" %} -{% include-markdown "../reference/template-tag.md" start="" end="" %} +{% include-markdown "./template-tag.md" start="" end="" %} === "components.py" ```python - {% include "../../examples/python/pyscript-ssr-parent.py" %} + {% include "../../examples/python/pyscript_ssr_parent.py" %} ``` === "root.py" ```python - {% include "../../examples/python/pyscript-ssr-child.py" %} + {% include "../../examples/python/pyscript_ssr_child.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-ssr-parent.html" %} + {% include "../../examples/html/pyscript_ssr_parent.html" %} ``` ??? example "See Interface" @@ -53,31 +53,31 @@ This allows you to embedded any number of client-side PyScript components within === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup.html" %} + {% include "../../examples/html/pyscript_setup.html" %} ``` -{% include-markdown "../reference/template-tag.md" start="" end="" %} +{% include-markdown "./template-tag.md" start="" end="" %} -{% include-markdown "../reference/template-tag.md" start="" end="" trailing-newlines=false preserve-includer-indent=false %} +{% include-markdown "./template-tag.md" start="" end="" trailing-newlines=false preserve-includer-indent=false %} === "components.py" ```python - {% include "../../examples/python/pyscript-component-multiple-files-root.py" %} + {% include "../../examples/python/pyscript_component_multiple_files_root.py" %} ``` === "root.py" ```python - {% include "../../examples/python/pyscript-multiple-files-root.py" %} + {% include "../../examples/python/pyscript_multiple_files_root.py" %} ``` === "child.py" ```python - {% include "../../examples/python/pyscript-multiple-files-child.py" %} + {% include "../../examples/python/pyscript_multiple_files_child.py" %} ``` ??? question "How do I display something while the component is loading?" @@ -89,7 +89,7 @@ This allows you to embedded any number of client-side PyScript components within === "components.py" ```python - {% include "../../examples/python/pyscript-component-initial-object.py" %} + {% include "../../examples/python/pyscript_component_initial_object.py" %} ``` However, you can also use a string containing raw HTML. @@ -97,7 +97,7 @@ This allows you to embedded any number of client-side PyScript components within === "components.py" ```python - {% include "../../examples/python/pyscript-component-initial-string.py" %} + {% include "../../examples/python/pyscript_component_initial_string.py" %} ``` ??? question "Can I use a different name for my root component?" @@ -107,13 +107,13 @@ This allows you to embedded any number of client-side PyScript components within === "components.py" ```python - {% include "../../examples/python/pyscript-component-root.py" %} + {% include "../../examples/python/pyscript_component_root.py" %} ``` === "main.py" ```python - {% include "../../examples/python/pyscript-root.py" %} + {% include "../../examples/python/pyscript_root.py" %} ``` --- @@ -171,7 +171,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-cbv.py" %} + {% include "../../examples/python/vtc_cbv.py" %} ``` === "views.py" @@ -187,7 +187,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-args.py" %} + {% include "../../examples/python/vtc_args.py" %} ``` === "views.py" @@ -215,7 +215,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-strict-parsing.py" %} + {% include "../../examples/python/vtc_strict_parsing.py" %} ``` === "views.py" @@ -237,7 +237,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vtc-transforms.py" %} + {% include "../../examples/python/vtc_transforms.py" %} ``` === "views.py" @@ -308,7 +308,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vti-cbv.py" %} + {% include "../../examples/python/vti_cbv.py" %} ``` === "views.py" @@ -332,7 +332,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vti-args.py" %} + {% include "../../examples/python/vti_args.py" %} ``` === "views.py" @@ -364,7 +364,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. === "components.py" ```python - {% include "../../examples/python/vti-extra-props.py" %} + {% include "../../examples/python/vti_extra_props.py" %} ``` === "views.py" @@ -388,7 +388,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. === "components.py" ```python - {% include "../../examples/python/django-css.py" %} + {% include "../../examples/python/django_css.py" %} ``` ??? example "See Interface" @@ -413,7 +413,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. Here's an example on what you should avoid doing for Django static files: ```python - {% include "../../examples/python/django-css-local-link.py" %} + {% include "../../examples/python/django_css_local_link.py" %} ``` ??? question "How do I load external CSS?" @@ -423,7 +423,7 @@ Allows you to defer loading a CSS stylesheet until a component begins rendering. For external CSS, you should use `#!python html.link`. ```python - {% include "../../examples/python/django-css-external-link.py" %} + {% include "../../examples/python/django_css_external_link.py" %} ``` ??? question "Why not load my CSS in `#!html `?" @@ -450,7 +450,7 @@ Be mindful of load order! If your JavaScript relies on the component existing on === "components.py" ```python - {% include "../../examples/python/django-js.py" %} + {% include "../../examples/python/django_js.py" %} ``` ??? example "See Interface" @@ -475,7 +475,7 @@ Be mindful of load order! If your JavaScript relies on the component existing on Here's an example on what you should avoid doing for Django static files: ```python - {% include "../../examples/python/django-js-local-script.py" %} + {% include "../../examples/python/django_js_local_script.py" %} ``` ??? question "How do I load external JS?" @@ -485,7 +485,7 @@ Be mindful of load order! If your JavaScript relies on the component existing on For external JavaScript, you should use `#!python html.script`. ```python - {% include "../../examples/python/django-js-remote-script.py" %} + {% include "../../examples/python/django_js_remote_script.py" %} ``` ??? question "Why not load my JS in `#!html `?" diff --git a/docs/src/reference/decorators.md b/docs/src/reference/decorators.md index bc84c75e..1763cf25 100644 --- a/docs/src/reference/decorators.md +++ b/docs/src/reference/decorators.md @@ -17,7 +17,7 @@ This only works with ReactPy components, and is inspired by Django's [`user_pass === "components.py" ```python - {% include "../../examples/python/user-passes-test.py" %} + {% include "../../examples/python/user_passes_test.py" %} ``` ??? example "See Interface" @@ -42,7 +42,7 @@ This only works with ReactPy components, and is inspired by Django's [`user_pass === "components.py" ```python - {% include "../../examples/python/user-passes-test-component-fallback.py" %} + {% include "../../examples/python/user_passes_test_component_fallback.py" %} ``` ??? question "How do I render a simple `#!python reactpy.html` snippet if the test fails?" @@ -52,5 +52,5 @@ This only works with ReactPy components, and is inspired by Django's [`user_pass === "components.py" ```python - {% include "../../examples/python/user-passes-test-vdom-fallback.py" %} + {% include "../../examples/python/user_passes_test_vdom_fallback.py" %} ``` diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 3c07639f..65bf1727 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -22,20 +22,20 @@ Prefabricated hooks can be used within your `components.py` to help simplify dev Execute functions in the background and return the result, typically to [read](https://www.sumologic.com/glossary/crud/) data from the Django ORM. -The [default postprocessor](../reference/utils.md#django-query-postprocessor) expects your query function to `#!python return` a Django `#!python Model` or `#!python QuerySet`. This needs to be changed or disabled to execute other types of queries. +The [default postprocessor](./utils.md#django-query-postprocessor) expects your query function to `#!python return` a Django `#!python Model` or `#!python QuerySet`. This needs [to be changed](./settings.md#reactpy_default_query_postprocessor) or disabled to execute other types of queries. Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-query.py" %} + {% include "../../examples/python/use_query.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? example "See Interface" @@ -63,7 +63,7 @@ Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-query-args.py" %} + {% include "../../examples/python/use_query_args.py" %} ``` ??? question "How can I customize this hook's behavior?" @@ -83,7 +83,7 @@ Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-query-thread-sensitive.py" %} + {% include "../../examples/python/use_query_thread_sensitive.py" %} ``` --- @@ -102,7 +102,7 @@ Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-query-postprocessor-disable.py" %} + {% include "../../examples/python/use_query_postprocessor_disable.py" %} ``` If you wish to create a custom `#!python postprocessor`, you will need to create a function where the first must be the query `#!python data`. All proceeding arguments are optional `#!python postprocessor_kwargs` (see below). This `#!python postprocessor` function must return the modified `#!python data`. @@ -110,7 +110,7 @@ Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-query-postprocessor-change.py" %} + {% include "../../examples/python/use_query_postprocessor_change.py" %} ``` --- @@ -126,7 +126,7 @@ Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-query-postprocessor-kwargs.py" %} + {% include "../../examples/python/use_query_postprocessor_kwargs.py" %} ``` _Note: In Django's ORM design, the field name to access foreign keys is [postfixed with `_set`](https://docs.djangoproject.com/en/stable/topics/db/examples/many_to_one/) by default._ @@ -144,20 +144,20 @@ Query functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-reset.py" %} + {% include "../../examples/python/use_mutation_reset.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? question "Why does the example query function return `#!python TodoItem.objects.all()`?" This design decision was based on [Apollo's `#!javascript useQuery` hook](https://www.apollographql.com/docs/react/data/queries/), but ultimately helps avoid Django's `#!python SynchronousOnlyOperation` exceptions. - With the `#!python Model` or `#!python QuerySet` your function returns, this hook uses the [default postprocessor](../reference/utils.md#django-query-postprocessor) to ensure that all [deferred](https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.get_deferred_fields) or [lazy](https://docs.djangoproject.com/en/stable/topics/db/queries/#querysets-are-lazy) fields are executed. + With the `#!python Model` or `#!python QuerySet` your function returns, this hook uses the [default postprocessor](./utils.md#django-query-postprocessor) to ensure that all [deferred](https://docs.djangoproject.com/en/stable/ref/models/instances/#django.db.models.Model.get_deferred_fields) or [lazy](https://docs.djangoproject.com/en/stable/topics/db/queries/#querysets-are-lazy) fields are executed. --- @@ -172,13 +172,13 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation.py" %} + {% include "../../examples/python/use_mutation.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? example "See Interface" @@ -204,7 +204,7 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-args-kwargs.py" %} + {% include "../../examples/python/use_mutation_args_kwargs.py" %} ``` ??? question "How can I customize this hook's behavior?" @@ -224,7 +224,7 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-thread-sensitive.py" %} + {% include "../../examples/python/use_mutation_thread_sensitive.py" %} ``` ??? question "Can I make ORM calls without hooks?" @@ -240,13 +240,13 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-reset.py" %} + {% include "../../examples/python/use_mutation_reset.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? question "Can `#!python use_mutation` trigger a refetch of `#!python use_query`?" @@ -260,13 +260,13 @@ Mutation functions can be sync or async. === "components.py" ```python - {% include "../../examples/python/use-mutation-query-refetch.py" %} + {% include "../../examples/python/use_mutation_query_refetch.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` --- @@ -282,7 +282,7 @@ User data saved with this hook is stored within the `#!python REACTPY_DATABASE`. === "components.py" ```python - {% include "../../examples/python/use-user-data.py" %} + {% include "../../examples/python/use_user_data.py" %} ``` ??? example "See Interface" @@ -309,7 +309,7 @@ User data saved with this hook is stored within the `#!python REACTPY_DATABASE`. === "components.py" ```python - {% include "../../examples/python/use-user-data-defaults.py" %} + {% include "../../examples/python/use_user_data_defaults.py" %} ``` --- @@ -329,7 +329,7 @@ This is often used to create chat systems, synchronize data between components, === "components.py" ```python - {% include "../../examples/python/use-channel-layer.py" %} + {% include "../../examples/python/use_channel_layer.py" %} ``` ??? example "See Interface" @@ -391,7 +391,7 @@ This is often used to create chat systems, synchronize data between components, === "components.py" ```python - {% include "../../examples/python/use-channel-layer-group.py" %} + {% include "../../examples/python/use_channel_layer_group.py" %} ``` ??? question "How do I signal a re-render from something that isn't a component?" @@ -405,13 +405,13 @@ This is often used to create chat systems, synchronize data between components, === "signals.py" ```python - {% include "../../examples/python/use-channel-layer-signal-sender.py" %} + {% include "../../examples/python/use_channel_layer_signal_sender.py" %} ``` === "components.py" ```python - {% include "../../examples/python/use-channel-layer-signal-receiver.py" %} + {% include "../../examples/python/use_channel_layer_signal_receiver.py" %} ``` --- @@ -427,7 +427,7 @@ Returns the active connection, which is either a Django [WebSocket](https://chan === "components.py" ```python - {% include "../../examples/python/use-connection.py" %} + {% include "../../examples/python/use_connection.py" %} ``` ??? example "See Interface" @@ -451,7 +451,7 @@ Shortcut that returns the WebSocket or HTTP connection's [scope](https://channel === "components.py" ```python - {% include "../../examples/python/use-scope.py" %} + {% include "../../examples/python/use_scope.py" %} ``` ??? example "See Interface" @@ -475,7 +475,7 @@ Shortcut that returns the browser's current `#!python Location`. === "components.py" ```python - {% include "../../examples/python/use-location.py" %} + {% include "../../examples/python/use_location.py" %} ``` ??? example "See Interface" @@ -501,7 +501,7 @@ You can expect this hook to provide strings such as `http://example.com`. === "components.py" ```python - {% include "../../examples/python/use-origin.py" %} + {% include "../../examples/python/use_origin.py" %} ``` ??? example "See Interface" @@ -529,7 +529,7 @@ This is useful when used in combination with [`#!python use_channel_layer`](#use === "components.py" ```python - {% include "../../examples/python/use-root-id.py" %} + {% include "../../examples/python/use_root_id.py" %} ``` ??? example "See Interface" @@ -553,7 +553,7 @@ Shortcut that returns the WebSocket or HTTP connection's `#!python User`. === "components.py" ```python - {% include "../../examples/python/use-user.py" %} + {% include "../../examples/python/use_user.py" %} ``` ??? example "See Interface" diff --git a/docs/src/reference/html.md b/docs/src/reference/html.md index baef6ebf..c9bb0108 100644 --- a/docs/src/reference/html.md +++ b/docs/src/reference/html.md @@ -19,13 +19,13 @@ The `pyscript` tag functions identically to HTML tags contained within `#!python === "components.py" ```python - {% include "../../examples/python/pyscript-tag.py" %} + {% include "../../examples/python/pyscript_tag.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-tag.html" %} + {% include "../../examples/html/pyscript_tag.html" %} ``` -{% include-markdown "../reference/components.md" start="" end="" %} +{% include-markdown "./components.md" start="" end="" %} diff --git a/docs/src/reference/router.md b/docs/src/reference/router.md index be6093c6..757981f6 100644 --- a/docs/src/reference/router.md +++ b/docs/src/reference/router.md @@ -33,7 +33,7 @@ URL router that enables the ability to conditionally render other components bas === "components.py" ```python - {% include "../../examples/python/django-router.py" %} + {% include "../../examples/python/django_router.py" %} ``` ??? example "See Interface" diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index 23760919..6b1c78c4 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -145,7 +145,7 @@ The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. This is typically useful for self-hosted applications. -You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) to manually override this default. +You can use the `#!python host` argument in your [template tag](./template-tag.md#component) to manually override this default. --- @@ -164,7 +164,7 @@ During pre-rendering, there are some key differences in behavior: 3. The component will be non-interactive until a WebSocket connection is formed. 4. The component is re-rendered once a WebSocket connection is formed. -You can use the `#!python prerender` argument in your [template tag](../reference/template-tag.md#component) to manually override this default. +You can use the `#!python prerender` argument in your [template tag](./template-tag.md#component) to manually override this default. --- diff --git a/docs/src/reference/template-tag.md b/docs/src/reference/template-tag.md index 091b2ac8..f969eb00 100644 --- a/docs/src/reference/template-tag.md +++ b/docs/src/reference/template-tag.md @@ -56,10 +56,10 @@ Each component loaded via this template tag will receive a dedicated WebSocket c === "views.py" ```python - {% include "../../examples/python/template-tag-bad-view.py" %} + {% include "../../examples/python/template_tag_bad_view.py" %} ``` - _Note: If you decide to not follow this warning, you will need to use the [`register_component`](../reference/utils.md#register-component) function to manually register your components._ + _Note: If you decide to not follow this warning, you will need to use the [`register_component`](./utils.md#register-component) function to manually register your components._ @@ -102,12 +102,12 @@ Each component loaded via this template tag will receive a dedicated WebSocket c === "components.py" ```python - {% include "../../examples/python/template-tag-args-kwargs.py" %} + {% include "../../examples/python/template_tag_args_kwargs.py" %} ``` ??? question "Can I render components on a different server (distributed computing)?" - Yes! This is most commonly done through [`settings.py:REACTPY_HOSTS`](../reference/settings.md#reactpy_default_hosts). However, you can use the `#!python host` keyword to render components on a specific ASGI server. + Yes! This is most commonly done through [`settings.py:REACTPY_HOSTS`](./settings.md#reactpy_default_hosts). However, you can use the `#!python host` keyword to render components on a specific ASGI server. === "my_template.html" @@ -127,7 +127,7 @@ Each component loaded via this template tag will receive a dedicated WebSocket c ??? question "How do I pre-render components for SEO compatibility?" - This is most commonly done through [`settings.py:REACTPY_PRERENDER`](../reference/settings.md#reactpy_prerender). However, you can use the `#!python prerender` keyword to pre-render a specific component. + This is most commonly done through [`settings.py:REACTPY_PRERENDER`](./settings.md#reactpy_prerender). However, you can use the `#!python prerender` keyword to pre-render a specific component. === "my_template.html" @@ -175,13 +175,13 @@ The entire file path provided is loaded directly into the browser, and must have === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-component.html" %} + {% include "../../examples/html/pyscript_component.html" %} ``` === "hello_world.py" ```python - {% include "../../examples/python/pyscript-hello-world.py" %} + {% include "../../examples/python/pyscript_hello_world.py" %} ``` ??? example "See Interface" @@ -211,7 +211,7 @@ The entire file path provided is loaded directly into the browser, and must have === "root.py" ```python - {% include "../../examples/python/pyodide-js-module.py" %} + {% include "../../examples/python/pyodide_js_module.py" %} ``` **PyScript FFI** @@ -225,13 +225,13 @@ The entire file path provided is loaded directly into the browser, and must have === "root.py" ```python - {% include "../../examples/python/pyscript-local-import.py" %} + {% include "../../examples/python/pyscript_local_import.py" %} ``` === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-local-import.html" %} + {% include "../../examples/html/pyscript_local_import.html" %} ``` @@ -253,19 +253,19 @@ The entire file path provided is loaded directly into the browser, and must have === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-multiple-files.html" %} + {% include "../../examples/html/pyscript_multiple_files.html" %} ``` === "root.py" ```python - {% include "../../examples/python/pyscript-multiple-files-root.py" %} + {% include "../../examples/python/pyscript_multiple_files_root.py" %} ``` === "child.py" ```python - {% include "../../examples/python/pyscript-multiple-files-child.py" %} + {% include "../../examples/python/pyscript_multiple_files_child.py" %} ``` ??? question "How do I display something while the component is loading?" @@ -277,7 +277,7 @@ The entire file path provided is loaded directly into the browser, and must have === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-initial-string.html" %} + {% include "../../examples/html/pyscript_initial_string.html" %} ``` However, you can also insert a `#!python reactpy.html` snippet or a non-interactive `#!python @component` via template context. @@ -285,13 +285,13 @@ The entire file path provided is loaded directly into the browser, and must have === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-initial-object.html" %} + {% include "../../examples/html/pyscript_initial_object.html" %} ``` === "views.py" ```python - {% include "../../examples/python/pyscript-initial-object.py" %} + {% include "../../examples/python/pyscript_initial_object.py" %} ``` ??? question "Can I use a different name for my root component?" @@ -301,13 +301,13 @@ The entire file path provided is loaded directly into the browser, and must have === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-root.html" %} + {% include "../../examples/html/pyscript_root.html" %} ``` === "main.py" ```python - {% include "../../examples/python/pyscript-root.py" %} + {% include "../../examples/python/pyscript_root.py" %} ``` ## PyScript Setup @@ -319,7 +319,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup.html" %} + {% include "../../examples/html/pyscript_setup.html" %} ``` ??? example "See Interface" @@ -341,7 +341,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-dependencies.html" %} + {% include "../../examples/html/pyscript_setup_dependencies.html" %} ``` ??? question "How do I install additional Javascript dependencies?" @@ -351,13 +351,13 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-extra-js-object.html" %} + {% include "../../examples/html/pyscript_setup_extra_js_object.html" %} ``` === "views.py" ```python - {% include "../../examples/python/pyscript-setup-extra-js-object.py" %} + {% include "../../examples/python/pyscript_setup_extra_js_object.py" %} ``` The value for `#!python extra_js` is most commonly a Python dictionary, but JSON strings are also supported. @@ -365,7 +365,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-extra-js-string.html" %} + {% include "../../examples/html/pyscript_setup_extra_js_string.html" %} ``` ??? question "How do I modify the `pyscript` default configuration?" @@ -375,7 +375,7 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-config-string.html" %} + {% include "../../examples/html/pyscript_setup_config_string.html" %} ``` While this value is most commonly a JSON string, Python dictionary objects are also supported. @@ -383,13 +383,13 @@ You can optionally use this tag to configure the current PyScript environment. F === "my_template.html" ```jinja - {% include "../../examples/html/pyscript-setup-config-object.html" %} + {% include "../../examples/html/pyscript_setup_config_object.html" %} ``` === "views.py" ```python - {% include "../../examples/python/pyscript-setup-config-object.py" %} + {% include "../../examples/python/pyscript_setup_config_object.py" %} ``` ??? question "Can I use a local interpreter for PyScript?" @@ -403,5 +403,5 @@ You can optionally use this tag to configure the current PyScript environment. F 3. Configure your `#!jinja {% pyscript_setup %}` template tag to use `pyodide` as an interpreter. ```jinja linenums="0" - {% include "../../examples/html/pyscript-setup-local-interpreter.html" %} + {% include "../../examples/html/pyscript_setup_local_interpreter.html" %} ``` diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md index 917ba959..c5887d04 100644 --- a/docs/src/reference/utils.md +++ b/docs/src/reference/utils.md @@ -16,7 +16,7 @@ Utility functions provide various miscellaneous functionality for advanced use c This function is used register a Django view as a ReactPy `#!python iframe`. -It is mandatory to use this function alongside [`view_to_iframe`](../reference/components.md#view-to-iframe). +It is mandatory to use this function alongside [`view_to_iframe`](./components.md#view-to-iframe). === "apps.py" @@ -51,7 +51,7 @@ Typically, this function is automatically called on all components contained wit === "apps.py" ```python - {% include "../../examples/python/register-component.py" %} + {% include "../../examples/python/register_component.py" %} ``` ??? example "See Interface" @@ -76,7 +76,7 @@ Typically, this function is automatically called on all components contained wit For security reasons, ReactPy requires all root components to be registered. However, all components contained within Django templates are automatically registered. - This function is commonly needed when you have configured your [`host`](../reference/template-tag.md#component) to a dedicated Django rendering application that doesn't have templates. + This function is commonly needed when you have configured your [`host`](./template-tag.md#component) to a dedicated Django rendering application that doesn't have templates. --- @@ -89,13 +89,13 @@ Since ReactPy is rendered within an `#!python asyncio` loop, this postprocessor === "components.py" ```python - {% include "../../examples/python/django-query-postprocessor.py" %} + {% include "../../examples/python/django_query_postprocessor.py" %} ``` === "models.py" ```python - {% include "../../examples/python/example/models.py" %} + {% include "../../examples/python/todo_item_model.py" %} ``` ??? example "See Interface" diff --git a/pyproject.toml b/pyproject.toml index 44f920a6..dbb94c21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,7 +178,6 @@ linkcheck = [ ] deploy_latest = ["cd docs && mike deploy --push --update-aliases {args} latest"] deploy_develop = ["cd docs && mike deploy --push develop"] -check_examples = ["ruff check docs/examples/python"] ############################ # >>> Hatch JS Scripts <<< # @@ -210,6 +209,13 @@ lint.extend-ignore = [ "SLF001", # Private member accessed "E501", # Line too long "PLC0415", # `import` should be at the top-level of a file + "BLE001", # Do not catch blind exception: `Exception` + "PLW0603", # Using global statement is discouraged + "PLR6301", # Method could be a function, class method, or static method + "S403", # `dill` module is possibly insecure + "S301", # `dill` deserialization is possibly insecure unless using trusted data + "RUF029", # Function is declared async but doesn't contain await expression ] lint.preview = true -lint.isort.known-first-party = ["src", "tests"] +lint.isort.known-first-party = ["reactpy_django", "test_app", "example"] +lint.isort.known-third-party = ["js"] diff --git a/src/build_scripts/copy_dir.py b/src/build_scripts/copy_dir.py index 1f446f83..0a2cafab 100644 --- a/src/build_scripts/copy_dir.py +++ b/src/build_scripts/copy_dir.py @@ -1,3 +1,5 @@ +# ruff: noqa: INP001 +import logging import shutil import sys from pathlib import Path @@ -17,7 +19,7 @@ def copy_files(source: Path, destination: Path) -> None: if __name__ == "__main__": if len(sys.argv) != 3: - print("Usage: python copy_dir.py ") + logging.error("Script used incorrectly!\nUsage: python copy_dir.py ") sys.exit(1) root_dir = Path(__file__).parent.parent.parent @@ -25,7 +27,7 @@ def copy_files(source: Path, destination: Path) -> None: dest = Path(root_dir / sys.argv[2]) if not src.exists(): - print(f"Source directory {src} does not exist") + logging.error("Source directory %s does not exist", src) sys.exit(1) copy_files(src, dest) diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py index f3fb1545..b34398d8 100644 --- a/src/reactpy_django/__init__.py +++ b/src/reactpy_django/__init__.py @@ -16,13 +16,13 @@ __version__ = "5.1.0" __all__ = [ "REACTPY_WEBSOCKET_ROUTE", - "html", - "hooks", "components", "decorators", + "hooks", + "html", + "router", "types", "utils", - "router", ] # Fixes bugs with REACTPY_BACKHAUL_THREAD + built-in asyncio event loops. diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py index 740df974..888cc47d 100644 --- a/src/reactpy_django/checks.py +++ b/src/reactpy_django/checks.py @@ -17,19 +17,16 @@ def reactpy_warnings(app_configs, **kwargs): from reactpy_django.config import REACTPY_FAILED_COMPONENTS warnings = [] - INSTALLED_APPS: list[str] = getattr(settings, "INSTALLED_APPS", []) + installed_apps: list[str] = getattr(settings, "INSTALLED_APPS", []) # Check if REACTPY_DATABASE is not an in-memory database. if ( - getattr(settings, "DATABASES", {}) - .get(getattr(settings, "REACTPY_DATABASE", "default"), {}) - .get("NAME", None) + getattr(settings, "DATABASES", {}).get(getattr(settings, "REACTPY_DATABASE", "default"), {}).get("NAME", None) == ":memory:" ): warnings.append( Warning( - "Using ReactPy with an in-memory database can cause unexpected " - "behaviors.", + "Using ReactPy with an in-memory database can cause unexpected behaviors.", hint="Configure settings.py:DATABASES[REACTPY_DATABASE], to use a " "multiprocessing and thread safe database.", id="reactpy_django.W001", @@ -52,14 +49,12 @@ def reactpy_warnings(app_configs, **kwargs): ) # Warn if REACTPY_BACKHAUL_THREAD is set to True with Daphne - if ( - sys.argv[0].endswith("daphne") - or ("runserver" in sys.argv and "daphne" in INSTALLED_APPS) - ) and getattr(settings, "REACTPY_BACKHAUL_THREAD", False): + if (sys.argv[0].endswith("daphne") or ("runserver" in sys.argv and "daphne" in installed_apps)) and getattr( + settings, "REACTPY_BACKHAUL_THREAD", False + ): warnings.append( Warning( - "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled " - "and you running with Daphne.", + "Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled and you running with Daphne.", hint="Set settings.py:REACTPY_BACKHAUL_THREAD to False or use a different web server.", id="reactpy_django.W003", ) @@ -79,10 +74,8 @@ def reactpy_warnings(app_configs, **kwargs): if REACTPY_FAILED_COMPONENTS: warnings.append( Warning( - "ReactPy failed to register the following components:\n\t+ " - + "\n\t+ ".join(REACTPY_FAILED_COMPONENTS), - hint="Check if these paths are valid, or if an exception is being " - "raised during import.", + "ReactPy failed to register the following components:\n\t+ " + "\n\t+ ".join(REACTPY_FAILED_COMPONENTS), + hint="Check if these paths are valid, or if an exception is being raised during import.", id="reactpy_django.W005", ) ) @@ -106,10 +99,8 @@ def reactpy_warnings(app_configs, **kwargs): # Check if REACTPY_URL_PREFIX is being used properly in our HTTP URLs with contextlib.suppress(NoReverseMatch): - full_path = reverse("reactpy:web_modules", kwargs={"file": "example"}).strip( - "/" - ) - reactpy_http_prefix = f'{full_path[: full_path.find("web_module/")].strip("/")}' + full_path = reverse("reactpy:web_modules", kwargs={"file": "example"}).strip("/") + reactpy_http_prefix = f"{full_path[: full_path.find('web_module/')].strip('/')}" if reactpy_http_prefix != config.REACTPY_URL_PREFIX: warnings.append( Warning( @@ -138,9 +129,7 @@ def reactpy_warnings(app_configs, **kwargs): ) # Check if `daphne` is not in installed apps when using `runserver` - if "runserver" in sys.argv and "daphne" not in getattr( - settings, "INSTALLED_APPS", [] - ): + if "runserver" in sys.argv and "daphne" not in getattr(settings, "INSTALLED_APPS", []): warnings.append( Warning( "You have not configured the `runserver` command to use ASGI. " @@ -153,10 +142,7 @@ def reactpy_warnings(app_configs, **kwargs): # DELETED W013: Check if deprecated value REACTPY_RECONNECT_MAX exists # Check if REACTPY_RECONNECT_INTERVAL is set to a large value - if ( - isinstance(config.REACTPY_RECONNECT_INTERVAL, int) - and config.REACTPY_RECONNECT_INTERVAL > 30000 - ): + if isinstance(config.REACTPY_RECONNECT_INTERVAL, int) and config.REACTPY_RECONNECT_INTERVAL > 30000: warnings.append( Warning( "REACTPY_RECONNECT_INTERVAL is set to >30 seconds. Are you sure this is intentional? " @@ -167,10 +153,7 @@ def reactpy_warnings(app_configs, **kwargs): ) # Check if REACTPY_RECONNECT_MAX_RETRIES is set to a large value - if ( - isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) - and config.REACTPY_RECONNECT_MAX_RETRIES > 5000 - ): + if isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) and config.REACTPY_RECONNECT_MAX_RETRIES > 5000: warnings.append( Warning( "REACTPY_RECONNECT_MAX_RETRIES is set to a very large value " @@ -204,18 +187,12 @@ def reactpy_warnings(app_configs, **kwargs): and config.REACTPY_RECONNECT_MAX_INTERVAL > 0 and config.REACTPY_RECONNECT_MAX_RETRIES > 0 and config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER > 1 - and ( - config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER - ** config.REACTPY_RECONNECT_MAX_RETRIES - ) + and (config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER**config.REACTPY_RECONNECT_MAX_RETRIES) * config.REACTPY_RECONNECT_INTERVAL < config.REACTPY_RECONNECT_MAX_INTERVAL ): max_value = math.floor( - ( - config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER - ** config.REACTPY_RECONNECT_MAX_RETRIES - ) + (config.REACTPY_RECONNECT_BACKOFF_MULTIPLIER**config.REACTPY_RECONNECT_MAX_RETRIES) * config.REACTPY_RECONNECT_INTERVAL ) warnings.append( @@ -229,13 +206,10 @@ def reactpy_warnings(app_configs, **kwargs): # Check if 'reactpy_django' is in the correct position in INSTALLED_APPS position_to_beat = 0 - for app in INSTALLED_APPS: + for app in installed_apps: if app.startswith("django.contrib."): - position_to_beat = INSTALLED_APPS.index(app) - if ( - "reactpy_django" in INSTALLED_APPS - and INSTALLED_APPS.index("reactpy_django") < position_to_beat - ): + position_to_beat = installed_apps.index(app) + if "reactpy_django" in installed_apps and installed_apps.index("reactpy_django") < position_to_beat: warnings.append( Warning( "The position of 'reactpy_django' in INSTALLED_APPS is suspicious.", @@ -276,17 +250,13 @@ def reactpy_errors(app_configs, **kwargs): ) # DATABASE_ROUTERS is properly configured when REACTPY_DATABASE is defined - if getattr( - settings, "REACTPY_DATABASE", None - ) and "reactpy_django.database.Router" not in getattr( + if getattr(settings, "REACTPY_DATABASE", None) and "reactpy_django.database.Router" not in getattr( settings, "DATABASE_ROUTERS", [] ): errors.append( Error( - "ReactPy database has been changed but the database router is " - "not configured.", - hint="Set settings.py:DATABASE_ROUTERS to " - "['reactpy_django.database.Router', ...]", + "ReactPy database has been changed but the database router is not configured.", + hint="Set settings.py:DATABASE_ROUTERS to ['reactpy_django.database.Router', ...]", id="reactpy_django.E002", ) ) @@ -336,9 +306,7 @@ def reactpy_errors(app_configs, **kwargs): ) # Check if REACTPY_DEFAULT_QUERY_POSTPROCESSOR is a valid data type - if not isinstance( - getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None)) - ): + if not isinstance(getattr(settings, "REACTPY_DEFAULT_QUERY_POSTPROCESSOR", ""), (str, type(None))): errors.append( Error( "Invalid type for REACTPY_DEFAULT_QUERY_POSTPROCESSOR.", @@ -397,10 +365,7 @@ def reactpy_errors(app_configs, **kwargs): ) # Check if REACTPY_RECONNECT_INTERVAL is a positive integer - if ( - isinstance(config.REACTPY_RECONNECT_INTERVAL, int) - and config.REACTPY_RECONNECT_INTERVAL < 0 - ): + if isinstance(config.REACTPY_RECONNECT_INTERVAL, int) and config.REACTPY_RECONNECT_INTERVAL < 0: errors.append( Error( "Invalid value for REACTPY_RECONNECT_INTERVAL.", @@ -420,10 +385,7 @@ def reactpy_errors(app_configs, **kwargs): ) # Check if REACTPY_RECONNECT_MAX_INTERVAL is a positive integer - if ( - isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) - and config.REACTPY_RECONNECT_MAX_INTERVAL < 0 - ): + if isinstance(config.REACTPY_RECONNECT_MAX_INTERVAL, int) and config.REACTPY_RECONNECT_MAX_INTERVAL < 0: errors.append( Error( "Invalid value for REACTPY_RECONNECT_MAX_INTERVAL.", @@ -457,10 +419,7 @@ def reactpy_errors(app_configs, **kwargs): ) # Check if REACTPY_RECONNECT_MAX_RETRIES is a positive integer - if ( - isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) - and config.REACTPY_RECONNECT_MAX_RETRIES < 0 - ): + if isinstance(config.REACTPY_RECONNECT_MAX_RETRIES, int) and config.REACTPY_RECONNECT_MAX_RETRIES < 0: errors.append( Error( "Invalid value for REACTPY_RECONNECT_MAX_RETRIES.", @@ -523,10 +482,7 @@ def reactpy_errors(app_configs, **kwargs): ) # Check if REACTPY_CLEAN_INTERVAL is a positive integer - if ( - isinstance(config.REACTPY_CLEAN_INTERVAL, int) - and config.REACTPY_CLEAN_INTERVAL < 0 - ): + if isinstance(config.REACTPY_CLEAN_INTERVAL, int) and config.REACTPY_CLEAN_INTERVAL < 0: errors.append( Error( "Invalid value for REACTPY_CLEAN_INTERVAL.", diff --git a/src/reactpy_django/clean.py b/src/reactpy_django/clean.py index 1ec327ee..0a7e9017 100644 --- a/src/reactpy_django/clean.py +++ b/src/reactpy_django/clean.py @@ -12,9 +12,7 @@ if TYPE_CHECKING: from reactpy_django.models import Config -CLEAN_NEEDED_BY: datetime = datetime( - year=1, month=1, day=1, tzinfo=timezone.now().tzinfo -) +CLEAN_NEEDED_BY: datetime = datetime(year=1, month=1, day=1, tzinfo=timezone.now().tzinfo) def clean( @@ -36,8 +34,8 @@ def clean( user_data = REACTPY_CLEAN_USER_DATA if args: - sessions = any(value in args for value in {"sessions", "all"}) - user_data = any(value in args for value in {"user_data", "all"}) + sessions = any(value in args for value in ("sessions", "all")) + user_data = any(value in args for value in ("user_data", "all")) if sessions: clean_sessions(verbosity) @@ -54,16 +52,14 @@ def clean_sessions(verbosity: int = 1): from reactpy_django.models import ComponentSession if verbosity >= 2: - print("Cleaning ReactPy component sessions...") + _logger.info("Cleaning ReactPy component sessions...") start_time = timezone.now() expiration_date = timezone.now() - timedelta(seconds=REACTPY_SESSION_MAX_AGE) - session_objects = ComponentSession.objects.filter( - last_accessed__lte=expiration_date - ) + session_objects = ComponentSession.objects.filter(last_accessed__lte=expiration_date) if verbosity >= 2: - print(f"Deleting {session_objects.count()} expired component sessions...") + _logger.info("Deleting %d expired component sessions...", session_objects.count()) session_objects.delete() @@ -83,7 +79,7 @@ def clean_user_data(verbosity: int = 1): from reactpy_django.models import UserDataModel if verbosity >= 2: - print("Cleaning ReactPy user data...") + _logger.info("Cleaning ReactPy user data...") start_time = timezone.now() user_model = get_user_model() @@ -92,14 +88,12 @@ def clean_user_data(verbosity: int = 1): # Django doesn't support using QuerySets as an argument with cross-database relations. if user_model.objects.db != UserDataModel.objects.db: - all_user_pks = list(all_user_pks) # type: ignore + all_user_pks = list(all_user_pks) user_data_objects = UserDataModel.objects.exclude(user_pk__in=all_user_pks) if verbosity >= 2: - print( - f"Deleting {user_data_objects.count()} user data objects not associated with an existing user..." - ) + _logger.info("Deleting %d user data objects not associated with an existing user...", user_data_objects.count()) user_data_objects.delete() @@ -129,9 +123,7 @@ def inspect_clean_duration(start_time: datetime, task_name: str, verbosity: int) clean_duration = timezone.now() - start_time if verbosity >= 3: - print( - f"Cleaned ReactPy {task_name} in {clean_duration.total_seconds()} seconds." - ) + _logger.info("Cleaned ReactPy %s in %s seconds.", task_name, clean_duration.total_seconds()) if clean_duration.total_seconds() > 1: _logger.warning( diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index 98981730..d9ed0e6a 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -2,7 +2,7 @@ import json import os -from typing import Any, Callable, Sequence, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Union, cast from urllib.parse import urlencode from uuid import uuid4 @@ -10,7 +10,6 @@ from django.core.cache import caches from django.http import HttpRequest from django.urls import reverse -from django.views import View from reactpy import component, hooks, html, utils from reactpy.types import ComponentType, Key, VdomDict @@ -24,6 +23,11 @@ vdom_or_component_to_string, ) +if TYPE_CHECKING: + from collections.abc import Sequence + + from django.views import View + def view_to_component( view: Callable | View | str, @@ -62,9 +66,7 @@ def constructor( return constructor -def view_to_iframe( - view: Callable | View | str, extra_props: dict[str, Any] | None = None -): +def view_to_iframe(view: Callable | View | str, extra_props: dict[str, Any] | None = None): """ Args: view: The view function or class to convert, or the dotted path to the view. @@ -81,9 +83,7 @@ def constructor( key: Key | None = None, **kwargs, ): - return _view_to_iframe( - view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key - ) + return _view_to_iframe(view=view, extra_props=extra_props, args=args, kwargs=kwargs, key=key) return constructor @@ -147,9 +147,7 @@ def _view_to_component( kwargs: dict | None, ): """The actual component. Used to prevent pollution of acceptable kwargs keys.""" - converted_view, set_converted_view = hooks.use_state( - cast(Union[VdomDict, None], None) - ) + converted_view, set_converted_view = hooks.use_state(cast(Union[VdomDict, None], None)) _args: Sequence = args or () _kwargs: dict = kwargs or {} if request: @@ -157,13 +155,13 @@ def _view_to_component( else: _request = HttpRequest() _request.method = "GET" - resolved_view: Callable = import_module(view) if isinstance(view, str) else view # type: ignore[assignment] + resolved_view: Callable = import_module(view) if isinstance(view, str) else view # Render the view render within a hook @hooks.use_effect( dependencies=[ - json.dumps(vars(_request), default=lambda x: generate_obj_name(x)), - json.dumps([_args, _kwargs], default=lambda x: generate_obj_name(x)), + json.dumps(vars(_request), default=generate_obj_name), + json.dumps([_args, _kwargs], default=generate_obj_name), ] ) async def async_render(): @@ -199,10 +197,11 @@ def _view_to_iframe( registered_view = REACTPY_REGISTERED_IFRAME_VIEWS.get(dotted_path) if not registered_view: - raise ViewNotRegisteredError( + msg = ( f"'{dotted_path}' has not been registered as an iframe! " "Are you sure you called `register_iframe` within a Django `AppConfig.ready` method?" ) + raise ViewNotRegisteredError(msg) query = kwargs.copy() if args: @@ -237,23 +236,18 @@ def _cached_static_contents(static_path: str) -> str: # Try to find the file within Django's static files abs_path = find(static_path) if not abs_path: - raise FileNotFoundError( - f"Could not find static file {static_path} within Django's static files." - ) + msg = f"Could not find static file {static_path} within Django's static files." + raise FileNotFoundError(msg) # Fetch the file from cache, if available last_modified_time = os.stat(abs_path).st_mtime cache_key = f"reactpy_django:static_contents:{static_path}" - file_contents: str | None = caches[REACTPY_CACHE].get( - cache_key, version=int(last_modified_time) - ) + file_contents: str | None = caches[REACTPY_CACHE].get(cache_key, version=int(last_modified_time)) if file_contents is None: with open(abs_path, encoding="utf-8") as static_file: file_contents = static_file.read() caches[REACTPY_CACHE].delete(cache_key) - caches[REACTPY_CACHE].set( - cache_key, file_contents, timeout=None, version=int(last_modified_time) - ) + caches[REACTPY_CACHE].set(cache_key, file_contents, timeout=None, version=int(last_modified_time)) return file_contents diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 090980a5..3f46c48b 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -1,22 +1,25 @@ from __future__ import annotations from itertools import cycle -from typing import Callable +from typing import TYPE_CHECKING, Callable from django.conf import settings 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 -from reactpy_django.types import ( - AsyncPostprocessor, - SyncPostprocessor, -) from reactpy_django.utils import import_dotted_path +if TYPE_CHECKING: + from django.views import View + from reactpy.core.types import ComponentConstructor + + from reactpy_django.types import ( + AsyncPostprocessor, + SyncPostprocessor, + ) + # Non-configurable values REACTPY_REGISTERED_COMPONENTS: dict[str, ComponentConstructor] = {} REACTPY_FAILED_COMPONENTS: set[str] = set() @@ -25,9 +28,7 @@ # Configurable through Django settings.py DJANGO_DEBUG = settings.DEBUG # Snapshot of Django's DEBUG setting _REACTPY_DEBUG_MODE.set_current(settings.DEBUG) -_REACTPY_ASYNC_RENDERING.set_current( - getattr(settings, "REACTPY_ASYNC_RENDERING", _REACTPY_ASYNC_RENDERING.current) -) +_REACTPY_ASYNC_RENDERING.set_current(getattr(settings, "REACTPY_ASYNC_RENDERING", _REACTPY_ASYNC_RENDERING.current)) REACTPY_URL_PREFIX: str = getattr( settings, "REACTPY_URL_PREFIX", @@ -59,10 +60,7 @@ else: REACTPY_DEFAULT_QUERY_POSTPROCESSOR = import_dotted_path( "reactpy_django.utils.django_query_postprocessor" - if ( - _default_query_postprocessor == "UNSET" - or not isinstance(_default_query_postprocessor, str) - ) + if (_default_query_postprocessor == "UNSET" or not isinstance(_default_query_postprocessor, str)) else _default_query_postprocessor ) REACTPY_AUTH_BACKEND: str | None = getattr( @@ -81,9 +79,7 @@ None, ) REACTPY_DEFAULT_HOSTS: cycle[str] | None = ( - cycle([host.strip("/") for host in _default_hosts if isinstance(host, str)]) - if _default_hosts - else None + cycle([host.strip("/") for host in _default_hosts if isinstance(host, str)]) if _default_hosts else None ) REACTPY_RECONNECT_INTERVAL: int = getattr( settings, diff --git a/src/reactpy_django/database.py b/src/reactpy_django/database.py index 0d0b2065..2acd673e 100644 --- a/src/reactpy_django/database.py +++ b/src/reactpy_django/database.py @@ -1,3 +1,5 @@ +from typing import ClassVar + from reactpy_django.config import REACTPY_DATABASE @@ -7,17 +9,19 @@ class Router: auth and contenttypes applications. """ - route_app_labels = {"reactpy_django"} + route_app_labels: ClassVar[set[str]] = {"reactpy_django"} def db_for_read(self, model, **hints): """Attempts to read go to REACTPY_DATABASE.""" if model._meta.app_label in self.route_app_labels: return REACTPY_DATABASE + return None def db_for_write(self, model, **hints): """Attempts to write go to REACTPY_DATABASE.""" if model._meta.app_label in self.route_app_labels: return REACTPY_DATABASE + return None def allow_relation(self, obj1, obj2, **hints): """Returning `None` only allow relations within the same database. @@ -27,5 +31,4 @@ def allow_relation(self, obj1, obj2, **hints): def allow_migrate(self, db, app_label, model_name=None, **hints): """Make sure ReactPy models only appear in REACTPY_DATABASE.""" - if app_label in self.route_app_labels: - return db == REACTPY_DATABASE + return db == REACTPY_DATABASE if app_label in self.route_app_labels else None diff --git a/src/reactpy_django/decorators.py b/src/reactpy_django/decorators.py index d5278f7d..804e10bb 100644 --- a/src/reactpy_django/decorators.py +++ b/src/reactpy_django/decorators.py @@ -4,13 +4,13 @@ from typing import TYPE_CHECKING, Any, Callable from reactpy import component -from reactpy.core.types import ComponentConstructor from reactpy_django.exceptions import DecoratorParamError from reactpy_django.hooks import use_user if TYPE_CHECKING: from django.contrib.auth.models import AbstractUser + from reactpy.core.types import ComponentConstructor def user_passes_test( @@ -31,9 +31,7 @@ def user_passes_test( def decorator(user_component): @wraps(user_component) def _wrapper(*args, **kwargs): - return _user_passes_test( - user_component, fallback, test_func, *args, **kwargs - ) + return _user_passes_test(user_component, fallback, test_func, *args, **kwargs) return _wrapper @@ -49,10 +47,11 @@ def _user_passes_test(component_constructor, fallback, test_func, *args, **kwarg # Ensure that the component is a ReactPy component. user_component = component_constructor(*args, **kwargs) if not getattr(user_component, "render", None): - raise DecoratorParamError( + msg = ( "`user_passes_test` is not decorating a ReactPy component. " "Did you forget `@user_passes_test` must be ABOVE the `@component` decorator?" ) + raise DecoratorParamError(msg) # Render the component. return user_component diff --git a/src/reactpy_django/exceptions.py b/src/reactpy_django/exceptions.py index 412d647f..c0d4b32d 100644 --- a/src/reactpy_django/exceptions.py +++ b/src/reactpy_django/exceptions.py @@ -1,34 +1,25 @@ -class ComponentParamError(TypeError): - ... +class ComponentParamError(TypeError): ... -class ComponentDoesNotExistError(AttributeError): - ... +class ComponentDoesNotExistError(AttributeError): ... -class OfflineComponentMissing(ComponentDoesNotExistError): - ... +class OfflineComponentMissingError(ComponentDoesNotExistError): ... -class InvalidHostError(ValueError): - ... +class InvalidHostError(ValueError): ... -class ComponentCarrierError(Exception): - ... +class ComponentCarrierError(Exception): ... -class UserNotFoundError(Exception): - ... +class UserNotFoundError(Exception): ... -class ViewNotRegisteredError(AttributeError): - ... +class ViewNotRegisteredError(AttributeError): ... -class ViewDoesNotExistError(AttributeError): - ... +class ViewDoesNotExistError(AttributeError): ... -class DecoratorParamError(TypeError): - ... +class DecoratorParamError(TypeError): ... diff --git a/src/reactpy_django/hooks.py b/src/reactpy_django/hooks.py index c226e1ca..9f76902f 100644 --- a/src/reactpy_django/hooks.py +++ b/src/reactpy_django/hooks.py @@ -2,13 +2,11 @@ import asyncio import logging +from collections import defaultdict from typing import ( TYPE_CHECKING, Any, - Awaitable, Callable, - DefaultDict, - Sequence, Union, cast, ) @@ -22,7 +20,6 @@ from reactpy import use_connection as _use_connection from reactpy import use_location as _use_location from reactpy import use_scope as _use_scope -from reactpy.backend.types import Location from reactpy_django.exceptions import UserNotFoundError from reactpy_django.types import ( @@ -40,14 +37,15 @@ from reactpy_django.utils import django_query_postprocessor, generate_obj_name, get_pk if TYPE_CHECKING: + from collections.abc import Awaitable, Sequence + from channels_redis.core import RedisChannelLayer from django.contrib.auth.models import AbstractUser + from reactpy.backend.types import Location _logger = logging.getLogger(__name__) -_REFETCH_CALLBACKS: DefaultDict[Callable[..., Any], set[Callable[[], None]]] = ( - DefaultDict(set) -) +_REFETCH_CALLBACKS: defaultdict[Callable[..., Any], set[Callable[[], None]]] = defaultdict(set) def use_location() -> Location: @@ -62,21 +60,11 @@ def use_origin() -> str | None: try: if scope["type"] == "websocket": return next( - ( - header[1].decode("utf-8") - for header in scope["headers"] - if header[0] == b"origin" - ), + (header[1].decode("utf-8") for header in scope["headers"] if header[0] == b"origin"), None, ) if scope["type"] == "http": - host = next( - ( - header[1].decode("utf-8") - for header in scope["headers"] - if header[0] == b"host" - ) - ) + host = next(header[1].decode("utf-8") for header in scope["headers"] if header[0] == b"host") return f"{scope['scheme']}://{host}" if host else None except Exception: _logger.info("Failed to get origin") @@ -90,7 +78,8 @@ def use_scope() -> dict[str, Any]: if isinstance(scope, dict): return scope - raise TypeError(f"Expected scope to be a dict, got {type(scope)}") + msg = f"Expected scope to be a dict, got {type(scope)}" + raise TypeError(msg) def use_connection() -> ConnectionType: @@ -103,9 +92,7 @@ def use_query( kwargs: dict[str, Any] | None = None, *, thread_sensitive: bool = True, - postprocessor: ( - AsyncPostprocessor | SyncPostprocessor | None - ) = django_query_postprocessor, + postprocessor: (AsyncPostprocessor | SyncPostprocessor | None) = django_query_postprocessor, postprocessor_kwargs: dict[str, Any] | None = None, ) -> Query[Inferred]: """This hook is used to execute functions in the background and return the result, \ @@ -139,31 +126,31 @@ def use_query( loading, set_loading = use_state(True) error, set_error = use_state(cast(Union[Exception, None], None)) query_ref = use_ref(query) + async_task_refs = use_ref(set()) kwargs = kwargs or {} postprocessor_kwargs = postprocessor_kwargs or {} if query_ref.current is not query: - raise ValueError(f"Query function changed from {query_ref.current} to {query}.") + msg = f"Query function changed from {query_ref.current} to {query}." + raise ValueError(msg) async def execute_query() -> None: """The main running function for `use_query`""" try: # Run the query if asyncio.iscoroutinefunction(query): - new_data = await query(**kwargs) # type: ignore[call-arg] + new_data = await query(**kwargs) else: - new_data = await database_sync_to_async( - query, thread_sensitive=thread_sensitive - )(**kwargs) + new_data = await database_sync_to_async(query, thread_sensitive=thread_sensitive)(**kwargs) # Run the postprocessor if postprocessor: if asyncio.iscoroutinefunction(postprocessor): new_data = await postprocessor(new_data, **postprocessor_kwargs) else: - new_data = await database_sync_to_async( - postprocessor, thread_sensitive=thread_sensitive - )(new_data, **postprocessor_kwargs) + new_data = await database_sync_to_async(postprocessor, thread_sensitive=thread_sensitive)( + new_data, **postprocessor_kwargs + ) # Log any errors and set the error state except Exception as e: @@ -181,14 +168,18 @@ async def execute_query() -> None: @use_effect(dependencies=None) def schedule_query() -> None: - """Schedule the query to be run when needed""" + """Schedule the query to be run""" # Make sure we don't re-execute the query unless we're told to if not should_execute: return set_should_execute(False) # Execute the query in the background - asyncio.create_task(execute_query()) + task = asyncio.create_task(execute_query()) + + # Add the task to a set to prevent it from being garbage collected + async_task_refs.current.add(task) + task.add_done_callback(async_task_refs.current.remove) @use_callback def refetch() -> None: @@ -210,9 +201,7 @@ def register_refetch_callback() -> Callable[[], None]: def use_mutation( - mutation: ( - Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]] - ), + mutation: (Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]), *, thread_sensitive: bool = True, refetch: Callable[..., Any] | Sequence[Callable[..., Any]] | None = None, @@ -245,6 +234,7 @@ def use_mutation( loading, set_loading = use_state(False) error, set_error = use_state(cast(Union[Exception, None], None)) + async_task_refs = use_ref(set()) # The main "running" function for `use_mutation` async def execute_mutation(exec_args, exec_kwargs) -> None: @@ -253,17 +243,15 @@ async def execute_mutation(exec_args, exec_kwargs) -> None: if asyncio.iscoroutinefunction(mutation): should_refetch = await mutation(*exec_args, **exec_kwargs) else: - should_refetch = await database_sync_to_async( - mutation, thread_sensitive=thread_sensitive - )(*exec_args, **exec_kwargs) + should_refetch = await database_sync_to_async(mutation, thread_sensitive=thread_sensitive)( + *exec_args, **exec_kwargs + ) # Log any errors and set the error state except Exception as e: set_loading(False) set_error(e) - _logger.exception( - "Failed to execute mutation: %s", generate_obj_name(mutation) - ) + _logger.exception("Failed to execute mutation: %s", generate_obj_name(mutation)) # Mutation was successful else: @@ -279,18 +267,18 @@ async def execute_mutation(exec_args, exec_kwargs) -> None: # Schedule the mutation to be run when needed @use_callback - def schedule_mutation( - *exec_args: FuncParams.args, **exec_kwargs: FuncParams.kwargs - ) -> None: + def schedule_mutation(*exec_args: FuncParams.args, **exec_kwargs: FuncParams.kwargs) -> None: # Set the loading state. # It's okay to re-execute the mutation if we're told to. The user # can use the `loading` state to prevent this. set_loading(True) # Execute the mutation in the background - asyncio.ensure_future( - execute_mutation(exec_args=exec_args, exec_kwargs=exec_kwargs) - ) + task = asyncio.ensure_future(execute_mutation(exec_args=exec_args, exec_kwargs=exec_kwargs)) + + # Add the task to a set to prevent it from being garbage collected + async_task_refs.current.add(task) + task.add_done_callback(async_task_refs.current.remove) # Used when the user has told us to reset this mutation @use_callback @@ -307,14 +295,13 @@ def use_user() -> AbstractUser: connection = use_connection() user = connection.scope.get("user") or getattr(connection.carrier, "user", None) if user is None: - raise UserNotFoundError("No user is available in the current environment.") + msg = "No user is available in the current environment." + raise UserNotFoundError(msg) return user def use_user_data( - default_data: ( - None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any] - ) = None, + default_data: (None | dict[str, Callable[[], Any] | Callable[[], Awaitable[Any]] | Any]) = None, save_default_data: bool = False, ) -> UserData: """Get or set user data stored within the REACTPY_DATABASE. @@ -332,9 +319,11 @@ def use_user_data( async def _set_user_data(data: dict): if not isinstance(data, dict): - raise TypeError(f"Expected dict while setting user data, got {type(data)}") + msg = f"Expected dict while setting user data, got {type(data)}" + raise TypeError(msg) if user.is_anonymous: - raise ValueError("AnonymousUser cannot have user data.") + msg = "AnonymousUser cannot have user data." + raise ValueError(msg) pk = get_pk(user) model, _ = await UserDataModel.objects.aget_or_create(user_pk=pk) @@ -386,13 +375,15 @@ def use_channel_layer( channel_name = use_memo(lambda: str(name or uuid4())) if not name and not group_name: - raise ValueError("You must define a `name` or `group_name` for the channel.") + msg = "You must define a `name` or `group_name` for the channel." + raise ValueError(msg) if not channel_layer: - raise ValueError( + msg = ( f"Channel layer '{layer}' is not available. Are you sure you" " configured settings.py:CHANNEL_LAYERS properly?" ) + raise ValueError(msg) # Add/remove a group's channel during component mount/dismount respectively. @use_effect(dependencies=[]) @@ -401,9 +392,8 @@ async def group_manager(): await channel_layer.group_add(group_name, channel_name) if group_name and group_discard: - return lambda: asyncio.run( - channel_layer.group_discard(group_name, channel_name) - ) + return lambda: asyncio.run(channel_layer.group_discard(group_name, channel_name)) + return None # Listen for messages on the channel using the provided `receiver` function. @use_effect @@ -433,9 +423,7 @@ def use_root_id() -> str: return scope["reactpy"]["id"] -async def _get_user_data( - user: AbstractUser, default_data: None | dict, save_default_data: bool -) -> dict | None: +async def _get_user_data(user: AbstractUser, default_data: None | dict, save_default_data: bool) -> dict | None: """The mutation function for `use_user_data`""" from reactpy_django.models import UserDataModel @@ -447,7 +435,8 @@ async def _get_user_data( data = orjson.loads(model.data) if model.data else {} if not isinstance(data, dict): - raise TypeError(f"Expected dict while loading user data, got {type(data)}") + msg = f"Expected dict while loading user data, got {type(data)}" + raise TypeError(msg) # Set default values, if needed if default_data: diff --git a/src/reactpy_django/http/urls.py b/src/reactpy_django/http/urls.py index def755e4..11f3ec31 100644 --- a/src/reactpy_django/http/urls.py +++ b/src/reactpy_django/http/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from . import views +from reactpy_django.http import views app_name = "reactpy" diff --git a/src/reactpy_django/http/views.py b/src/reactpy_django/http/views.py index 780ccc17..25315479 100644 --- a/src/reactpy_django/http/views.py +++ b/src/reactpy_django/http/views.py @@ -1,45 +1,25 @@ -import asyncio import os from urllib.parse import parse_qs -from django.core.cache import caches from django.core.exceptions import SuspiciousOperation -from django.http import HttpRequest, HttpResponse, HttpResponseNotFound +from django.http import FileResponse, HttpRequest, HttpResponse, HttpResponseNotFound from reactpy.config import REACTPY_WEB_MODULES_DIR -from reactpy_django.utils import create_cache_key, render_view +from reactpy_django.utils import FileAsyncIterator, render_view -async def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: - """Gets JavaScript required for ReactPy modules at runtime. These modules are - returned from cache if available.""" - from reactpy_django.config import REACTPY_CACHE +def web_modules_file(request: HttpRequest, file: str) -> HttpResponse: + """Gets JavaScript required for ReactPy modules at runtime.""" web_modules_dir = REACTPY_WEB_MODULES_DIR.current path = os.path.abspath(web_modules_dir.joinpath(file)) # Prevent attempts to walk outside of the web modules dir if str(web_modules_dir) != os.path.commonpath((path, web_modules_dir)): - raise SuspiciousOperation( - "Attempt to access a directory outside of REACTPY_WEB_MODULES_DIR." - ) + msg = "Attempt to access a directory outside of REACTPY_WEB_MODULES_DIR." + raise SuspiciousOperation(msg) - # Fetch the file from cache, if available - last_modified_time = os.stat(path).st_mtime - cache_key = create_cache_key("web_modules", path) - file_contents = await caches[REACTPY_CACHE].aget( - cache_key, version=int(last_modified_time) - ) - if file_contents is None: - with open(path, "r", encoding="utf-8") as fp: - file_contents = await asyncio.to_thread(fp.read) - await caches[REACTPY_CACHE].adelete(cache_key) - 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") + return FileResponse(FileAsyncIterator(path), content_type="text/javascript") async def view_to_iframe(request: HttpRequest, dotted_path: str) -> HttpResponse: diff --git a/src/reactpy_django/management/commands/clean_reactpy.py b/src/reactpy_django/management/commands/clean_reactpy.py index 0c5dc308..1b1fe9a2 100644 --- a/src/reactpy_django/management/commands/clean_reactpy.py +++ b/src/reactpy_django/management/commands/clean_reactpy.py @@ -1,7 +1,10 @@ +from logging import getLogger from typing import Literal from django.core.management.base import BaseCommand +_logger = getLogger(__name__) + class Command(BaseCommand): help = "Manually clean ReactPy data. When using this command without args, it will perform all cleaning operations." @@ -22,7 +25,7 @@ def handle(self, **options): clean(*cleaning_args, immediate=True, verbosity=verbosity) if verbosity >= 1: - print("ReactPy data has been cleaned!") + _logger.info("ReactPy data has been cleaned!") def add_arguments(self, parser): parser.add_argument( diff --git a/src/reactpy_django/models.py b/src/reactpy_django/models.py index 5256fba6..15f07595 100644 --- a/src/reactpy_django/models.py +++ b/src/reactpy_django/models.py @@ -9,15 +9,15 @@ class ComponentSession(models.Model): """A model for storing component sessions.""" - uuid = models.UUIDField(primary_key=True, editable=False, unique=True) # type: ignore - params = models.BinaryField(editable=False) # type: ignore - last_accessed = models.DateTimeField(auto_now=True) # type: ignore + uuid = models.UUIDField(primary_key=True, editable=False, unique=True) + params = models.BinaryField(editable=False) + last_accessed = models.DateTimeField(auto_now=True) class Config(models.Model): """A singleton model for storing ReactPy configuration.""" - cleaned_at = models.DateTimeField(auto_now_add=True) # type: ignore + cleaned_at = models.DateTimeField(auto_now_add=True) def save(self, *args, **kwargs): """Singleton save method.""" @@ -36,8 +36,8 @@ class UserDataModel(models.Model): # We can't store User as a ForeignKey/OneToOneField because it may not be in the same database # and Django does not allow cross-database relations. Also, since we can't know the type of the UserModel PK, # we store it as a string to normalize. - user_pk = models.CharField(max_length=255, unique=True) # type: ignore - data = models.BinaryField(null=True, blank=True) # type: ignore + user_pk = models.CharField(max_length=255, unique=True) + data = models.BinaryField(null=True, blank=True) @receiver(pre_delete, sender=get_user_model(), dispatch_uid="reactpy_delete_user_data") diff --git a/src/reactpy_django/pyscript/component_template.py b/src/reactpy_django/pyscript/component_template.py index 59442571..0dfb27b7 100644 --- a/src/reactpy_django/pyscript/component_template.py +++ b/src/reactpy_django/pyscript/component_template.py @@ -1,3 +1,4 @@ +# ruff: noqa: TCH004, N802, N816, RUF006 from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/src/reactpy_django/pyscript/layout_handler.py b/src/reactpy_django/pyscript/layout_handler.py index f5a72fa9..77aa8c81 100644 --- a/src/reactpy_django/pyscript/layout_handler.py +++ b/src/reactpy_django/pyscript/layout_handler.py @@ -1,5 +1,11 @@ -# mypy: disable-error-code=attr-defined import asyncio +import logging + +import js +from jsonpointer import set_pointer +from pyodide.ffi.wrappers import add_event_listener +from pyscript.js_modules import morphdom +from reactpy.core.layout import Layout class ReactPyLayoutHandler: @@ -12,11 +18,11 @@ class ReactPyLayoutHandler: def __init__(self, uuid): self.uuid = uuid + self.running_tasks = set() @staticmethod def update_model(update, root_model): """Apply an update ReactPy's internal DOM model.""" - from jsonpointer import set_pointer if update["path"]: set_pointer(root_model, update["path"], update["model"]) @@ -25,9 +31,6 @@ def update_model(update, root_model): def render_html(self, layout, model): """Submit ReactPy's internal DOM model into the HTML DOM.""" - from pyscript.js_modules import morphdom - - import js # Create a new container to render the layout into container = js.document.getElementById(f"pyscript-{self.uuid}") @@ -42,8 +45,6 @@ def render_html(self, layout, model): def build_element_tree(self, layout, parent, model): """Recursively build an element tree, starting from the root component.""" - import js - if isinstance(model, str): parent.appendChild(js.document.createTextNode(model)) elif isinstance(model, dict): @@ -63,30 +64,26 @@ def build_element_tree(self, layout, parent, model): element.className = value else: element.setAttribute(key, value) - for event_name, event_handler_model in model.get( - "eventHandlers", {} - ).items(): - self.create_event_handler( - layout, element, event_name, event_handler_model - ) + for event_name, event_handler_model in model.get("eventHandlers", {}).items(): + self.create_event_handler(layout, element, event_name, event_handler_model) for child in children: self.build_element_tree(layout, element, child) parent.appendChild(element) else: - raise ValueError(f"Unknown model type: {type(model)}") + msg = f"Unknown model type: {type(model)}" + raise TypeError(msg) - @staticmethod - def create_event_handler(layout, element, event_name, event_handler_model): + def create_event_handler(self, layout, element, event_name, event_handler_model): """Create an event handler for an element. This function is used as an adapter between ReactPy and browser events.""" - from pyodide.ffi.wrappers import add_event_listener - target = event_handler_model["target"] def event_handler(*args): - asyncio.create_task( - layout.deliver({"type": "layout-event", "target": target, "data": args}) - ) + task = asyncio.create_task(layout.deliver({"type": "layout-event", "target": target, "data": args})) + + # Add the task to a set to prevent it from being garbage collected + self.running_tasks.add(task) + task.add_done_callback(self.running_tasks.remove) event_name = event_name.lstrip("on_").lower().replace("_", "") add_event_listener(element, event_name, event_handler) @@ -97,15 +94,9 @@ def delete_old_workspaces(): it is no longer in use (removed from the page). To do this, we compare what UUIDs exist on the DOM, versus what UUIDs exist within the PyScript global interpreter.""" - import js - dom_workspaces = js.document.querySelectorAll(".pyscript") dom_uuids = {element.dataset.uuid for element in dom_workspaces} - python_uuids = { - value.split("_")[-1] - for value in globals() - if value.startswith("user_workspace_") - } + python_uuids = {value.split("_")[-1] for value in globals() if value.startswith("user_workspace_")} # Delete any workspaces that are not being used for uuid in python_uuids - dom_uuids: @@ -115,20 +106,16 @@ def delete_old_workspaces(): task.cancel() del globals()[task_name] else: - print(f"Warning: Could not auto delete PyScript task {task_name}") + logging.error("Could not auto delete PyScript task %s", task_name) workspace_name = f"user_workspace_{uuid}" if workspace_name in globals(): del globals()[workspace_name] else: - print( - f"Warning: Could not auto delete PyScript workspace {workspace_name}" - ) + logging.error("Could not auto delete PyScript workspace %s", workspace_name) async def run(self, workspace_function): """Run the layout handler. This function is main executor for all user generated code.""" - from reactpy.core.layout import Layout - self.delete_old_workspaces() root_model: dict = {} diff --git a/src/reactpy_django/router/converters.py b/src/reactpy_django/router/converters.py index 483dbcbb..0b54efbc 100644 --- a/src/reactpy_django/router/converters.py +++ b/src/reactpy_django/router/converters.py @@ -2,7 +2,6 @@ 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() + 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 4568786c..30bb3f46 100644 --- a/src/reactpy_django/router/resolvers.py +++ b/src/reactpy_django/router/resolvers.py @@ -1,10 +1,14 @@ from __future__ import annotations +from typing import TYPE_CHECKING + from reactpy_router.resolvers import StarletteResolver -from reactpy_router.types import ConversionInfo, Route from reactpy_django.router.converters import CONVERTERS +if TYPE_CHECKING: + from reactpy_router.types import ConversionInfo, Route + class DjangoResolver(StarletteResolver): """A simple route resolver that uses regex to match paths""" diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py index 1f419049..70b7fa5e 100644 --- a/src/reactpy_django/templatetags/reactpy.py +++ b/src/reactpy_django/templatetags/reactpy.py @@ -1,12 +1,11 @@ from __future__ import annotations from logging import getLogger +from typing import TYPE_CHECKING from uuid import uuid4 from django import template -from django.http import HttpRequest from django.urls import NoReverseMatch, reverse -from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict from reactpy_django import config as reactpy_config from reactpy_django.exceptions import ( @@ -14,7 +13,7 @@ ComponentDoesNotExistError, ComponentParamError, InvalidHostError, - OfflineComponentMissing, + OfflineComponentMissingError, ) from reactpy_django.utils import ( PYSCRIPT_LAYOUT_HANDLER, @@ -28,6 +27,10 @@ vdom_or_component_to_string, ) +if TYPE_CHECKING: + from django.http import HttpRequest + from reactpy.core.types import ComponentConstructor, ComponentType, VdomDict + try: RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/") except NoReverseMatch: @@ -78,14 +81,9 @@ def component( request: HttpRequest | None = context.get("request") perceived_host = (request.get_host() if request else "").strip("/") - host = ( - host - or ( - next(reactpy_config.REACTPY_DEFAULT_HOSTS) - if reactpy_config.REACTPY_DEFAULT_HOSTS - else "" - ) - ).strip("/") + host = (host or (next(reactpy_config.REACTPY_DEFAULT_HOSTS) if reactpy_config.REACTPY_DEFAULT_HOSTS else "")).strip( + "/" + ) is_local = not host or host.startswith(perceived_host) uuid = str(uuid4()) class_ = kwargs.pop("class", "") @@ -114,7 +112,10 @@ def component( try: validate_component_args(user_component, *args, **kwargs) except ComponentParamError as e: - _logger.error(str(e)) + _logger.exception( + "The parameters you provided for component '%s' was incorrect.", + dotted_path, + ) return failure_context(dotted_path, e) # Store args & kwargs in the database (fetched by our websocket later) @@ -123,7 +124,7 @@ def component( save_component_params(args, kwargs, uuid) except Exception as e: _logger.exception( - "An unknown error has occurred while saving component params for '%s'.", + "An unknown error has occurred while saving component parameters for '%s'.", dotted_path, ) return failure_context(dotted_path, e) @@ -145,9 +146,7 @@ def component( ) _logger.error(msg) return failure_context(dotted_path, ComponentCarrierError(msg)) - _prerender_html = prerender_component( - user_component, args, kwargs, uuid, request - ) + _prerender_html = prerender_component(user_component, args, kwargs, uuid, request) # Fetch the offline component's HTML, if requested if offline: @@ -155,7 +154,7 @@ def component( if not offline_component: msg = f"Cannot render offline component '{offline}'. It is not registered as a component." _logger.error(msg) - return failure_context(dotted_path, OfflineComponentMissing(msg)) + return failure_context(dotted_path, OfflineComponentMissingError(msg)) if not request: msg = ( "Cannot render an offline component without a HTTP request. Are you missing the " @@ -201,9 +200,8 @@ def pyscript_component( root: The name of the root component function. """ if not file_paths: - raise ValueError( - "At least one file path must be provided to the 'pyscript_component' tag." - ) + msg = "At least one file path must be provided to the 'pyscript_component' tag." + raise ValueError(msg) uuid = uuid4().hex request: HttpRequest | None = context.get("request") diff --git a/src/reactpy_django/types.py b/src/reactpy_django/types.py index 91ffc319..75aa1d64 100644 --- a/src/reactpy_django/types.py +++ b/src/reactpy_django/types.py @@ -6,10 +6,8 @@ Any, Callable, Generic, - MutableMapping, NamedTuple, Protocol, - Sequence, TypeVar, Union, ) @@ -19,6 +17,8 @@ from typing_extensions import ParamSpec if TYPE_CHECKING: + from collections.abc import MutableMapping, Sequence + from reactpy_django.websocket.consumer import ReactpyAsyncWebsocketConsumer diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py index b86cabdc..6d8b150d 100644 --- a/src/reactpy_django/utils.py +++ b/src/reactpy_django/utils.py @@ -8,11 +8,12 @@ import re import textwrap from asyncio import iscoroutinefunction +from concurrent.futures import ThreadPoolExecutor from copy import deepcopy from fnmatch import fnmatch from importlib import import_module from pathlib import Path -from typing import Any, Callable, Mapping, Sequence +from typing import TYPE_CHECKING, Any, Callable from uuid import UUID, uuid4 import dill @@ -28,12 +29,10 @@ from django.template import engines from django.templatetags.static import static from django.utils.encoding import smart_str -from django.views import View from reactpy import vdom_to_html from reactpy.backend.hooks import ConnectionContext from reactpy.backend.types import Connection, Location from reactpy.core.layout import Layout -from reactpy.types import ComponentConstructor from reactpy_django.exceptions import ( ComponentDoesNotExistError, @@ -42,12 +41,16 @@ ViewDoesNotExistError, ) +if TYPE_CHECKING: + from collections.abc import Mapping, Sequence + + from django.views import View + from reactpy.types import ComponentConstructor + _logger = logging.getLogger(__name__) _TAG_PATTERN = r"(?Pcomponent)" _PATH_PATTERN = r"""(?P"[^"'\s]+"|'[^"'\s]+')""" -_OFFLINE_KWARG_PATTERN = ( - rf"""(\s*offline\s*=\s*{_PATH_PATTERN.replace(r"", r"")})""" -) +_OFFLINE_KWARG_PATTERN = rf"""(\s*offline\s*=\s*{_PATH_PATTERN.replace(r"", r"")})""" _GENERIC_KWARG_PATTERN = r"""(\s*.*?)""" COMMENT_REGEX = re.compile(r"") COMPONENT_REGEX = re.compile( @@ -58,13 +61,10 @@ + rf"({_OFFLINE_KWARG_PATTERN}|{_GENERIC_KWARG_PATTERN})*?" + r"\s*%}" ) -PYSCRIPT_COMPONENT_TEMPLATE = ( - Path(__file__).parent / "pyscript" / "component_template.py" -).read_text(encoding="utf-8") -PYSCRIPT_LAYOUT_HANDLER = ( - Path(__file__).parent / "pyscript" / "layout_handler.py" -).read_text(encoding="utf-8") +PYSCRIPT_COMPONENT_TEMPLATE = (Path(__file__).parent / "pyscript" / "component_template.py").read_text(encoding="utf-8") +PYSCRIPT_LAYOUT_HANDLER = (Path(__file__).parent / "pyscript" / "layout_handler.py").read_text(encoding="utf-8") PYSCRIPT_DEFAULT_CONFIG: dict[str, Any] = {} +FILE_ASYNC_ITERATOR_THREAD = ThreadPoolExecutor(max_workers=1, thread_name_prefix="ReactPy-Django-FileAsyncIterator") async def render_view( @@ -76,7 +76,7 @@ async def render_view( """Ingests a Django view (class or function) and returns an HTTP response object.""" # Convert class-based view to function-based view if getattr(view, "as_view", None): - view = view.as_view() # type: ignore[union-attr] + view = view.as_view() # Async function view if iscoroutinefunction(view): @@ -105,16 +105,13 @@ def register_component(component: ComponentConstructor | str): REACTPY_REGISTERED_COMPONENTS, ) - dotted_path = ( - component if isinstance(component, str) else generate_obj_name(component) - ) + dotted_path = component if isinstance(component, str) else generate_obj_name(component) try: REACTPY_REGISTERED_COMPONENTS[dotted_path] = import_dotted_path(dotted_path) except AttributeError as e: REACTPY_FAILED_COMPONENTS.add(dotted_path) - raise ComponentDoesNotExistError( - f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}." - ) from e + msg = f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}." + raise ComponentDoesNotExistError(msg) from e def register_iframe(view: Callable | View | str): @@ -131,9 +128,8 @@ def register_iframe(view: Callable | View | str): try: REACTPY_REGISTERED_IFRAME_VIEWS[dotted_path] = import_dotted_path(dotted_path) except AttributeError as e: - raise ViewDoesNotExistError( - f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}." - ) from e + msg = f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}." + raise ViewDoesNotExistError(msg) from e def import_dotted_path(dotted_path: str) -> Callable: @@ -143,9 +139,8 @@ def import_dotted_path(dotted_path: str) -> Callable: try: module = import_module(module_name) except ImportError as error: - raise RuntimeError( - f"Failed to import {module_name!r} while loading {component_name!r}" - ) from error + msg = f"Failed to import {module_name!r} while loading {component_name!r}" + raise RuntimeError(msg) from error return getattr(module, component_name) @@ -171,9 +166,7 @@ def get_loaders(self): template_source_loaders = [] for e in engines.all(): if hasattr(e, "engine"): - template_source_loaders.extend( - e.engine.get_template_loaders(e.engine.loaders) - ) + template_source_loaders.extend(e.engine.get_template_loaders(e.engine.loaders)) loaders = [] for loader in template_source_loaders: if hasattr(loader, "loaders"): @@ -203,8 +196,7 @@ def get_templates(self, paths: set[str]) -> set[str]: templates.update( os.path.join(root, name) for name in files - if not name.startswith(".") - and any(fnmatch(name, f"*{glob}") for glob in extensions) + if not name.startswith(".") and any(fnmatch(name, f"*{glob}") for glob in extensions) ) return templates @@ -213,21 +205,16 @@ def get_components(self, templates: set[str]) -> set[str]: """Obtains a set of all ReactPy components by parsing HTML templates.""" components: set[str] = set() for template in templates: - with contextlib.suppress(Exception): - with open(template, "r", encoding="utf-8") as template_file: - clean_template = COMMENT_REGEX.sub("", template_file.read()) - regex_iterable = COMPONENT_REGEX.finditer(clean_template) - new_components: list[str] = [] - for match in regex_iterable: - new_components.append( - match.group("path").replace('"', "").replace("'", "") - ) - offline_path = match.group("offline_path") - if offline_path: - new_components.append( - offline_path.replace('"', "").replace("'", "") - ) - components.update(new_components) + with contextlib.suppress(Exception), open(template, encoding="utf-8") as template_file: + clean_template = COMMENT_REGEX.sub("", template_file.read()) + regex_iterable = COMPONENT_REGEX.finditer(clean_template) + new_components: list[str] = [] + for match in regex_iterable: + new_components.append(match.group("path").replace('"', "").replace("'", "")) + offline_path = match.group("offline_path") + if offline_path: + new_components.append(offline_path.replace('"', "").replace("'", "")) + components.update(new_components) if not components: _logger.warning( "\033[93m" @@ -312,7 +299,7 @@ def django_query_postprocessor( # Force the query to execute getattr(data, field.name, None) - if many_to_one and type(field) == ManyToOneRel: # noqa: E721 + if many_to_one and type(field) == ManyToOneRel: prefetch_fields.append(field.related_name or f"{field.name}_set") elif many_to_many and isinstance(field, ManyToManyField): @@ -329,13 +316,14 @@ def django_query_postprocessor( # Unrecognized type else: - raise TypeError( + msg = ( f"Django query postprocessor expected a Model or QuerySet, got {data!r}.\n" "One of the following may have occurred:\n" " - You are using a non-Django ORM.\n" " - You are attempting to use `use_query` to fetch non-ORM data.\n\n" "If these situations apply, you may want to disable the postprocessor." ) + raise TypeError(msg) return data @@ -352,9 +340,8 @@ def validate_component_args(func, *args, **kwargs): signature.bind(*args, **kwargs) except TypeError as e: name = generate_obj_name(func) - raise ComponentParamError( - f"Invalid args for '{name}'. {str(e).capitalize()}." - ) from e + msg = f"Invalid args for '{name}'. {str(e).capitalize()}." + raise ComponentParamError(msg) from e def create_cache_key(*args): @@ -362,7 +349,8 @@ def create_cache_key(*args): all *args separated by `:`.""" if not args: - raise ValueError("At least one argument is required to create a cache key.") + msg = "At least one argument is required to create a cache key." + raise ValueError(msg) return f"reactpy_django:{':'.join(str(arg) for arg in args)}" @@ -388,7 +376,7 @@ def get_pk(model): return getattr(model, model._meta.pk.name) -def strtobool(val): +def strtobool(val: str) -> bool: """Convert a string representation of truth to true (1) or false (0). True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values @@ -396,12 +384,12 @@ def strtobool(val): 'val' is anything else. """ val = val.lower() - if val in ("y", "yes", "t", "true", "on", "1"): - return 1 - elif val in ("n", "no", "f", "false", "off", "0"): - return 0 - else: - raise ValueError(f"invalid truth value {val}") + if val in {"y", "yes", "t", "true", "on", "1"}: + return True + if val in {"n", "no", "f", "false", "off", "0"}: + return False + msg = f"invalid truth value {val}" + raise ValueError(msg) def prerender_component( @@ -421,9 +409,7 @@ def prerender_component( user_component(*args, **kwargs), value=Connection( scope=scope, - location=Location( - pathname=request.path, search=f"?{search}" if search else "" - ), + location=Location(pathname=request.path, search=f"?{search}" if search else ""), carrier=request, ), ) @@ -439,7 +425,7 @@ def vdom_or_component_to_string( """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 vdom_to_html(vdom_or_component) if hasattr(vdom_or_component, "render"): if not request: @@ -452,10 +438,8 @@ def vdom_or_component_to_string( if isinstance(vdom_or_component, str): return vdom_or_component - raise ValueError( - f"Invalid type for vdom_or_component: {type(vdom_or_component)}. " - "Expected a VdomDict, component, or string." - ) + msg = f"Invalid type for vdom_or_component: {type(vdom_or_component)}. Expected a VdomDict, component, or string." + raise ValueError(msg) def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str): @@ -474,9 +458,7 @@ def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str): # Try to get user code from cache cache_key = create_cache_key("pyscript", file_path) last_modified_time = os.stat(file_path).st_mtime - file_contents: str = caches[REACTPY_CACHE].get( - cache_key, version=int(last_modified_time) - ) + file_contents: str = caches[REACTPY_CACHE].get(cache_key, version=int(last_modified_time)) if file_contents: all_file_contents.append(file_contents) @@ -484,9 +466,7 @@ def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str): else: file_contents = Path(file_path).read_text(encoding="utf-8").strip() all_file_contents.append(file_contents) - caches[REACTPY_CACHE].set( - cache_key, file_contents, version=int(last_modified_time) - ) + caches[REACTPY_CACHE].set(cache_key, file_contents, version=int(last_modified_time)) # Prepare the PyScript code block user_code = "\n".join(all_file_contents) # Combine all user code @@ -497,26 +477,18 @@ def render_pyscript_template(file_paths: Sequence[str], uuid: str, root: str): return executor.replace(" def root(): ...", user_code) -def extend_pyscript_config( - extra_py: Sequence, extra_js: dict | str, config: dict | str -) -> str: +def extend_pyscript_config(extra_py: Sequence, extra_js: dict | str, config: dict | str) -> str: """Extends ReactPy's default PyScript config with user provided values.""" # Lazily set up the initial config in to wait for Django's static file system if not PYSCRIPT_DEFAULT_CONFIG: - PYSCRIPT_DEFAULT_CONFIG.update( - { - "packages": [ - f"reactpy=={reactpy.__version__}", - f"jsonpointer=={jsonpointer.__version__}", - "ssl", - ], - "js_modules": { - "main": { - static("reactpy_django/morphdom/morphdom-esm.js"): "morphdom" - } - }, - } - ) + PYSCRIPT_DEFAULT_CONFIG.update({ + "packages": [ + f"reactpy=={reactpy.__version__}", + f"jsonpointer=={jsonpointer.__version__}", + "ssl", + ], + "js_modules": {"main": {static("reactpy_django/morphdom/morphdom-esm.js"): "morphdom"}}, + }) # Extend the Python dependency list pyscript_config = deepcopy(PYSCRIPT_DEFAULT_CONFIG) @@ -553,8 +525,27 @@ def validate_host(host: str) -> None: """Validates the host string to ensure it does not contain a protocol.""" if "://" in host: protocol = host.split("://")[0] - msg = ( - f"Invalid host provided to component. Contains a protocol '{protocol}://'." - ) + msg = f"Invalid host provided to component. Contains a protocol '{protocol}://'." _logger.error(msg) raise InvalidHostError(msg) + + +class FileAsyncIterator: + """Async iterator that yields chunks of data from the provided async file.""" + + def __init__(self, file_path: str): + self.file_path = file_path + + async def __aiter__(self): + file_opened = False + try: + file_handle = FILE_ASYNC_ITERATOR_THREAD.submit(open, self.file_path, "rb").result() + file_opened = True + while True: + chunk = FILE_ASYNC_ITERATOR_THREAD.submit(file_handle.read, 8192).result() + if not chunk: + break + yield chunk + finally: + if file_opened: + file_handle.close() diff --git a/src/reactpy_django/websocket/consumer.py b/src/reactpy_django/websocket/consumer.py index 345f399e..d877679b 100644 --- a/src/reactpy_django/websocket/consumer.py +++ b/src/reactpy_django/websocket/consumer.py @@ -6,13 +6,12 @@ import contextlib import logging import traceback -from concurrent.futures import Future from datetime import timedelta from threading import Thread -from typing import TYPE_CHECKING, Any, MutableMapping, Sequence +from typing import TYPE_CHECKING, Any from urllib.parse import parse_qs -import dill as pickle +import dill import orjson from channels.auth import login from channels.db import database_sync_to_async @@ -24,12 +23,15 @@ from reactpy.core.serve import serve_layout from reactpy_django.clean import clean -from reactpy_django.types import ComponentParams if TYPE_CHECKING: + from collections.abc import MutableMapping, Sequence + from concurrent.futures import Future + from django.contrib.auth.models import AbstractUser from reactpy_django import models + from reactpy_django.types import ComponentParams _logger = logging.getLogger(__name__) BACKHAUL_LOOP = asyncio.new_event_loop() @@ -41,9 +43,7 @@ def start_backhaul_loop(): BACKHAUL_LOOP.run_forever() -BACKHAUL_THREAD = Thread( - target=start_backhaul_loop, daemon=True, name="ReactPyBackhaul" -) +BACKHAUL_THREAD = Thread(target=start_backhaul_loop, daemon=True, name="ReactPyBackhaul") class ReactpyAsyncWebsocketConsumer(AsyncJsonWebsocketConsumer): @@ -57,7 +57,7 @@ def __init__(self, *args, **kwargs): self.threaded: bool self.recv_queue: asyncio.Queue self.dotted_path: str - self.component_session: "models.ComponentSession" | None = None + self.component_session: models.ComponentSession | None = None async def connect(self) -> None: """The browser has connected.""" @@ -77,29 +77,23 @@ async def connect(self) -> None: except Exception: await asyncio.to_thread( _logger.error, - "ReactPy websocket authentication has failed!\n" - f"{traceback.format_exc()}", + f"ReactPy websocket authentication has failed!\n{traceback.format_exc()}", ) try: await database_sync_to_async(self.scope["session"].save)() except Exception: await asyncio.to_thread( _logger.error, - "ReactPy has failed to save scope['session']!\n" - f"{traceback.format_exc()}", + f"ReactPy has failed to save scope['session']!\n{traceback.format_exc()}", ) # Start the component dispatcher self.threaded = REACTPY_BACKHAUL_THREAD if self.threaded: if not BACKHAUL_THREAD.is_alive(): - await asyncio.to_thread( - _logger.debug, "Starting ReactPy backhaul thread." - ) + await asyncio.to_thread(_logger.debug, "Starting ReactPy backhaul thread.") BACKHAUL_THREAD.start() - self.dispatcher = asyncio.run_coroutine_threadsafe( - self.run_dispatcher(), BACKHAUL_LOOP - ) + self.dispatcher = asyncio.run_coroutine_threadsafe(self.run_dispatcher(), BACKHAUL_LOOP) else: self.dispatcher = asyncio.create_task(self.run_dispatcher()) @@ -116,8 +110,7 @@ async def disconnect(self, code: int) -> None: except Exception: await asyncio.to_thread( _logger.error, - "ReactPy has failed to save component session!\n" - f"{traceback.format_exc()}", + f"ReactPy has failed to save component session!\n{traceback.format_exc()}", ) # Queue a cleanup, if needed @@ -127,7 +120,7 @@ async def disconnect(self, code: int) -> None: except Exception: await asyncio.to_thread( _logger.error, - "ReactPy cleaning failed!\n" f"{traceback.format_exc()}", + f"ReactPy cleaning failed!\n{traceback.format_exc()}", ) await super().disconnect(code) @@ -135,9 +128,7 @@ async def disconnect(self, code: int) -> None: async def receive_json(self, content: Any, **_) -> None: """Receive a message from the browser. Typically, messages are event signals.""" if self.threaded: - asyncio.run_coroutine_threadsafe( - self.recv_queue.put(content), BACKHAUL_LOOP - ) + asyncio.run_coroutine_threadsafe(self.recv_queue.put(content), BACKHAUL_LOOP) else: await self.recv_queue.put(content) @@ -192,14 +183,12 @@ async def run_dispatcher(self): uuid=uuid, last_accessed__gt=now - timedelta(seconds=REACTPY_SESSION_MAX_AGE), ) - params: ComponentParams = pickle.loads(self.component_session.params) + params: ComponentParams = dill.loads(self.component_session.params) component_session_args = params.args component_session_kwargs = params.kwargs # Generate the initial component instance - root_component = root_component_constructor( - *component_session_args, **component_session_kwargs - ) + root_component = root_component_constructor(*component_session_args, **component_session_kwargs) except models.ComponentSession.DoesNotExist: await asyncio.to_thread( _logger.warning, diff --git a/src/reactpy_django/websocket/paths.py b/src/reactpy_django/websocket/paths.py index 435f9b71..258c58f2 100644 --- a/src/reactpy_django/websocket/paths.py +++ b/src/reactpy_django/websocket/paths.py @@ -1,8 +1,7 @@ from django.urls import path from reactpy_django.config import REACTPY_URL_PREFIX - -from .consumer import ReactpyAsyncWebsocketConsumer +from reactpy_django.websocket.consumer import ReactpyAsyncWebsocketConsumer REACTPY_WEBSOCKET_ROUTE = path( f"{REACTPY_URL_PREFIX}////", diff --git a/tests/manage.py b/tests/manage.py index 700db7bd..0f1d3262 100644 --- a/tests/manage.py +++ b/tests/manage.py @@ -1,5 +1,6 @@ -#!/usr/bin/env python +# ruff: noqa: INP001 """Django's command-line utility for administrative tasks.""" + import os import sys @@ -11,11 +12,12 @@ def main(): try: from django.core.management import execute_from_command_line except ImportError as exc: - raise ImportError( + msg = ( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) from exc + ) + raise ImportError(msg) from exc execute_from_command_line(sys.argv) diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py index 27d5e41d..06cebfe6 100644 --- a/tests/test_app/__init__.py +++ b/tests/test_app/__init__.py @@ -4,13 +4,7 @@ # Make sure the JS is always re-built before running the tests js_dir = Path(__file__).parent.parent.parent / "src" / "js" -static_dir = ( - Path(__file__).parent.parent.parent - / "src" - / "reactpy_django" - / "static" - / "reactpy_django" -) +static_dir = Path(__file__).parent.parent.parent / "src" / "reactpy_django" / "static" / "reactpy_django" assert subprocess.run(["bun", "install"], cwd=str(js_dir), check=True).returncode == 0 assert ( subprocess.run( @@ -38,22 +32,12 @@ def copy_js_files(source_dir: Path, destination: Path) -> None: # Copy PyScript copy_js_files( js_dir / "node_modules" / "@pyscript" / "core" / "dist", - Path(__file__).parent.parent.parent - / "src" - / "reactpy_django" - / "static" - / "reactpy_django" - / "pyscript", + Path(__file__).parent.parent.parent / "src" / "reactpy_django" / "static" / "reactpy_django" / "pyscript", ) # Copy MorphDOM copy_js_files( js_dir / "node_modules" / "morphdom" / "dist", - Path(__file__).parent.parent.parent - / "src" - / "reactpy_django" - / "static" - / "reactpy_django" - / "morphdom", + Path(__file__).parent.parent.parent / "src" / "reactpy_django" / "static" / "reactpy_django" / "morphdom", ) diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py index baf3ebdb..d462bd34 100644 --- a/tests/test_app/admin.py +++ b/tests/test_app/admin.py @@ -1,6 +1,7 @@ +# ruff: noqa: RUF012 from django.contrib import admin -from reactpy_django.models import ComponentSession, Config, UserDataModel +from reactpy_django.models import ComponentSession, Config, UserDataModel from test_app.models import ( AsyncForiegnChild, AsyncRelationalChild, diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py index c49de99d..99397bba 100644 --- a/tests/test_app/asgi.py +++ b/tests/test_app/asgi.py @@ -16,11 +16,10 @@ from channels.auth import AuthMiddlewareStack # noqa: E402 from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402 + from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402 -application = ProtocolTypeRouter( - { - "http": http_asgi_app, - "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), - } -) +application = ProtocolTypeRouter({ + "http": http_asgi_app, + "websocket": AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE])), +}) diff --git a/tests/test_app/channel_layers/components.py b/tests/test_app/channel_layers/components.py index 4f40a248..e6b427c3 100644 --- a/tests/test_app/channel_layers/components.py +++ b/tests/test_app/channel_layers/components.py @@ -1,4 +1,5 @@ from reactpy import component, hooks, html + from reactpy_django.hooks import use_channel_layer @@ -34,7 +35,7 @@ async def submit_event(event): @component -def group_receiver(id: int): +def group_receiver(id_number: int): state, set_state = hooks.use_state("None") async def receiver(message): @@ -43,8 +44,8 @@ async def receiver(message): use_channel_layer(receiver=receiver, group_name="group-messenger") return html.div( - {"id": f"group-receiver-{id}", "data-message": state}, - f"Group Message Receiver #{id}: {state}", + {"id": f"group-receiver-{id_number}", "data-message": state}, + f"Group Message Receiver #{id_number}: {state}", ) diff --git a/tests/test_app/components.py b/tests/test_app/components.py index 4ae0544e..ad13ac30 100644 --- a/tests/test_app/components.py +++ b/tests/test_app/components.py @@ -37,12 +37,10 @@ def button(): html.div( "button:", html.button( - {"id": "counter-inc", "on_click": lambda event: set_count(count + 1)}, + {"id": "counter-inc", "on_click": lambda _: set_count(count + 1)}, "Click me!", ), - html.p( - {"id": "counter-num", "data-count": count}, f"Current count is: {count}" - ), + html.p({"id": "counter-num", "data-count": count}, f"Current count is: {count}"), ) ) @@ -61,12 +59,8 @@ def parameterized_component(x, y): @component def object_in_templatetag(my_object: TestObject): success = bool(my_object and my_object.value) - co_name = inspect.currentframe().f_code.co_name # type: ignore - return html._( - html.div( - {"id": co_name, "data-success": success}, f"{co_name}: ", str(my_object) - ) - ) + co_name = inspect.currentframe().f_code.co_name + return html._(html.div({"id": co_name, "data-success": success}, f"{co_name}: ", str(my_object))) SimpleButtonModule = web.module_from_file( @@ -80,9 +74,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 @@ -95,9 +87,7 @@ def use_connection(): and getattr(ws.carrier, "disconnect", None) and getattr(ws.carrier, "dotted_path", None) ) - return html.div( - {"id": "use-connection", "data-success": success}, f"use_connection: {ws}" - ) + return html.div({"id": "use-connection", "data-success": success}, f"use_connection: {ws}") @component @@ -111,18 +101,14 @@ def use_scope(): def use_location(): location = reactpy_django.hooks.use_location() success = bool(location) - return html.div( - {"id": "use-location", "data-success": success}, f"use_location: {location}" - ) + return html.div({"id": "use-location", "data-success": success}, f"use_location: {location}") @component def use_origin(): origin = reactpy_django.hooks.use_origin() success = bool(origin) - return html.div( - {"id": "use-origin", "data-success": success}, f"use_origin: {origin}" - ) + return html.div({"id": "use-origin", "data-success": success}, f"use_origin: {origin}") @component @@ -158,16 +144,14 @@ def authorized_user(): @reactpy_django.decorators.user_passes_test( lambda user: user.is_active, - fallback=html.div( - {"id": "unauthorized-user-fallback"}, "unauthorized_user: Success" - ), + fallback=html.div({"id": "unauthorized-user-fallback"}, "unauthorized_user: Success"), ) @component def unauthorized_user(): return html.div({"id": "unauthorized-user"}, "unauthorized_user: Fail") -@reactpy_django.decorators.user_passes_test(lambda user: True) +@reactpy_django.decorators.user_passes_test(lambda _: True) def incorrect_user_passes_test_decorator(): return html.div("incorrect_decorator_test: Fail") @@ -190,9 +174,7 @@ def get_relational_parent_query(): def get_foriegn_child_query(): child = ForiegnChild.objects.first() if not child: - child = ForiegnChild.objects.create( - parent=get_relational_parent_query(), text="Foriegn Child" - ) + child = ForiegnChild.objects.create(parent=get_relational_parent_query(), text="Foriegn Child") child.save() return child @@ -215,7 +197,7 @@ def relational_query(): "id": "relational-query", "data-success": bool(mtm) and bool(oto) and bool(mto) and bool(fk), }, - html.p(inspect.currentframe().f_code.co_name), # type: ignore + html.p(inspect.currentframe().f_code.co_name), html.div(f"Relational Parent Many To Many: {mtm}"), html.div(f"Relational Parent One To One: {oto}"), html.div(f"Relational Parent Many to One: {mto}"), @@ -249,9 +231,7 @@ async def async_get_foriegn_child_query(): child = await AsyncForiegnChild.objects.afirst() if not child: parent = await async_get_or_create_relational_parent() - child = await AsyncForiegnChild.objects.acreate( - parent=parent, text="Foriegn Child" - ) + child = await AsyncForiegnChild.objects.acreate(parent=parent, text="Foriegn Child") await child.asave() return child @@ -259,9 +239,7 @@ async def async_get_foriegn_child_query(): @component def async_relational_query(): foriegn_child = reactpy_django.hooks.use_query(async_get_foriegn_child_query) - relational_parent = reactpy_django.hooks.use_query( - async_get_relational_parent_query - ) + relational_parent = reactpy_django.hooks.use_query(async_get_relational_parent_query) if not relational_parent.data or not foriegn_child.data: return @@ -276,7 +254,7 @@ def async_relational_query(): "id": "async-relational-query", "data-success": bool(mtm) and bool(oto) and bool(mto) and bool(fk), }, - html.p(inspect.currentframe().f_code.co_name), # type: ignore + html.p(inspect.currentframe().f_code.co_name), html.div(f"Relational Parent Many To Many: {mtm}"), html.div(f"Relational Parent One To One: {oto}"), html.div(f"Relational Parent Many to One: {mto}"), @@ -294,10 +272,10 @@ def add_todo_mutation(text: str): if existing.done: existing.done = False existing.save() - else: - return False - else: - TodoItem(text=text, done=False).save() + return None + return False + TodoItem(text=text, done=False).save() + return None def toggle_todo_mutation(item: TodoItem): @@ -306,23 +284,19 @@ def toggle_todo_mutation(item: TodoItem): def _render_todo_items(items, toggle_item): - return html.ul( - [ - html.li( - {"id": f"todo-item-{item.text}", "key": item.text}, - item.text, - html.input( - { - "id": f"todo-item-{item.text}-checkbox", - "type": "checkbox", - "checked": item.done, - "on_change": lambda event, i=item: toggle_item(i), - } - ), - ) - for item in items - ] - ) + return html.ul([ + html.li( + {"id": f"todo-item-{item.text}", "key": item.text}, + item.text, + html.input({ + "id": f"todo-item-{item.text}-checkbox", + "type": "checkbox", + "checked": item.done, + "on_change": lambda _, i=item: toggle_item(i), + }), + ) + for item in items + ]) @component @@ -330,9 +304,7 @@ def todo_list(): input_value, set_input_value = hooks.use_state("") items = reactpy_django.hooks.use_query(get_todo_query) toggle_item = reactpy_django.hooks.use_mutation(toggle_todo_mutation) - add_item = reactpy_django.hooks.use_mutation( - add_todo_mutation, refetch=get_todo_query - ) + add_item = reactpy_django.hooks.use_mutation(add_todo_mutation, refetch=get_todo_query) def on_submit(event): if event["key"] == "Enter": @@ -359,21 +331,19 @@ def on_change(event): elif add_item.error: mutation_status = html.h2(f"Error when adding - {add_item.error}") else: - mutation_status = "" # type: ignore + mutation_status = "" return html.div( {"id": "todo-list"}, - html.p(inspect.currentframe().f_code.co_name), # type: ignore + html.p(inspect.currentframe().f_code.co_name), html.label("Add an item:"), - html.input( - { - "type": "text", - "id": "todo-input", - "value": input_value, - "on_key_press": on_submit, - "on_change": on_change, - } - ), + html.input({ + "type": "text", + "id": "todo-input", + "value": input_value, + "on_key_press": on_submit, + "on_change": on_change, + }), mutation_status, rendered_items, ) @@ -389,10 +359,10 @@ async def async_add_todo_mutation(text: str): if existing.done: existing.done = False await existing.asave() - else: - return False - else: - await AsyncTodoItem(text=text, done=False).asave() + return None + return False + await AsyncTodoItem(text=text, done=False).asave() + return None async def async_toggle_todo_mutation(item: AsyncTodoItem): @@ -405,9 +375,7 @@ def async_todo_list(): input_value, set_input_value = hooks.use_state("") items = reactpy_django.hooks.use_query(async_get_todo_query) toggle_item = reactpy_django.hooks.use_mutation(async_toggle_todo_mutation) - add_item = reactpy_django.hooks.use_mutation( - async_add_todo_mutation, refetch=async_get_todo_query - ) + add_item = reactpy_django.hooks.use_mutation(async_add_todo_mutation, refetch=async_get_todo_query) async def on_submit(event): if event["key"] == "Enter": @@ -434,21 +402,19 @@ async def on_change(event): elif add_item.error: mutation_status = html.h2(f"Error when adding - {add_item.error}") else: - mutation_status = "" # type: ignore + mutation_status = "" return html.div( {"id": "async-todo-list"}, - html.p(inspect.currentframe().f_code.co_name), # type: ignore + html.p(inspect.currentframe().f_code.co_name), html.label("Add an item:"), - html.input( - { - "type": "text", - "id": "async-todo-input", - "value": input_value, - "on_key_press": on_submit, - "on_change": on_change, - } - ), + html.input({ + "type": "text", + "id": "async-todo-input", + "value": input_value, + "on_key_press": on_submit, + "on_change": on_change, + }), mutation_status, rendered_items, ) @@ -456,22 +422,14 @@ async def on_change(event): view_to_component_sync_func = view_to_component(views.view_to_component_sync_func) view_to_component_async_func = view_to_component(views.view_to_component_async_func) -view_to_component_sync_class = view_to_component( - views.ViewToComponentSyncClass.as_view() -) -view_to_component_async_class = view_to_component( - views.ViewToComponentAsyncClass.as_view() -) -view_to_component_template_view_class = view_to_component( - views.ViewToComponentTemplateViewClass.as_view() -) +view_to_component_sync_class = view_to_component(views.ViewToComponentSyncClass.as_view()) +view_to_component_async_class = view_to_component(views.ViewToComponentAsyncClass.as_view()) +view_to_component_template_view_class = view_to_component(views.ViewToComponentTemplateViewClass.as_view()) _view_to_iframe_sync_func = view_to_iframe(views.view_to_iframe_sync_func) _view_to_iframe_async_func = view_to_iframe(views.view_to_iframe_async_func) _view_to_iframe_sync_class = view_to_iframe(views.ViewToIframeSyncClass.as_view()) _view_to_iframe_async_class = view_to_iframe(views.ViewToIframeAsyncClass.as_view()) -_view_to_iframe_template_view_class = view_to_iframe( - views.ViewToIframeTemplateViewClass.as_view() -) +_view_to_iframe_template_view_class = view_to_iframe(views.ViewToIframeTemplateViewClass.as_view()) _view_to_iframe_args = view_to_iframe(views.view_to_iframe_args) _view_to_iframe_not_registered = view_to_iframe("view_does_not_exist") view_to_component_script = view_to_component(views.view_to_component_script) @@ -483,7 +441,7 @@ async def on_change(event): @component def view_to_iframe_sync_func(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_sync_func(key="test"), ) @@ -491,7 +449,7 @@ def view_to_iframe_sync_func(): @component def view_to_iframe_async_func(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_async_func(), ) @@ -499,7 +457,7 @@ def view_to_iframe_async_func(): @component def view_to_iframe_sync_class(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_sync_class(), ) @@ -507,7 +465,7 @@ def view_to_iframe_sync_class(): @component def view_to_iframe_async_class(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_async_class(), ) @@ -515,7 +473,7 @@ def view_to_iframe_async_class(): @component def view_to_iframe_template_view_class(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_template_view_class(), ) @@ -523,7 +481,7 @@ def view_to_iframe_template_view_class(): @component def view_to_iframe_args(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_args("Arg1", "Arg2", kwarg1="Kwarg1", kwarg2="Kwarg2"), ) @@ -531,7 +489,7 @@ def view_to_iframe_args(): @component def view_to_iframe_not_registered(): return html.div( - {"id": inspect.currentframe().f_code.co_name}, # type: ignore + {"id": inspect.currentframe().f_code.co_name}, _view_to_iframe_not_registered(), ) @@ -543,12 +501,12 @@ def view_to_component_request(): def on_click(_): post_request = HttpRequest() post_request.method = "POST" - set_request(post_request) # type: ignore + set_request(post_request) return html._( html.button( { - "id": f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore + "id": f"{inspect.currentframe().f_code.co_name}_btn", "on_click": on_click, }, "Click me", @@ -567,7 +525,7 @@ def on_click(_): return html._( html.button( { - "id": f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore + "id": f"{inspect.currentframe().f_code.co_name}_btn", "on_click": on_click, }, "Click me", @@ -586,7 +544,7 @@ def on_click(_): return html._( html.button( { - "id": f"{inspect.currentframe().f_code.co_name}_btn", # type: ignore + "id": f"{inspect.currentframe().f_code.co_name}_btn", "on_click": on_click, }, "Click me", @@ -602,7 +560,7 @@ def custom_host(number=0): return html.div( { - "class_name": f"{inspect.currentframe().f_code.co_name}-{number}", # type: ignore + "class_name": f"{inspect.currentframe().f_code.co_name}-{number}", "data-port": port, }, f"Server Port: {port}", @@ -611,9 +569,7 @@ def custom_host(number=0): @component def broken_postprocessor_query(): - relational_parent = reactpy_django.hooks.use_query( - get_relational_parent_query, postprocessor=None - ) + relational_parent = reactpy_django.hooks.use_query(get_relational_parent_query, postprocessor=None) if not relational_parent.data: return @@ -661,10 +617,7 @@ async def clear_data(event): async def on_submit(event): if event["key"] == "Enter": - user_data_mutation( - (user_data_query.data or {}) - | {event["target"]["value"]: event["target"]["value"]} - ) + user_data_mutation((user_data_query.data or {}) | {event["target"]["value"]: event["target"]["value"]}) return html.div( { @@ -673,9 +626,7 @@ async def on_submit(event): "data-fetch-error": bool(user_data_query.error), "data-mutation-error": bool(user_data_mutation.error), "data-loading": user_data_query.loading or user_data_mutation.loading, - "data-username": ( - "AnonymousUser" if current_user.is_anonymous else current_user.username - ), + "data-username": ("AnonymousUser" if current_user.is_anonymous else current_user.username), }, html.div("use_user_data"), html.button({"className": "login-1", "on_click": login_user1}, "Login 1"), @@ -684,17 +635,9 @@ async def on_submit(event): html.button({"className": "clear", "on_click": clear_data}, "Clear Data"), html.div(f"User: {current_user}"), html.div(f"Data: {user_data_query.data}"), - html.div( - f"Data State: (loading={user_data_query.loading}, error={user_data_query.error})" - ), - html.div( - f"Mutation State: (loading={user_data_mutation.loading}, error={user_data_mutation.error})" - ), - html.div( - html.input( - {"on_key_press": on_submit, "placeholder": "Type here to add data"} - ) - ), + html.div(f"Data State: (loading={user_data_query.loading}, error={user_data_query.error})"), + html.div(f"Mutation State: (loading={user_data_mutation.loading}, error={user_data_mutation.error})"), + html.div(html.input({"on_key_press": on_submit, "placeholder": "Type here to add data"})), ) @@ -730,10 +673,7 @@ async def clear_data(event): async def on_submit(event): if event["key"] == "Enter": - user_data_mutation( - (user_data_query.data or {}) - | {event["target"]["value"]: event["target"]["value"]} - ) + user_data_mutation((user_data_query.data or {}) | {event["target"]["value"]: event["target"]["value"]}) return html.div( { @@ -741,24 +681,14 @@ async def on_submit(event): "data-fetch-error": bool(user_data_query.error), "data-mutation-error": bool(user_data_mutation.error), "data-loading": user_data_query.loading or user_data_mutation.loading, - "data-username": ( - "AnonymousUser" if current_user.is_anonymous else current_user.username - ), + "data-username": ("AnonymousUser" if current_user.is_anonymous else current_user.username), }, html.div("use_user_data_with_default"), html.button({"className": "login-3", "on_click": login_user3}, "Login 3"), html.button({"className": "clear", "on_click": clear_data}, "Clear Data"), html.div(f"User: {current_user}"), html.div(f"Data: {user_data_query.data}"), - html.div( - f"Data State: (loading={user_data_query.loading}, error={user_data_query.error})" - ), - html.div( - f"Mutation State: (loading={user_data_mutation.loading}, error={user_data_mutation.error})" - ), - html.div( - html.input( - {"on_key_press": on_submit, "placeholder": "Type here to add data"} - ) - ), + html.div(f"Data State: (loading={user_data_query.loading}, error={user_data_query.error})"), + html.div(f"Mutation State: (loading={user_data_mutation.loading}, error={user_data_mutation.error})"), + html.div(html.input({"on_key_press": on_submit, "placeholder": "Type here to add data"})), ) diff --git a/tests/test_app/middleware.py b/tests/test_app/middleware.py index 0927a100..ff40c7c6 100644 --- a/tests/test_app/middleware.py +++ b/tests/test_app/middleware.py @@ -13,9 +13,7 @@ def __init__(self, get_response): # One-time configuration and initialization. self.get_response = get_response with contextlib.suppress(Exception): - User.objects.create_superuser( - username="admin", email="admin@example.com", password="password" - ) + User.objects.create_superuser(username="admin", email="admin@example.com", password="password") if iscoroutinefunction(self.get_response): markcoroutinefunction(self) diff --git a/tests/test_app/migrations/0001_initial.py b/tests/test_app/migrations/0001_initial.py index 69498ba5..aa7816b6 100644 --- a/tests/test_app/migrations/0001_initial.py +++ b/tests/test_app/migrations/0001_initial.py @@ -7,7 +7,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [] # type: ignore + dependencies = [] operations = [ migrations.CreateModel( diff --git a/tests/test_app/models.py b/tests/test_app/models.py index 8d421042..8b873dd6 100644 --- a/tests/test_app/models.py +++ b/tests/test_app/models.py @@ -2,35 +2,33 @@ class TodoItem(models.Model): - done = models.BooleanField() # type: ignore - text = models.CharField(max_length=1000, unique=True) # type: ignore + done = models.BooleanField() + text = models.CharField(max_length=1000, unique=True) class AsyncTodoItem(models.Model): - done = models.BooleanField() # type: ignore - text = models.CharField(max_length=1000, unique=True) # type: ignore + done = models.BooleanField() + text = models.CharField(max_length=1000, unique=True) class RelationalChild(models.Model): - text = models.CharField(max_length=1000) # type: ignore + text = models.CharField(max_length=1000) class AsyncRelationalChild(models.Model): - text = models.CharField(max_length=1000) # type: ignore + text = models.CharField(max_length=1000) class RelationalParent(models.Model): - done = models.BooleanField(default=True) # type: ignore - many_to_many = models.ManyToManyField(RelationalChild, related_name="many_to_many") # type: ignore - one_to_one = models.OneToOneField( # type: ignore - RelationalChild, related_name="one_to_one", on_delete=models.SET_NULL, null=True - ) + done = models.BooleanField(default=True) + many_to_many = models.ManyToManyField(RelationalChild, related_name="many_to_many") + one_to_one = models.OneToOneField(RelationalChild, related_name="one_to_one", on_delete=models.SET_NULL, null=True) class AsyncRelationalParent(models.Model): - done = models.BooleanField(default=True) # type: ignore - many_to_many = models.ManyToManyField(AsyncRelationalChild, related_name="many_to_many") # type: ignore - one_to_one = models.OneToOneField( # type: ignore + done = models.BooleanField(default=True) + many_to_many = models.ManyToManyField(AsyncRelationalChild, related_name="many_to_many") + one_to_one = models.OneToOneField( AsyncRelationalChild, related_name="one_to_one", on_delete=models.SET_NULL, @@ -39,10 +37,10 @@ class AsyncRelationalParent(models.Model): class ForiegnChild(models.Model): - text = models.CharField(max_length=1000) # type: ignore - parent = models.ForeignKey(RelationalParent, related_name="many_to_one", on_delete=models.CASCADE) # type: ignore + text = models.CharField(max_length=1000) + parent = models.ForeignKey(RelationalParent, related_name="many_to_one", on_delete=models.CASCADE) class AsyncForiegnChild(models.Model): - text = models.CharField(max_length=1000) # type: ignore - parent = models.ForeignKey(AsyncRelationalParent, related_name="many_to_one", on_delete=models.CASCADE) # type: ignore + text = models.CharField(max_length=1000) + parent = models.ForeignKey(AsyncRelationalParent, related_name="many_to_one", on_delete=models.CASCADE) diff --git a/tests/test_app/offline/components.py b/tests/test_app/offline/components.py index daa7238d..381faa7a 100644 --- a/tests/test_app/offline/components.py +++ b/tests/test_app/offline/components.py @@ -5,8 +5,7 @@ def online(): return html.div( {"id": "online"}, - "This is the ONLINE component. " - "Shut down your webserver and check if the offline component appears.", + "This is the ONLINE component. Shut down your webserver and check if the offline component appears.", ) diff --git a/tests/test_app/performance/components.py b/tests/test_app/performance/components.py index 7dba23bc..54dd2280 100644 --- a/tests/test_app/performance/components.py +++ b/tests/test_app/performance/components.py @@ -1,13 +1,12 @@ -from datetime import datetime - +from django.utils import timezone from reactpy import component, hooks, html @component def renders_per_second(): - start_time, _set_start_time = hooks.use_state(datetime.now()) + start_time, _set_start_time = hooks.use_state(timezone.now()) count, set_count = hooks.use_state(0) - seconds_elapsed = (datetime.now() - start_time).total_seconds() + seconds_elapsed = (timezone.now() - start_time).total_seconds() @hooks.use_effect def run_tests(): @@ -46,9 +45,9 @@ def net_io_time_to_load(): @component def mixed_time_to_load(): - start_time, _set_start_time = hooks.use_state(datetime.now()) + start_time, _set_start_time = hooks.use_state(timezone.now()) count, set_count = hooks.use_state(0) - seconds_elapsed = (datetime.now() - start_time).total_seconds() + seconds_elapsed = (timezone.now() - start_time).total_seconds() @hooks.use_effect def run_tests(): @@ -69,8 +68,8 @@ def run_tests(): @component def event_renders_per_second(): count, set_count = hooks.use_state(0) - start_time, _set_start_time = hooks.use_state(datetime.now()) - seconds_elapsed = (datetime.now() - start_time).total_seconds() + start_time, _set_start_time = hooks.use_state(timezone.now()) + seconds_elapsed = (timezone.now() - start_time).total_seconds() erps = count / (seconds_elapsed or 0.01) async def event_handler(event): @@ -83,14 +82,12 @@ async def event_handler(event): {"class_name": "erps", "data-erps": erps}, f"Event Renders Per Second: {erps}", ), - html.input( - { - "type": "text", - "default_value": "0", - "data-count": str(count), - "on_click": event_handler, - } - ), + html.input({ + "type": "text", + "default_value": "0", + "data-count": str(count), + "on_click": event_handler, + }), ) diff --git a/tests/test_app/performance/urls.py b/tests/test_app/performance/urls.py index 6908222c..74a46bf9 100644 --- a/tests/test_app/performance/urls.py +++ b/tests/test_app/performance/urls.py @@ -9,7 +9,6 @@ time_to_load, ) - urlpatterns = [ path("rps/", renders_per_second), path("rps/", renders_per_second), diff --git a/tests/test_app/prerender/components.py b/tests/test_app/prerender/components.py index 7a2b29b4..bc8f900b 100644 --- a/tests/test_app/prerender/components.py +++ b/tests/test_app/prerender/components.py @@ -14,11 +14,7 @@ def prerender_string(): if scope.get("type") != "http": sleep(SLEEP_TIME) - return ( - "prerender_string: Fully Rendered" - if scope.get("type") == "websocket" - else "prerender_string: Prerendered" - ) + return "prerender_string: Fully Rendered" if scope.get("type") == "websocket" else "prerender_string: Prerendered" @component @@ -52,13 +48,9 @@ def use_user(): success = bool(user) if scope.get("type") == "http": - return html.div( - {"id": "use-user-http", "data-success": success}, f"use_user: {user} (HTTP)" - ) + return html.div({"id": "use-user-http", "data-success": success}, f"use_user: {user} (HTTP)") - return html.div( - {"id": "use-user-ws", "data-success": success}, f"use_user: {user} (WebSocket)" - ) + return html.div({"id": "use-user-ws", "data-success": success}, f"use_user: {user} (WebSocket)") @component diff --git a/tests/test_app/pyscript/components/child.py b/tests/test_app/pyscript/components/child.py index 1f4a7824..0ef5b3fb 100644 --- a/tests/test_app/pyscript/components/child.py +++ b/tests/test_app/pyscript/components/child.py @@ -10,16 +10,14 @@ def root(): html.div( {"className": "grid"}, html.button( - {"className": "plus", "on_click": lambda event: set_value(value + 1)}, + {"className": "plus", "on_click": lambda _: set_value(value + 1)}, "+", ), html.button( - {"className": "minus", "on_click": lambda event: set_value(value - 1)}, + {"className": "minus", "on_click": lambda _: set_value(value - 1)}, "-", ), ), "Current value", - html.pre( - {"style": {"font-style": "bold"}, "data-value": str(value)}, str(value) - ), + html.pre({"style": {"font-style": "bold"}, "data-value": str(value)}, str(value)), ) diff --git a/tests/test_app/pyscript/components/counter.py b/tests/test_app/pyscript/components/counter.py index 31df55a1..b8041057 100644 --- a/tests/test_app/pyscript/components/counter.py +++ b/tests/test_app/pyscript/components/counter.py @@ -9,16 +9,14 @@ def root(): html.div( {"className": "grid"}, html.button( - {"className": "plus", "on_click": lambda event: set_value(value + 1)}, + {"className": "plus", "on_click": lambda _: set_value(value + 1)}, "+", ), html.button( - {"className": "minus", "on_click": lambda event: set_value(value - 1)}, + {"className": "minus", "on_click": lambda _: set_value(value - 1)}, "-", ), ), "Current value", - html.pre( - {"style": {"font-style": "bold"}, "data-value": str(value)}, str(value) - ), + html.pre({"style": {"font-style": "bold"}, "data-value": str(value)}, str(value)), ) diff --git a/tests/test_app/pyscript/components/multifile_parent.py b/tests/test_app/pyscript/components/multifile_parent.py index 48a1b1d8..c54d7719 100644 --- a/tests/test_app/pyscript/components/multifile_parent.py +++ b/tests/test_app/pyscript/components/multifile_parent.py @@ -1,3 +1,4 @@ +# ruff: noqa: TCH004 from typing import TYPE_CHECKING from reactpy import component, html diff --git a/tests/test_app/pyscript/components/server_side.py b/tests/test_app/pyscript/components/server_side.py index fe31d527..682411d5 100644 --- a/tests/test_app/pyscript/components/server_side.py +++ b/tests/test_app/pyscript/components/server_side.py @@ -1,4 +1,5 @@ from reactpy import component, html, use_state + from reactpy_django.components import pyscript_component @@ -18,7 +19,7 @@ def parent_toggle(): return html.div( {"id": "parent-toggle"}, html.button( - {"onClick": lambda x: set_state(not state)}, + {"onClick": lambda _: set_state(not state)}, "Click to show/hide", ), ) @@ -26,7 +27,7 @@ def parent_toggle(): return html.div( {"id": "parent-toggle"}, html.button( - {"onClick": lambda x: set_state(not state)}, + {"onClick": lambda _: set_state(not state)}, "Click to show/hide", ), pyscript_component("./test_app/pyscript/components/child.py"), diff --git a/tests/test_app/settings_multi_db.py b/tests/test_app/settings_multi_db.py index fb390e28..76e20789 100644 --- a/tests/test_app/settings_multi_db.py +++ b/tests/test_app/settings_multi_db.py @@ -12,14 +12,12 @@ SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" # Run in production mode when using a real web server -DEBUG = not any( - sys.argv[0].endswith(webserver_name) - for webserver_name in ["hypercorn", "uvicorn", "daphne"] -) +DEBUG = not any(sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn", "daphne"]) ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ + "servestatic.runserver_nostatic", "daphne", # Overrides `runserver` command with an ASGI server "django.contrib.admin", "django.contrib.auth", @@ -146,6 +144,8 @@ CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} # ReactPy-Django Settings -REACTPY_BACKHAUL_THREAD = any( - sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn"] -) +REACTPY_BACKHAUL_THREAD = any(sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn"]) + +# ServeStatic Settings +SERVESTATIC_USE_FINDERS = True +SERVESTATIC_AUTOREFRESH = True diff --git a/tests/test_app/settings_single_db.py b/tests/test_app/settings_single_db.py index e5f8969a..21112086 100644 --- a/tests/test_app/settings_single_db.py +++ b/tests/test_app/settings_single_db.py @@ -12,14 +12,12 @@ SECRET_KEY = "django-insecure-n!bd1#+7ufw5#9ipayu9k(lyu@za$c2ajbro7es(v8_7w1$=&c" # Run in production mode when using a real web server -DEBUG = not any( - sys.argv[0].endswith(webserver_name) - for webserver_name in ["hypercorn", "uvicorn", "daphne"] -) +DEBUG = not any(sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn", "daphne"]) ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ + "servestatic.runserver_nostatic", "daphne", # Overrides `runserver` command with an ASGI server "django.contrib.admin", "django.contrib.auth", @@ -87,9 +85,7 @@ # Password validation AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator" - }, + {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, @@ -134,6 +130,8 @@ CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} # ReactPy-Django Settings -REACTPY_BACKHAUL_THREAD = any( - sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn"] -) +REACTPY_BACKHAUL_THREAD = any(sys.argv[0].endswith(webserver_name) for webserver_name in ["hypercorn", "uvicorn"]) + +# ServeStatic Settings +SERVESTATIC_USE_FINDERS = True +SERVESTATIC_AUTOREFRESH = True diff --git a/tests/test_app/templates/channel_layers.html b/tests/test_app/templates/channel_layers.html index af3db04b..26361861 100644 --- a/tests/test_app/templates/channel_layers.html +++ b/tests/test_app/templates/channel_layers.html @@ -17,11 +17,11 @@

ReactPy Channel Layers Test Page


{% component "test_app.channel_layers.components.sender" %}
- {% component "test_app.channel_layers.components.group_receiver" id=1 %} + {% component "test_app.channel_layers.components.group_receiver" id_number=1 %}
- {% component "test_app.channel_layers.components.group_receiver" id=2 %} + {% component "test_app.channel_layers.components.group_receiver" id_number=2 %}
- {% component "test_app.channel_layers.components.group_receiver" id=3 %} + {% component "test_app.channel_layers.components.group_receiver" id_number=3 %}
{% component "test_app.channel_layers.components.group_sender" %}
diff --git a/tests/test_app/tests/js/button-from-js-module.js b/tests/test_app/tests/js/button-from-js-module.js index e68b9638..2b49f505 100644 --- a/tests/test_app/tests/js/button-from-js-module.js +++ b/tests/test_app/tests/js/button-from-js-module.js @@ -4,22 +4,22 @@ import htm from "https://unpkg.com/htm?module"; const html = htm.bind(h); export function bind(node, config) { - return { - create: (type, props, children) => h(type, props, ...children), - render: (element) => render(element, node), - unmount: () => render(null, node), - }; + return { + create: (type, props, children) => h(type, props, ...children), + render: (element) => render(element, node), + unmount: () => render(null, node), + }; } export function SimpleButton(props) { - return h( - "button", - { - id: props.id, - onClick(event) { - props.onClick({ data: props.eventResponseData }); - }, - }, - "simple button" - ); + return h( + "button", + { + id: props.id, + onClick(event) { + props.onClick({ data: props.eventResponseData }); + }, + }, + "simple button", + ); } diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py index c4848ccf..81a8eabf 100644 --- a/tests/test_app/tests/test_components.py +++ b/tests/test_app/tests/test_components.py @@ -1,7 +1,9 @@ +# ruff: noqa: RUF012, N802 import os import socket from time import sleep +import pytest from playwright.sync_api import TimeoutError from reactpy_django.models import ComponentSession @@ -13,7 +15,6 @@ class GenericComponentTests(PlaywrightTestCase): - databases = {"default"} @classmethod @@ -51,32 +52,24 @@ def test_use_origin(self): self.page.locator("#use-origin[data-success=true]").wait_for() def test_static_css(self): - self.assertEqual( + assert ( self.page.wait_for_selector("#django-css button").evaluate( "e => window.getComputedStyle(e).getPropertyValue('color')" - ), - "rgb(0, 0, 255)", + ) + == "rgb(0, 0, 255)" ) def test_static_js(self): self.page.locator("#django-js[data-success=true]").wait_for() def test_unauthorized_user(self): - self.assertRaises( - TimeoutError, - self.page.wait_for_selector, - "#unauthorized-user", - timeout=1, - ) + with pytest.raises(TimeoutError): + self.page.wait_for_selector("#unauthorized-user", timeout=1) self.page.wait_for_selector("#unauthorized-user-fallback") def test_authorized_user(self): - self.assertRaises( - TimeoutError, - self.page.wait_for_selector, - "#authorized-user-fallback", - timeout=1, - ) + with pytest.raises(TimeoutError): + self.page.wait_for_selector("#authorized-user-fallback", timeout=1) self.page.wait_for_selector("#authorized-user") def test_relational_query(self): @@ -94,15 +87,9 @@ def test_use_query_and_mutation(self): todo_input.type(f"sample-{i}", delay=CLICK_DELAY) todo_input.press("Enter", delay=CLICK_DELAY) self.page.wait_for_selector(f"#todo-list #todo-item-sample-{i}") - self.page.wait_for_selector( - f"#todo-list #todo-item-sample-{i}-checkbox" - ).click() - self.assertRaises( - TimeoutError, - self.page.wait_for_selector, - f"#todo-list #todo-item-sample-{i}", - timeout=1, - ) + self.page.wait_for_selector(f"#todo-list #todo-item-sample-{i}-checkbox").click() + with pytest.raises(TimeoutError): + self.page.wait_for_selector(f"#todo-list #todo-item-sample-{i}", timeout=1) def test_async_use_query_and_mutation(self): todo_input = self.page.wait_for_selector("#async-todo-input") @@ -113,15 +100,9 @@ def test_async_use_query_and_mutation(self): todo_input.type(f"sample-{i}", delay=CLICK_DELAY) todo_input.press("Enter", delay=CLICK_DELAY) self.page.wait_for_selector(f"#async-todo-list #todo-item-sample-{i}") - self.page.wait_for_selector( - f"#async-todo-list #todo-item-sample-{i}-checkbox" - ).click() - self.assertRaises( - TimeoutError, - self.page.wait_for_selector, - f"#async-todo-list #todo-item-sample-{i}", - timeout=1, - ) + self.page.wait_for_selector(f"#async-todo-list #todo-item-sample-{i}-checkbox").click() + with pytest.raises(TimeoutError): + self.page.wait_for_selector(f"#async-todo-list #todo-item-sample-{i}", timeout=1) def test_view_to_component_sync_func(self): self.page.locator("#view_to_component_sync_func[data-success=true]").wait_for() @@ -136,9 +117,7 @@ def test_view_to_component_async_class(self): self.page.locator("#ViewToComponentAsyncClass[data-success=true]").wait_for() def test_view_to_component_template_view_class(self): - self.page.locator( - "#ViewToComponentTemplateViewClass[data-success=true]" - ).wait_for() + self.page.locator("#ViewToComponentTemplateViewClass[data-success=true]").wait_for() def _click_btn_and_check_success(self, name): self.page.locator(f"#{name}:not([data-success=true])").wait_for() @@ -197,7 +176,7 @@ def test_component_session_exists(self): query = ComponentSession.objects.filter(uuid=session_id) query_exists = query.exists() os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE") - self.assertTrue(query_exists) + assert query_exists def test_component_session_missing(self): """No session should exist for components that don't have args/kwargs.""" @@ -209,7 +188,7 @@ def test_component_session_missing(self): query = ComponentSession.objects.filter(uuid=session_id) query_exists = query.exists() os.environ.pop("DJANGO_ALLOW_ASYNC_UNSAFE") - self.assertFalse(query_exists) + assert not query_exists def test_use_user_data(self): text_input = self.page.wait_for_selector("#use-user-data input") @@ -222,27 +201,27 @@ def test_use_user_data(self): user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=false][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=AnonymousUser]" ) - self.assertIn("Data: None", user_data_div.text_content()) + assert "Data: None" in user_data_div.text_content() # Test first user's data login_1.click() user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=false][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_1]" ) - self.assertIn(r"Data: {}", user_data_div.text_content()) + assert "Data: {}" in user_data_div.text_content() text_input.type("test", delay=CLICK_DELAY) text_input.press("Enter", delay=CLICK_DELAY) user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=true][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_1]" ) - self.assertIn("Data: {'test': 'test'}", user_data_div.text_content()) + assert "Data: {'test': 'test'}" in user_data_div.text_content() # Test second user's data login_2.click() user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=false][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_2]" ) - self.assertIn(r"Data: {}", user_data_div.text_content()) + assert "Data: {}" in user_data_div.text_content() text_input.press("Control+A", delay=CLICK_DELAY) text_input.press("Backspace", delay=CLICK_DELAY) text_input.type("test 2", delay=CLICK_DELAY) @@ -250,21 +229,21 @@ def test_use_user_data(self): user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=true][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_2]" ) - self.assertIn("Data: {'test 2': 'test 2'}", user_data_div.text_content()) + assert "Data: {'test 2': 'test 2'}" in user_data_div.text_content() # Attempt to clear data clear.click() user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=false][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_2]" ) - self.assertIn(r"Data: {}", user_data_div.text_content()) + assert "Data: {}" in user_data_div.text_content() # Attempt to logout logout.click() user_data_div = self.page.wait_for_selector( "#use-user-data[data-success=false][data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=AnonymousUser]" ) - self.assertIn(r"Data: None", user_data_div.text_content()) + assert "Data: None" in user_data_div.text_content() def test_use_user_data_with_default(self): text_input = self.page.wait_for_selector("#use-user-data-with-default input") @@ -275,25 +254,22 @@ def test_use_user_data_with_default(self): user_data_div = self.page.wait_for_selector( "#use-user-data-with-default[data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=AnonymousUser]" ) - self.assertIn("Data: None", user_data_div.text_content()) + assert "Data: None" in user_data_div.text_content() # Test first user's data login_3.click() user_data_div = self.page.wait_for_selector( "#use-user-data-with-default[data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_3]" ) - self.assertIn( - "Data: {'default1': 'value', 'default2': 'value2', 'default3': 'value3'}", - user_data_div.text_content(), - ) + assert "Data: {'default1': 'value', 'default2': 'value2', 'default3': 'value3'}" in user_data_div.text_content() text_input.type("test", delay=CLICK_DELAY) text_input.press("Enter", delay=CLICK_DELAY) user_data_div = self.page.wait_for_selector( "#use-user-data-with-default[data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_3]" ) - self.assertIn( - "Data: {'default1': 'value', 'default2': 'value2', 'default3': 'value3', 'test': 'test'}", - user_data_div.text_content(), + assert ( + "Data: {'default1': 'value', 'default2': 'value2', 'default3': 'value3', 'test': 'test'}" + in user_data_div.text_content() ) # Attempt to clear data @@ -302,14 +278,10 @@ def test_use_user_data_with_default(self): user_data_div = self.page.wait_for_selector( "#use-user-data-with-default[data-fetch-error=false][data-mutation-error=false][data-loading=false][data-username=user_3]" ) - self.assertIn( - "Data: {'default1': 'value', 'default2': 'value2', 'default3': 'value3'}", - user_data_div.text_content(), - ) + assert "Data: {'default1': 'value', 'default2': 'value2', 'default3': 'value3'}" in user_data_div.text_content() class PrerenderTests(PlaywrightTestCase): - @classmethod def setUpClass(cls): super().setUpClass() @@ -331,28 +303,23 @@ def test_prerender(self): component.wait_for() use_root_id_http.wait_for() use_user_http.wait_for() - self.assertEqual(string.all_inner_texts(), ["prerender_string: Prerendered"]) - self.assertEqual(vdom.all_inner_texts(), ["prerender_vdom: Prerendered"]) - self.assertEqual( - component.all_inner_texts(), ["prerender_component: Prerendered"] - ) + assert string.all_inner_texts() == ["prerender_string: Prerendered"] + assert vdom.all_inner_texts() == ["prerender_vdom: Prerendered"] + assert component.all_inner_texts() == ["prerender_component: Prerendered"] root_id_value = use_root_id_http.get_attribute("data-value") - self.assertEqual(len(root_id_value), 36) + assert len(root_id_value) == 36 # Check if the full render occurred sleep(2) - self.assertEqual(string.all_inner_texts(), ["prerender_string: Fully Rendered"]) - self.assertEqual(vdom.all_inner_texts(), ["prerender_vdom: Fully Rendered"]) - self.assertEqual( - component.all_inner_texts(), ["prerender_component: Fully Rendered"] - ) + assert string.all_inner_texts() == ["prerender_string: Fully Rendered"] + assert vdom.all_inner_texts() == ["prerender_vdom: Fully Rendered"] + assert component.all_inner_texts() == ["prerender_component: Fully Rendered"] use_root_id_ws.wait_for() use_user_ws.wait_for() - self.assertEqual(use_root_id_ws.get_attribute("data-value"), root_id_value) + assert use_root_id_ws.get_attribute("data-value") == root_id_value class ErrorTests(PlaywrightTestCase): - @classmethod def setUpClass(cls): super().setUpClass() @@ -361,119 +328,107 @@ def setUpClass(cls): def test_component_does_not_exist_error(self): broken_component = self.page.locator("#component_does_not_exist_error") broken_component.wait_for() - self.assertIn("ComponentDoesNotExistError:", broken_component.text_content()) + assert "ComponentDoesNotExistError:" in broken_component.text_content() def test_component_param_error(self): broken_component = self.page.locator("#component_param_error") broken_component.wait_for() - self.assertIn("ComponentParamError:", broken_component.text_content()) + assert "ComponentParamError:" in broken_component.text_content() def test_invalid_host_error(self): broken_component = self.page.locator("#invalid_host_error") broken_component.wait_for() - self.assertIn("InvalidHostError:", broken_component.text_content()) + assert "InvalidHostError:" in broken_component.text_content() def test_synchronous_only_operation_error(self): broken_component = self.page.locator("#broken_postprocessor_query pre") broken_component.wait_for() - self.assertIn("SynchronousOnlyOperation:", broken_component.text_content()) + assert "SynchronousOnlyOperation:" in broken_component.text_content() def test_view_not_registered_error(self): broken_component = self.page.locator("#view_to_iframe_not_registered pre") broken_component.wait_for() - self.assertIn("ViewNotRegisteredError:", broken_component.text_content()) + assert "ViewNotRegisteredError:" in broken_component.text_content() def test_decorator_param_error(self): broken_component = self.page.locator("#incorrect_user_passes_test_decorator") broken_component.wait_for() - self.assertIn("DecoratorParamError:", broken_component.text_content()) + assert "DecoratorParamError:" in broken_component.text_content() class UrlRouterTests(PlaywrightTestCase): - def test_url_router(self): self.page.goto(f"{self.live_server_url}/router/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/", path.get_attribute("data-path")) + assert "/router/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/", string.text_content()) + assert string.text_content() == "/router/" def test_url_router_subroute(self): self.page.goto(f"{self.live_server_url}/router/subroute/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/subroute/", path.get_attribute("data-path")) + assert "/router/subroute/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("subroute/", string.text_content()) + assert string.text_content() == "subroute/" def test_url_unspecified(self): self.page.goto(f"{self.live_server_url}/router/unspecified/123/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/unspecified/123/", path.get_attribute("data-path")) + assert "/router/unspecified/123/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/unspecified//", string.text_content()) + assert string.text_content() == "/router/unspecified//" def test_url_router_integer(self): self.page.goto(f"{self.live_server_url}/router/integer/123/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/integer/123/", path.get_attribute("data-path")) + assert "/router/integer/123/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/integer//", string.text_content()) + assert string.text_content() == "/router/integer//" def test_url_router_path(self): self.page.goto(f"{self.live_server_url}/router/path/abc/123/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/path/abc/123/", path.get_attribute("data-path")) + assert "/router/path/abc/123/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/path//", string.text_content()) + assert string.text_content() == "/router/path//" def test_url_router_slug(self): self.page.goto(f"{self.live_server_url}/router/slug/abc-123/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/slug/abc-123/", path.get_attribute("data-path")) + assert "/router/slug/abc-123/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/slug//", string.text_content()) + assert string.text_content() == "/router/slug//" def test_url_router_string(self): self.page.goto(f"{self.live_server_url}/router/string/abc/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/string/abc/", path.get_attribute("data-path")) + assert "/router/string/abc/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/string//", string.text_content()) + assert string.text_content() == "/router/string//" def test_url_router_uuid(self): - self.page.goto( - f"{self.live_server_url}/router/uuid/123e4567-e89b-12d3-a456-426614174000/" - ) + self.page.goto(f"{self.live_server_url}/router/uuid/123e4567-e89b-12d3-a456-426614174000/") path = self.page.wait_for_selector("#router-path") - self.assertIn( - "/router/uuid/123e4567-e89b-12d3-a456-426614174000/", - path.get_attribute("data-path"), - ) + assert "/router/uuid/123e4567-e89b-12d3-a456-426614174000/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/uuid//", string.text_content()) + assert string.text_content() == "/router/uuid//" def test_url_router_any(self): - self.page.goto( - f"{self.live_server_url}/router/any/adslkjgklasdjhfah/6789543256/" - ) + self.page.goto(f"{self.live_server_url}/router/any/adslkjgklasdjhfah/6789543256/") path = self.page.wait_for_selector("#router-path") - self.assertIn( - "/router/any/adslkjgklasdjhfah/6789543256/", - path.get_attribute("data-path"), - ) + assert "/router/any/adslkjgklasdjhfah/6789543256/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/any/", string.text_content()) + assert string.text_content() == "/router/any/" def test_url_router_int_and_string(self): self.page.goto(f"{self.live_server_url}/router/two/123/abc/") path = self.page.wait_for_selector("#router-path") - self.assertIn("/router/two/123/abc/", path.get_attribute("data-path")) + assert "/router/two/123/abc/" in path.get_attribute("data-path") string = self.page.query_selector("#router-string") - self.assertEqual("/router/two///", string.text_content()) + assert string.text_content() == "/router/two///" class ChannelLayersTests(PlaywrightTestCase): - @classmethod def setUpClass(cls): super().setUpClass() @@ -484,27 +439,20 @@ def test_channel_layer_components(self): sender.type("test", delay=CLICK_DELAY) sender.press("Enter", delay=CLICK_DELAY) receiver = self.page.wait_for_selector("#receiver[data-message='test']") - self.assertIsNotNone(receiver) + assert receiver is not None sender = self.page.wait_for_selector("#group-sender") sender.type("1234", delay=CLICK_DELAY) sender.press("Enter", delay=CLICK_DELAY) - receiver_1 = self.page.wait_for_selector( - "#group-receiver-1[data-message='1234']" - ) - receiver_2 = self.page.wait_for_selector( - "#group-receiver-2[data-message='1234']" - ) - receiver_3 = self.page.wait_for_selector( - "#group-receiver-3[data-message='1234']" - ) - self.assertIsNotNone(receiver_1) - self.assertIsNotNone(receiver_2) - self.assertIsNotNone(receiver_3) + receiver_1 = self.page.wait_for_selector("#group-receiver-1[data-message='1234']") + receiver_2 = self.page.wait_for_selector("#group-receiver-2[data-message='1234']") + receiver_3 = self.page.wait_for_selector("#group-receiver-3[data-message='1234']") + assert receiver_1 is not None + assert receiver_2 is not None + assert receiver_3 is not None class PyscriptTests(PlaywrightTestCase): - @classmethod def setUpClass(cls): super().setUpClass() @@ -559,7 +507,6 @@ def test_1_javascript_module_execution_within_pyscript(self): class DistributedComputingTests(PlaywrightTestCase): - @classmethod def setUpServer(cls): super().setUpServer() @@ -576,9 +523,7 @@ def tearDownServer(cls): def test_host_roundrobin(self): """Verify if round-robin host selection is working.""" - self.page.goto( - f"{self.live_server_url}/roundrobin/{self._port}/{self._port2}/8" - ) + self.page.goto(f"{self.live_server_url}/roundrobin/{self._port}/{self._port2}/8") elem0 = self.page.locator(".custom_host-0") elem1 = self.page.locator(".custom_host-1") elem2 = self.page.locator(".custom_host-2") @@ -601,18 +546,15 @@ def test_host_roundrobin(self): } # There should only be two ports in the set - self.assertEqual(current_ports, correct_ports) - self.assertEqual(len(current_ports), 2) + assert current_ports == correct_ports + assert len(current_ports) == 2 def test_custom_host(self): """Make sure that the component is rendered by a separate server.""" self.page.goto(f"{self.live_server_url}/port/{self._port2}/") elem = self.page.locator(".custom_host-0") elem.wait_for() - self.assertIn( - f"Server Port: {self._port2}", - elem.text_content(), - ) + assert f"Server Port: {self._port2}" in elem.text_content() def test_custom_host_wrong_port(self): """Make sure that other ports are not rendering components.""" @@ -620,7 +562,7 @@ def test_custom_host_wrong_port(self): tmp_sock.bind((self._server_process.host, 0)) random_port = tmp_sock.getsockname()[1] self.page.goto(f"{self.live_server_url}/port/{random_port}/") - with self.assertRaises(TimeoutError): + with pytest.raises(TimeoutError): self.page.locator(".custom_host").wait_for(timeout=1000) @@ -632,8 +574,8 @@ def setUpClass(cls): def test_offline_components(self): self.page.wait_for_selector("div:not([hidden]) > #online") - self.assertIsNotNone(self.page.query_selector("div[hidden] > #offline")) + assert self.page.query_selector("div[hidden] > #offline") is not None self._server_process.terminate() self._server_process.join() self.page.wait_for_selector("div:not([hidden]) > #offline") - self.assertIsNotNone(self.page.query_selector("div[hidden] > #online")) + assert self.page.query_selector("div[hidden] > #online") is not None diff --git a/tests/test_app/tests/test_database.py b/tests/test_app/tests/test_database.py index 83e34ccb..5d613ad5 100644 --- a/tests/test_app/tests/test_database.py +++ b/tests/test_app/tests/test_database.py @@ -1,8 +1,9 @@ +# ruff: noqa: RUF012 from time import sleep from typing import Any from uuid import uuid4 -import dill as pickle +import dill from django.test import TransactionTestCase from reactpy_django import clean @@ -31,40 +32,36 @@ def test_component_params(self): clean.clean(immediate=True) # Make sure the ComponentParams table is empty - self.assertEqual(ComponentSession.objects.count(), 0) + assert ComponentSession.objects.count() == 0 params_1 = self._save_params_to_db(1) # Check if a component params are in the database - self.assertEqual(ComponentSession.objects.count(), 1) - self.assertEqual( - pickle.loads(ComponentSession.objects.first().params), params_1 # type: ignore - ) + assert ComponentSession.objects.count() == 1 + assert dill.loads(ComponentSession.objects.first().params) == params_1 # Force `params_1` to expire sleep(config.REACTPY_CLEAN_INTERVAL) # Create a new, non-expired component params params_2 = self._save_params_to_db(2) - self.assertEqual(ComponentSession.objects.count(), 2) + assert ComponentSession.objects.count() == 2 # Try to delete the `params_1` via cleaning (it should be expired) # Note: We don't use `immediate` here in order to test timestamping logic clean.clean() # Make sure `params_1` has expired, but `params_2` is still there - self.assertEqual(ComponentSession.objects.count(), 1) - self.assertEqual( - pickle.loads(ComponentSession.objects.first().params), params_2 # type: ignore - ) + assert ComponentSession.objects.count() == 1 + assert dill.loads(ComponentSession.objects.first().params) == params_2 finally: config.REACTPY_CLEAN_INTERVAL = initial_clean_interval config.REACTPY_SESSION_MAX_AGE = initial_session_max_age config.REACTPY_CLEAN_USER_DATA = initial_clean_user_data def _save_params_to_db(self, value: Any) -> ComponentParams: - db = list(self.databases)[0] + db = next(iter(self.databases)) param_data = ComponentParams((value,), {"test_value": value}) - model = ComponentSession(str(uuid4()), params=pickle.dumps(param_data)) + model = ComponentSession(str(uuid4()), params=dill.dumps(param_data)) model.clean_fields() model.clean() model.save(using=db) @@ -100,13 +97,13 @@ def test_user_data_cleanup(self): user_data.save() # Make sure the orphaned user data object is deleted - self.assertEqual(UserDataModel.objects.count(), initial_count + 1) + assert UserDataModel.objects.count() == initial_count + 1 clean.clean_user_data() - self.assertEqual(UserDataModel.objects.count(), initial_count) + assert UserDataModel.objects.count() == initial_count # Check if deleting a user deletes the associated UserData user.delete() - self.assertEqual(UserDataModel.objects.count(), initial_count - 1) + assert UserDataModel.objects.count() == initial_count - 1 # Make sure one user data object remains - self.assertEqual(UserDataModel.objects.count(), 1) + assert UserDataModel.objects.count() == 1 diff --git a/tests/test_app/tests/test_regex.py b/tests/test_app/tests/test_regex.py index 5c3ec95a..6328fee5 100644 --- a/tests/test_app/tests/test_regex.py +++ b/tests/test_app/tests/test_regex.py @@ -8,163 +8,100 @@ class RegexTests(TestCase): def test_component_regex(self): # Real component matches - self.assertRegex(r'{%component "my.component"%}', COMPONENT_REGEX) - self.assertRegex(r'{%component "my.component"%}', COMPONENT_REGEX) - self.assertRegex(r"{%component 'my.component'%}", COMPONENT_REGEX) - self.assertRegex(r'{% component "my.component" %}', COMPONENT_REGEX) - self.assertRegex(r"{% component 'my.component' %}", COMPONENT_REGEX) - self.assertRegex( - r'{% component "my.component" class="my_thing" %}', COMPONENT_REGEX - ) - self.assertRegex( - r'{% component "my.component" class="my_thing" attr="attribute" %}', - COMPONENT_REGEX, - ) - self.assertRegex( - r"""{% - component - "my.component" - class="my_thing" - attr="attribute" - - %}""", # noqa: W291 - COMPONENT_REGEX, - ) - self.assertRegex(r'{% component "my.component" my_object %}', COMPONENT_REGEX) - self.assertRegex( - r'{% component "my.component" class="example-cls" x=123 y=456 %}', - COMPONENT_REGEX, - ) - self.assertRegex( - r'{% component "my.component" class = "example-cls" %}', + assert re.search(COMPONENT_REGEX, '{%component "my.component"%}') + assert re.search(COMPONENT_REGEX, '{%component "my.component"%}') + assert re.search(COMPONENT_REGEX, "{%component 'my.component'%}") + assert re.search(COMPONENT_REGEX, '{% component "my.component" %}') + assert re.search(COMPONENT_REGEX, "{% component 'my.component' %}") + assert re.search(COMPONENT_REGEX, '{% component "my.component" class="my_thing" %}') + assert re.search(COMPONENT_REGEX, '{% component "my.component" class="my_thing" attr="attribute" %}') + assert re.search( COMPONENT_REGEX, + '{%\n component\n "my.component"\n class="my_thing"\n attr="attribute"\n\n %}', ) + assert re.search(COMPONENT_REGEX, '{% component "my.component" my_object %}') + assert re.search(COMPONENT_REGEX, '{% component "my.component" class="example-cls" x=123 y=456 %}') + assert re.search(COMPONENT_REGEX, '{% component "my.component" class = "example-cls" %}') # Fake component matches - self.assertNotRegex(r'{% not_a_real_thing "my.component" %}', COMPONENT_REGEX) - self.assertNotRegex(r"{% component my.component %}", COMPONENT_REGEX) - self.assertNotRegex(r"""{% component 'my.component" %}""", COMPONENT_REGEX) - self.assertNotRegex(r'{ component "my.component" }', COMPONENT_REGEX) - self.assertNotRegex(r'{{ component "my.component" }}', COMPONENT_REGEX) - self.assertNotRegex(r"component", COMPONENT_REGEX) - self.assertNotRegex(r"{%%}", COMPONENT_REGEX) - self.assertNotRegex(r" ", COMPONENT_REGEX) - self.assertNotRegex(r"", COMPONENT_REGEX) - self.assertNotRegex(r'{% component " my.component " %}', COMPONENT_REGEX) - self.assertNotRegex( - r"""{% component "my.component - " %}""", - COMPONENT_REGEX, - ) - self.assertNotRegex(r'{{ component """ }}', COMPONENT_REGEX) - self.assertNotRegex(r'{{ component "" }}', COMPONENT_REGEX) + assert not re.search(COMPONENT_REGEX, '{% not_a_real_thing "my.component" %}') + assert not re.search(COMPONENT_REGEX, "{% component my.component %}") + assert not re.search(COMPONENT_REGEX, "{% component 'my.component\" %}") + assert not re.search(COMPONENT_REGEX, '{ component "my.component" }') + assert not re.search(COMPONENT_REGEX, '{{ component "my.component" }}') + assert not re.search(COMPONENT_REGEX, "component") + assert not re.search(COMPONENT_REGEX, "{%%}") + assert not re.search(COMPONENT_REGEX, " ") + assert not re.search(COMPONENT_REGEX, "") + assert not re.search(COMPONENT_REGEX, '{% component " my.component " %}') + assert not re.search(COMPONENT_REGEX, '{% component "my.component\n " %}') + assert not re.search(COMPONENT_REGEX, '{{ component """ }}') + assert not re.search(COMPONENT_REGEX, '{{ component "" }}') # Make sure back-to-back components are not merged into one match double_component_match = COMPONENT_REGEX.search( r'{% component "my.component" %} {% component "my.component" %}' ) - self.assertTrue(double_component_match[0] == r'{% component "my.component" %}') # type: ignore + assert double_component_match[0] == '{% component "my.component" %}' def test_comment_regex(self): # Real comment matches - self.assertRegex(r"", COMMENT_REGEX) - self.assertRegex( - r"""""", - COMMENT_REGEX, - ) - self.assertRegex( - r"""""", - COMMENT_REGEX, - ) - self.assertRegex( - r"""""", - COMMENT_REGEX, - ) - self.assertRegex( - r"""""", # noqa: W291 - COMMENT_REGEX, + assert re.search(COMMENT_REGEX, "") + assert re.search(COMMENT_REGEX, "") + assert re.search(COMMENT_REGEX, "") + assert re.search(COMMENT_REGEX, "") + assert re.search( + COMMENT_REGEX, "" ) # Fake comment matches - self.assertNotRegex(r"", COMMENT_REGEX) - self.assertNotRegex(r"", COMMENT_REGEX) - self.assertNotRegex(r'{% component "my.component" %}', COMMENT_REGEX) + assert not re.search(COMMENT_REGEX, "") + assert not re.search(COMMENT_REGEX, "") + assert not re.search(COMMENT_REGEX, '{% component "my.component" %}') # Components surrounded by comments - self.assertEqual( - COMMENT_REGEX.sub( - "", r'{% component "my.component" %} ' - ).strip(), - '{% component "my.component" %}', + assert ( + COMMENT_REGEX.sub("", '{% component "my.component" %} ').strip() + == '{% component "my.component" %}' ) - self.assertEqual( - COMMENT_REGEX.sub( - "", r' {% component "my.component" %}' - ).strip(), - '{% component "my.component" %}', + assert ( + COMMENT_REGEX.sub("", ' {% component "my.component" %}').strip() + == '{% component "my.component" %}' ) - self.assertEqual( - COMMENT_REGEX.sub( - "", r' {% component "my.component" %} ' - ).strip(), - '{% component "my.component" %}', + assert ( + COMMENT_REGEX.sub("", ' {% component "my.component" %} ').strip() + == '{% component "my.component" %}' ) - self.assertEqual( + assert ( COMMENT_REGEX.sub( "", - r""" {% component "my.component" %} - - """, - ).strip(), - '{% component "my.component" %}', + ' {% component "my.component" %}\n \n ', + ).strip() + == '{% component "my.component" %}' ) # Components surrounded by comments - self.assertEqual( - COMMENT_REGEX.sub("", r''), - "", - ) - self.assertEqual( + assert COMMENT_REGEX.sub("", '') == "" + assert ( COMMENT_REGEX.sub( "", - r"""""", # noqa: W291 - ), - "", + '', + ) + == "" ) def test_offline_component_regex(self): regex = re.compile(COMPONENT_REGEX) # Check if "offline_path" group is present and equals to "my_offline_path" - search = regex.search( - r'{% component "my.component" offline="my_offline_path" %}' - ) - self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore + search = regex.search(r'{% component "my.component" offline="my_offline_path" %}') + assert search["offline_path"] == '"my_offline_path"' - search = regex.search( - r'{% component "my.component" arg_1="1" offline="my_offline_path" arg_2="2" %}' - ) - self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore + search = regex.search(r'{% component "my.component" arg_1="1" offline="my_offline_path" arg_2="2" %}') + assert search["offline_path"] == '"my_offline_path"' - search = regex.search( - r'{% component "my.component" offline="my_offline_path" arg_2="2" %}' - ) + search = regex.search(r'{% component "my.component" offline="my_offline_path" arg_2="2" %}') - self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore - search = regex.search( - r'{% component "my.component" arg_1="1" offline="my_offline_path" %}' - ) - self.assertTrue(search["offline_path"] == '"my_offline_path"') # type: ignore + assert search["offline_path"] == '"my_offline_path"' + search = regex.search(r'{% component "my.component" arg_1="1" offline="my_offline_path" %}') + assert search["offline_path"] == '"my_offline_path"' diff --git a/tests/test_app/tests/utils.py b/tests/test_app/tests/utils.py index fe32d97d..64f0f60d 100644 --- a/tests/test_app/tests/utils.py +++ b/tests/test_app/tests/utils.py @@ -1,3 +1,4 @@ +# ruff: noqa: N802, RUF012 import asyncio import os import sys @@ -17,7 +18,6 @@ class PlaywrightTestCase(ChannelsLiveServerTestCase): - from reactpy_django import config databases = {"default"} @@ -27,12 +27,9 @@ def setUpClass(cls): # Repurposed from ChannelsLiveServerTestCase._pre_setup for connection in connections.all(): if cls._is_in_memory_db(cls, connection): - raise ImproperlyConfigured( - "ChannelLiveServerTestCase can not be used with in memory databases" - ) - cls._live_server_modified_settings = modify_settings( - ALLOWED_HOSTS={"append": cls.host} - ) + msg = "ChannelLiveServerTestCase can not be used with in memory databases" + raise ImproperlyConfigured(msg) + cls._live_server_modified_settings = modify_settings(ALLOWED_HOSTS={"append": cls.host}) cls._live_server_modified_settings.enable() cls.get_application = partial( make_application, @@ -69,7 +66,7 @@ def tearDownClass(cls): # Repurposed from ChannelsLiveServerTestCase._post_teardown cls._live_server_modified_settings.disable() # Using set to prevent duplicates - for db_name in {"default", config.REACTPY_DATABASE}: + for db_name in {"default", config.REACTPY_DATABASE}: # noqa: PLC0208 call_command( "flush", verbosity=0, diff --git a/tests/test_app/views.py b/tests/test_app/views.py index 0c75b357..695d0f43 100644 --- a/tests/test_app/views.py +++ b/tests/test_app/views.py @@ -22,19 +22,15 @@ def host_port_template(request: HttpRequest, port: int): return render(request, "host_port.html", {"new_host": host}) -def host_port_roundrobin_template( - request: HttpRequest, port1: int, port2: int, count: int = 1 -): +def host_port_roundrobin_template(request: HttpRequest, port1: int, port2: int, count: int = 1): from reactpy_django import config # Override ReactPy config to use round-robin hosts original = config.REACTPY_DEFAULT_HOSTS - config.REACTPY_DEFAULT_HOSTS = cycle( - [ - f"{request.get_host().split(':')[0]}:{port1}", - f"{request.get_host().split(':')[0]}:{port2}", - ] - ) + config.REACTPY_DEFAULT_HOSTS = cycle([ + f"{request.get_host().split(':')[0]}:{port1}", + f"{request.get_host().split(':')[0]}:{port2}", + ]) html = render( request, "host_port_roundrobin.html", @@ -129,9 +125,7 @@ def get_context_data(self, **kwargs): def view_to_iframe_args(request, arg1, arg2, kwarg1=None, kwarg2=None): - success = ( - arg1 == "Arg1" and arg2 == "Arg2" and kwarg1 == "Kwarg1" and kwarg2 == "Kwarg2" - ) + success = arg1 == "Arg1" and arg2 == "Arg2" and kwarg1 == "Kwarg1" and kwarg2 == "Kwarg2" return render( request, From 714191f3181a8dc730d9ecf7018d3b7b49e24d07 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 2 Dec 2024 02:02:53 -0800 Subject: [PATCH 18/35] v5.1.1 (#263) --- CHANGELOG.md | 7 ++++++- docs/src/assets/img/add-interactivity.png | Bin 20821 -> 22900 bytes src/reactpy_django/__init__.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 399d3668..f8c848e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] +- Nothing (yet)! + +## [5.1.1] - 2024-12-02 + ### Fixed - Fixed regression in v5.1.0 where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. @@ -521,7 +525,8 @@ Don't forget to remove deprecated code on each major release! - Support for IDOM within the Django -[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.1.0...HEAD +[Unreleased]: https://github.com/reactive-python/reactpy-django/compare/5.1.1...HEAD +[5.1.1]: https://github.com/reactive-python/reactpy-django/compare/5.1.0...5.1.1 [5.1.0]: https://github.com/reactive-python/reactpy-django/compare/5.0.0...5.1.0 [5.0.0]: https://github.com/reactive-python/reactpy-django/compare/4.0.0...5.0.0 [4.0.0]: https://github.com/reactive-python/reactpy-django/compare/3.8.1...4.0.0 diff --git a/docs/src/assets/img/add-interactivity.png b/docs/src/assets/img/add-interactivity.png index 009b52ac123e2854259c3b2b3e478a721ed6b110..c32431905252537a5387e25c4ec58a3f90f87f6c 100644 GIT binary patch literal 22900 zcmcG$1yo(jwk^6KAwhz>ySqCixD(tVxVt+^aDoJv00Dx#ySuwXaCdk3b7k+d-#z!= z`|p3RwUcT$&?;W4zeX3#gXH85syv$g|DgFt+OZg%>H zmc~v*2F9l5HvA+fjjbd^=0^M^YV5L1vUbA8X66ze4#r9za>|AtmWJF$B!U7&d~Q5I z1=hw+`b2KlRyK}2Zu}&F>g55h!M7Pni2f9Dvg9ZE0B(>-UG^i9u&skJ5jz71y&*Fj z7ZE2n12YF36DtQD5epMDHzN}lBQq;K6EhDJ2M;?Z(ck|_fYuz0On4MU#Qtsz_=}&! z%*n})hmq0M)s?}OmBH4*l#!X68>oYYk%fgGC_(S&ZsVlyMsMRt`mY*9j2#Uf%DKIy>=`06qQd6|C+4sn*8v?_mN4jL}Wsj**#x3H(Zb3K|*yQ_jxW!Rk-r zMuv>WR>s!GHcpN}S>}Jr+L_rp**cop{x?_2V%mwmCe{-+5?Cs7yR zHU1uu|FX2Bvb&uzqoT2+t+Runv8W5sO;YgN*zpKE80$OPIw;%PTK#LHKK`rAL@dBt z6H%#|+ZfrpI#U0K2aHAZos9WOSeRJY=$TpQnOKy8S!3nqVdbD_;^JXq`m3m{t&zEj z`+rrGjfsbe``?NJCS#=Ur2jt_HZtTfv30Q42VU6RTHn-|(ay${gy^4PGC1d-)udK|8{+J7oz9D!S@RJyV z=ik_f>vUHDZiE!`Jn9f<7nAiSxK#h z^Ylc<+r!qH9Gxj!k%}$Dxs4aHhH9xj0bTwKDp#6#sDMPLS|(&hTz~nqaaqCvla%&Y z<44=+Rnf-KSdCXA>2lLTks2*yK~&X#_-hN%%CN#gV((E4n_bgfigzL%)^s6qV1}Op7+vTSnbOOrsT11{z<50 zq;JQBX2r8VflPovgZ0W74FlY;t& zb!-I1kwtCfCa|-#X_xUeN|hA1v{L^}POy`U<@~Bg0y9Q^hS2N9hQgrGm$5Z)l}hNHH~D#%ASMP$`km zLmHKQ07#z9M#os7=;LM_*6{$gzqct%{1F36TMuPmF6n^Uv!!2i@G(GsUbtn-7ude6b2O7QVpJ<`5BIneCzqt}?h zgCHv9E2Qk;o}b8|MU&)ev#`lxMxFbSMz2yzKl(yU$p%3mPp2aVlgaBYjUEn{p@6ak zUOMD~EW>_C+@O|(u{txL56^6`pkaIG5e8MW1$IL-JQeO$>-Xq5+teRS2qNO)HXQEG zn(u4F%5v+Xe(t1T*(Ym^yURrrv)kmB|eVc%R zdET3i!9?tjBzT*YbGAuyA%Sf9mBl~`5QH% zG_W|3I8FTc&qBR|bYlmvT;qT{QE%HvZ;e6V&(>80Z(O1N+(Qje2m0PcX>!{L>3`cR z_}D3U+X$;P^71(P0$lj>eau4#*LVB+?}9&st;Py-Fd796s zIlUAr?E%xBiCjC@%@MNIlNz=6tk=~maed0e%!}}#;w#uRa_`0{B`nb{Wvpd{;wnh~ z)=#S9tWQ`dR!*u^Y$9u{8U7%Si%Ox3|GimUOs%~Ca_W4z-kkY21G|Zijm`1)mb`nT zwou<(cqQX?c|xt&#(sBmqwlGWa}E-RcsqSo%Hc_}ThR3|AN@}r%@zxxZjrbjOE_Jy zVQ+aCJdKXmKcA$fN5`lfPr&QaLzl217^SB8XLbMDV&SUQb6u_xZScAXY{NKQUonN*-zL7Jr!)2ri7YH@hN@m3*Ve&q{8yN)*OxJ z($nTSJ6zs-o>Ul=C_2B~a`}Et9IaJ%&>M!w6D=&yDooH|W_*LMT@~|8!m^q|>$RDZ z9^XhJGgm%)9h>M}|4tD8+tmVLWcmw2VtPC-yOZ_x7bu7s%N-lB5_P0ce+Ibod&z;t z7fxFVb%ti2-;aP@l`=qs{B&JfM~KHl5iG1rxqaHL1eZdWme3!sNyELoqg_?ETKs3e zYIrh!L1M9C1-))gCggW3SX(v(ebMEoznr3nU#VeOt*i)?XMnU7IT?s-aW1bMCU81a zd>%+R|LmFC>pOYDS^M@Qz7D(y`65G^=aQ*>C>a$vKV|Hb7XG#c>vb7kmNr$Lz26-}ppcZyDCYQ|h8 zj`%qmUEOWD_oy+yUZFw62Kio>t~_8f5nP%)@_`Cq@ZMP#9S=j_op|+L zBx=*9x0?a8L;1T0R%cM4TrIi-YG&s+ z?{%^DgPowx?xX(B7xB;I?A1YyC5yh6ciU=?oNs6GT378uHV!yF)R-=|vo?4@=0lDU z&Gh)7Q(!`A$!4GHULIZBlm59p1D-afIi*(8MN*w1S*iyGh z((90(7T@~2=q9vW9HUM^>z_2LUE8<`RawQ!F14n6XZ)%2bW51bJjf-^E z($r>tM!oSBNfiMeM%i+cs*Kn=UDoM?@XIyKgXJ!L#V+$Z#OP7zvPUzArI}3Dcm3=2 z3ep+<%uG|&b#4@l#PrC4!nuLX0x;zcSrNAN*XZn69XP@r#Sa>Z7Zo;&-QrwtldhgU`u*+mrb9}j{8r=-f&wa4D?s08tb^T5JVK_5DW5% z((Juty>BjSe(Zdyx$D@ooGEj^fB?-{%FaTxE&Sf(c=ELD+s~(%*n-hl#e949%Un-Y z6NgsY55)QVyc1tC=lxX_Cj zP=oc$L(2tAE3fA|ItB?~=5!%nARQLPrf$P!_)|J=3$9 zj-W8dx_)=PwpM9wj439)Z$YQ)O_Ukv_F3XJD3{z5&)^Q1f zcr{9NyAqAFAYpE0&! zP7)c%|Fc}Ace$a~s?Op}u@MF>5;En1ji=*fweu=P-!N?SE3ZmM5($>Wvk>H-_rU2< zOn8hL2#z>>Zvoa7q?FW+*6j5*%Ve5cQZR}?xN7W{-F^@O?Hv3%JV~g5u z-b4ujFa4mP`l!Qs*~hhhGB@7V#~EV5^4e1ltuvlBUdmd-Kpqlu2jta8Ky1Pj5(4}6 zD)|BAZP^DauSX^nJo<>|dr&QOt&9iayFO3X^^kM>breR>9|kAp9C^4~_Vm=@TW!58 zKZlv`%r;i~)-NxbJ`*EXTAU|5`u3>v`26x`YbhKkQFieTSw$}K8`v?s$?$7l(hvC< zVJ^8$nkpvwX)1mfsdF9~veJInlen;m$HI1?(Y<(%19fwVfsDL})-(Wk1q zFO+#2RTqo(p?A-N{0^OIH-$SZ$E;3H-*s;15_Y#mZs02n^{NflKao0ho%7gueJ%4> z){4h!D_)%O+cEeBLW=1@l&v_b|M4(QsZ&EG(}x%-(O|`TQFOI{t#)LUR=;6sIR~4- zxtcs&8(>wFwVA5IqchEL;{5>Q&h>OSZ<;Q;^f|837<77-ANG}zu}xq9w9L#7bDe8}ma98-Ba5Ry9)vA$L1FX7RRqpRM>f@YsEs z`4wA_4fC!|poGg)rbTg}GlE?-~Bkvmt zY>E=you9H2XnXiiUK9I&!>%@<95Vk|@rjMws8p?+8g~d+KlB}=LtmF1wP&P%x1M7j zR<%J;mMR_bZ&BiSzF$;b2;#)?NGF-(^Zml19WcaQvpxja*n1~D;MfL{2bchXqSS+u-u73!B^?^6{FqYK+79EHY z2uvb%pk4P z3u@I^%@3z=db>l94;@}}FF~jnYT(q)FKFYKYK3XB?KFCO0%pTxS#XMR6J_Siy0~-( zx~<<94qJ4{hIj5;&FOP3M@yCf$~b8g^$55=p5m}f?|%`2=AEOc3knkI>FM5c%M00n zHD1rZ)^T_e3)Z!>hsjKya~;uDD~(T!sqS{zq4n9IxE13ng(qTI5{37E7OSf;4obtO z(eM>Q1OWlR`$zSK4^7Emyvbo^56J7-o1*l?*n%ndQHD3Wt(}+$6Rqu`#~TnLl)SB< zy-(%xukP$Q^{w%Cd@20xya){HcXz##3=5{Am^te#^9su8;A^B!<<0{_Go|b>XLb>e z*GJw8DOtBj(J!Pmm*}RlebO=x=+*gNZ&_G}baUIch6Gxg{y423Phrgv&P$GJ?WER+ zWzXEd=d?8AUpN%1MVg8~dWB0&^YKwL0E3f)E&GST-9_^3UKtj`%>sp)tdt#NOTusH z98Dc2@sms8c3F7ud$GE0hOSUmN0bm6?t1o|n6M(503Y_lj9K*A0FX_5k|O@{XHG+SI4mE;6R27bX8~b7SdT_K!3cxUi+x z(v8UN`Yk@BOVeYxwq3YyN4W~-o2~e^K*7O_>vf+s@V?G`^&&I0gm=}UtKc;tJ~m&e zf-;#rJO4B@(4X1tpJC zeza0B7dn`(FM`Qb*3V-j`k2FDc!X)ZX2~$Z8OrfWI2H5u4N^+}+If&;cA3++90*Q? zW6R){OyObetl`C@UoDw+JjIgz$+4O5a)Xk;g%om<@Umf@vngpLTQkzOM&v1xEEqBN z&M{1hq;xbxws?^)&5ps@c9kDlM#$C$f&ib!{ii^)b~=K(<{ic_zEc=Ukqm*Bv6_^A zZIJST$`cRlQtFefe zn!&)~UYssJmKj|Q@}me+DGY0mJIO{6owfBZWKfiq`#|z8*-NWOcRycSuNC>2$DZ<0 ztjYd#$RnskYtYmf*F_IY96b{6BOU~3di=a2282l@j!G>~*F|nJ8MKF7Bc})4Rv|^R zK8SU8I_}ki$|_Aajr51wm|+hs_2)|LLhiOIJ>9^jKk8}gs-3AH`T>7=PHO4!BkK8u^7}A-t^Rn&o&yR?| z$jUkxB}pYb-sCz~Thet>Z<*b-uZwvQT6N$nE2b!`(!~yrL=F!^LEo|vDc?BV9&Sba zjqSYxNBY@ZgqgTRrbCt&u{pc3pr$uPKKJRE-*?s@$u!dEv0sUcyZuAAE?$yM->ZO} zwA1`eos&H$G`;?*s~Z(c`id(EPs)#7zeyc25xvCE;-c0FqE!!1li)#I6o!Pam7dvj zh7uv%cvE?e>l39QLC3ntIc28_FtYE<4YsH8@N&?A*7?YQ!&3-4$tso#KoW(#z6jBQ zF)maz0{|RDO8<%UZ}=4jbypxpZCrT{%ZzeQLt2S#W}?NFh?GgebWy&{fyqoW492mR zOh5|&QOzC<3RcO__JXllO3o0|WhH$4mXsH!6oILP1D(0n$N2cLrgWHw9URTRD{gWG(c4 z$9>%PC#ErfU}MO-?!DP5B3zay z7kacnQC)yAa)hMEr}t^D_j%m`={_l|?8fx%@U)`t|8w9z9QJ@J{VuCw`r{S_h6 zuT_324H9fC%^Q!mX*3xYbkbipQ11j$t0-*5oOiebwJ{lzi}d&`quqX!aI)S$d_u?y zCJMQrct8{Ch&{7m20TgZm)D>?YvRW)44PgK4^H6YU*jys>2MtDF=(n4L*mF)M&#gJF1jBre1jlThaNbul?uub_0dxyR|%p&cEO~8lT2Kz9=MvbX<{WC;pMp3Z4iG7fW2HpFm>{D6i zV0N>s2D4gzgYORt8#1LtThwkCO(ks00oiwcWXuWdape^3wVO!=%$(!ey?y7{8Z9GK z@|KRx>M1(!72Bm8^XbyZ*|aucOOSFjQz^DCMzaHnN1@EL!ZQOVgm$F#Ew&i&=xKnd zb{zTHK+X4|P!1<=*wpCmTMjqZS3!I;2UwIKCvA$R87frkTJ(;-CTzX>7MUjtZX)>| z9*w&Z>8C|ez!*Z-j-koH5KU&E(v)lGgeLHt>Ksi7q=diRVCFX{NZ5kXq`BlxE6=F+ z2kxIvGND)Wa9aciH})L}02JP^7y&v~oEgeNocvmHI0%0HL9?~idZi+Z$-YePdoGwG z_!8?S$6K(uxsTMT-x3(ih}Ph&B5u8SfwoSrrt(5nxztJ|hwQmWO8m@p!YL7VHJ#yC zDu@EJ@a_HAKw2`r;8GilFJlJeG)9j>7zl$11Cuhal@#gUr;{L~C+bRO4xO)R5bF&` zBj~;^y0^B|=(S@Whc9HFK~}-{uu}TR#-HXjZF!D&4o^-Q z7jNgki1*$=qOfp#;TqISPy2jD$LREVNFeiEhP{i*hfveOR&?M0!OY~LL4~B* zq-ewZW7}7@kk;wZ1=QA;OX0S$iY?CGm{7l%5EcP&EN$v1Nchd4Lt>m~&**1MglZP` zn-wQDO7`-t55(+(gXFZWbq;PS%ozNs-%iQqU~XD_^cS_>vA0~9P)X07HzI5Ywr6d0AUB6JY%81a_H7mnsW7B12Pj%w=xpI6Zn z1+&FvCrp_ez7M=#Osxn!*`brzVkR&TU|ZL3$o4Jwv1o$jL7Iui7h?+S5osJ0bVv@w zH&CE5Xc#*ma(Bx8&+o(G&f$#OkGfYaZ`0mp~dkBjUO#8%sMj^tbVE1&Ddf*6#WI4$K)! zK`+`dcW)(v(2r6h{lg;q$Bat7$oTU9xLDe^Y+HV%e*d^gkC{@>KRfl_es8fI4knNf z5bM{WwE3dSUs_qi%_FR1g=77!EkN3KzH!QY?k|JyljM{l3m# zuTUA=tkb-bDH^pxcQ7VBu@XGimKCbf5S(zGb6z_Pz{RL{m|On*$2z){1(fn7W#2KS z4LUXG-gxt_M+O7a%9Z@%)&lQ7ZLnqGk7pv~%2^x{0H*BPi3_q7o{I9crs5cYz_5K# z`Sfd98Cn~qC@Ha4H4NzYQ5AmDogckF8ZPxT5W3`y&BuX$K5pUsiEA~qG2B4PYZaF0l_W| z#iAb`sUjAcx=hHDEG~`~MMznV_gPej!!@0h+_v(ae(LJ1n;ZJ>2mNOVd*g z3ixat&{zU9l?zYncz0P;KL1Jfd@dAf3jLjfIzwDIj8Ww@dc!TtbKPUG!7RHndwh0( zIFAM#h(jseL=mG3;t@I@L&X{|rQLf2@9ihS3c7pd7@tpkDx^jgV;<$3`sH^w?Yvnh zCn*DAL9Ndb3@QsAOqM6m#PJ{i|3DLz+jEUUO3q5;vxPK6T)Im!fb7Cd#{tu39A0MW-`eCzZ|!}VUG`goV_y`*mA(v3zq*F4A8j$ zlh5~m;Zbky5|cTnl{wy7g$XA264=9H-$P z_NW5~k{Hp{jiJO}ZZWyKzK36A^xg>HDES*ZIwrgk(+59d3_KFFOsc~DfoW-kXXhjo z?Aj(`3o!VeMHJ!n@06}YHQzX3`91jNdmi{Vz!B| zKH4blP;18OIyvDl=*2X&TWNjT>?Q$x@7r#pDud31=9|Jr^!Xs&mtU^ig7nYuAc+eE zhLnqhhS^HQ<}~lM89{1SQxH>vt>C4{b%3qiP1p+`aFqdkd{ly;acHXNV;Pcte03-c z&tK`2gwko7dN+u79Q<C#nj!@0&1Tv)h!k0=^20+^UBqh;qIIL_N z-u;}pgirg?2(5nk1yAqCYDy(Oyl?%(VH4J&ju2D9xBO3 zBR&RJA4$|>g9P>--pEVwN2oP;UKOH(m~QW|FSrU1+e!7?>#jI0I#7iAzP8PV7=U(} zpF&>rdXEngKz)h)jwb4ITa1o}58o;eZEc_>R~H0Z1r%dk_dSgwx>i((fQW`X2H8NT z;YAc`c8rWfaJk}BL@VtFfV`)DWX-Ibxf~wSvsqX>4@}gGf2iCH3*zsh{A}p*5>JS{ zOHnPEKzPz>wg&z z2TUPxQ1Etr%=Rv*?kE9~jf_BfTltygt!wM;X4nJcXxa}=pNVUGnfrpT6)LOZXpPI0 z0+86FHqxc-t2-xYFE^z<_I*0DbOTqBCjrj0br!%78|~lDR~Qf4{BGJiu|P@z0-bF! zSDy!xMNe&*@NSQX~ru6Y$&V7mcuPSlOsH95rM*Ts_5xGnSQy}zpLUie*K zMDM@CK&1dQ;(zOJD{N(C^hbQaxz5YzwH)`1@b zgd|(&T0Pfz-Dw8RK0~ADUrZ6Ea*wa~6+M9`d;EkBoobAOo;N8127@47T3g9}T}>S4 zNS2@gktKvI|C%G9=(vpW9?A}o*6qSHCZFjS0E;zJ%n1zs5b<^|PrvPM<7yl0UTCk; zFeyeLK~A&O2xfQ90OM+-(?#EBVNMSKjEq+A3@#Sd2jW;juBC^xG)DDLNw7dv@Z%cO z78CP0+FIVe{No|@?+8>v=~{j#SRk&k3>2GO>)qT_= zJaWMOtx)a&U!?$n8lrY9S9}66)4>U{2O`W$-wB+I^?(2r2m|KVX=@It0Kt*cCN-Kn zc_2}Cl$6S$MmXux$lt;utqq> zf=bm%R6}6x1BMlVdQ8JR3EW8RX+K$SbVC0~#PT zVTH2M(5U4D0eNa*2G2^(@eLq`LN)gBf|Tu%dprbw?*iVlR3_THo0nHYycGZgdIMwb z|HPeBsqH^g<4npW0zq|6Hb`Im)>>byOMSFjJP4hIq67H~v#}-NHSCVv&4573HY&B( z9;|Yc2t`QGTF?5z)vV6#`SHzFSeEgI*3T~X?l)g z)9x?0D$WVx%{^r^;X zG%h`zcIsy?n60f-O6@w^ji+^;Mp&lbIDgj|L}L^KR=NJ$NgnOu()(+NZ{uaE>((Bx za*4~q$pIwDWYWclDfR)9(Z!ol>nJEigL^3FD5Ubx+EO3}PsKqI%cq+e!JF|{yC}o+ z6~ZrhhSYB!fO*^7^3)oLc81gwHwwL2A_K;@KNz<}`Z%UtGZUrVM;X;vignCzz9eBR z-vsGZrzF9}`;Ce~9)1h#<>2?AH?mlck2pNZy;LvD3MplAW|y zEhPuHKaoHfbor9stJJ(N&4(7O;pk1J7`<1kg#!`*)r8WOf_dw#6yEZO1PQ?xsz%Zx z7TTF5bsaZrynrU;W=kC|(9!|lb4prkcp-`XTh-@_qp*|XXZ6C>N)?aV-Zc^)L|qk5wj#U*sh1KU+2lzpo?@aJ8I>e0KihO)nFy+mLrH!Z zS(sZ);F$W-s=Z6U^y+$Yk2~&pk9iT*%QYF7m1R~0)aI?g-ZP*h8@=;l-pfenTEOAg zEZ(tRdR5|}jq4>gu)|39dNpEg@TKp5{HZ(-{XoI1({VJZkYIq>G=Q+)Q?V-HH$+6Lv1?_NGBH ztn^qA<)(#7x>Qn@a2WmoE`DlrNdDYoneRK=rcpc{*3oa3^qid8Uj(=7zfXgD?2--7 zK3HD`PVBQT>(VQaIX!XivFGn4$jB}JL97rmU9AXQP#?j*8)zeZp~!sCk$WW}n29 z%e|faqw!_Ezm;($sos5#RPZr@nn-7ci0p73#)j@#Z4HP`5&PnQcb`=22zq<7U zK=Fk_^U|HQTjJ8U;iM*!U@{D~{*Ejh&{e^Kzy#Xu3-3!lAjPT^4gVv-B5xJ8I`FxW zD#7#N1vEJl?_Pu487v*B2-_xfR2DbR=AVnaNnc_yr;)@`i@uCX83U>uO8Z>%f?>6M z2r_^`I+ji~p^{ob3;`y6-oRUY@($Rd0I$jX(Fqi0gQkoDZ z!3q=gN&Hdmw+}zeiYihgJjlb}h8nzV)SqK)8>J**9cBdF>l-$p7$PkmW*j9I6w+ks zctUC}$QK>TB>IaaAw)1k5iJVq37FR$m>D|dQP;?!q*vXJP@bLKlWpkK6)W8&=ERpa zC}_G|G+cCIzMGxss1B(V923q)G?fC}Y%@s{8F(*f&ZzPb-pf7?Ccj>??TWVkFblRw zv%{o%yLK@UbY#f;n*@f+g6O%}whL|I=(#B$b7AwVz=G?C_pVt-Stgd8dMu3Ey!YB;xBSh#C_%W_IUI+w z&9~T@lP-d9KyK&RdS1RwMUy0+d+D}%d_Gqrj;-(3N1wYBz6hAlr7YMz(&#SWJhOp_ zj@u>@?gbZ97RorQ*NXT*U(5|2WJp^BWy)%cP#GEA<~P>83woAB9Pvp9O^qJl70DCY zCu?wsBT*fS0Th_2x(4+Ay5Szc_W&4gi?v!f#KIGZcE8hF_W>v-kKw0G76?#EzCLhG zN*DO_jSeW~*^=1MscZ_vK4f~RK=bdA=irx`JTUq>51AFQvSR2ZYIde2M7+LV&nqh{ zXGPmu35Hi?%FxLqhv*ZtfF=Mub$-D4otm3gNnx^Zbe@w(@zO4BD9~AFig#ym-(fPw zjp1qKvuAP0Lo~YQ{U|79N1&W<0zhs?8kdGSmJ!qjU}V;sVM}iEhjLqLPQ&nP_ad=5 zN;;$-j^d!%0aL;d@RFa##qs=P?pqt%T4wiwL9!e~=%6KFA%1A0z^No6MdA8^Ymz=h z2L%B{e8I^m;u#mEU0u~m`*^=nGv<&3n1%UOW4iPVuraPXmJwEKjScG!6!b37b6fD^ zF9*AQwiP5Idmow>VM+R9dl#5&ZvN^0bbgw4`&Umbcy4Ni8$W@RGaGF#6J-=r^MKd%J;bi7(+AW-n)3 z>Jm5P$Gl@p_7^Q=Oel82D^JaO8va5;9ckhZ_RnyWj`xmXPliV`53SPDDH5m{zKhG$ zRU*E(gTBk(hqkru^n5$uL-y}lBV^?Vr^B=zuWff)tN8?O(&HheJ6|+r99?>eemtG_ zBj{^ScWdb_d$LFfSYbB$tJf>^>TTbL;(UuveEr7nzN(!@TfP!CbG%6D6{ymR;GK%s z7|O{j^>+{TaJ4nl(B^p;Qq7&(ODS>j9LW@Ppy2i9_o$StGiMo@=TqE9J zsoG8ING7<%!ea$18}WH?o#y2W86v;?pJ}?U%5dHy7e_TYJl4oUOONjz&xQ50n3(rbK|z5$&QC;qz-PRy8?D=%WaWv>EmCk_&S zD<;41B{VHlD-Utx93~KUvgfqiE%zVII2#3r$6;ZjNcb{|9K)@{jLn2p*uk>7&ia+B z#iHMaD*7)A=${%7x{w3ZXML=+=f;D3Ia~sU=Vuq)tTri!5F{Y$s8Z1AAo!XRGO4&= zB}1z%NvxKcfUQ4`wJE-3F*saZQ3(XP5yV`EWHay$tc3#9yy1nIH^`TZ2aja0Ke1ODZ|c{875S{are)s%&lE@gUBhFqK2q_y3aZpM?ap zuCZEr^va)p*))xtEA;ydm{d1ldBr+ACuek9OC>2xQ8rH~)mxo>{4nHixs%q&DKwpk zyR2XutN@5$!*AtMAI?-ihYj0!1k?UYcvtzdfBrG1A44}9J#18PUCe=uddBABvH!(p zSD?eNlEo7AbLX2`ljsfC$=C(YV6NC;4Q9ugPTNJD`;S)L@H4jM{j+WFxN~fXc*0a) z?QYJl$I81wL6NHlcNFqa{yX1Ss-s!mT*LUBCt(e8N|;g&jGoXY@e_mm!RiJF@Un_@ z*!m*nOiu2Gfo^y)w8R$);+H?D@n_;3iW{|9W}d&z{vO|RdU;+@Ph%PSbb<&fW8;&Z zTE|mZx;G2iYvcf7p1_j;vRKgR<#Bf*{%^R0;;^M_sG9bVB`P&en)vr<_=K=O3MGR% z4y@I*tP97cIV}Jd-s^mR9PmQ`h1OMMJYh)E0nI~(OT3v zm$-Au4$?EklbNydZLL7OUp9f27&z;Im-aR(bi`S*hG*wCci5;OiU+h8c605b#Gunj z`awfSywP9T%UFrGGdybU%ix8xTIRo*nBgcSbG0sRAk%`|a6%I>7`X?Z)at$ly|NoF zZ9u$KU)D}wMVF34zpj8!0+$f3A9L@Qcv zXr*tH-<75g{v`qI4H9I!RNHwl(DN0i)aJ`aKY)Lltt|YQM2-`B zj7N_22#hy~h`MzirWXxyjWZCtcZd6_31SxAQo7m=2W+6W%X!_F7w6N+Tn`uf0Kl2_ zVGotRCTP5yACy*0N6>?Bo4T8CrsRL_JE9F;)LV+T9IOk~B=Z)97q$|M&R301R8?c@ z(?8M>nWg{4%wVhWNs6ZJb|H2BL%m;l#9D!xI}LuGT0%MY(yR4AwXgz?h~ZU&b1ow_ zz8_dj)7x;~p$zhsrFrdrCU%nJ#9!N~a5KIyWJZ6mtRPnq_W>X5JX6}G+kEC|+ic|30V+U0w@$3JP=T28Yc2tv-- zRO(QPP?sQDFm|KOO@3-2JbQhqC+N%TINXuOntkojz%4s|go12oi2=*rO zc&6wIJL^>*tLmj3Raqm}r!RBpM4*B3lqf04RN%exFgQHb*VkqmoqOd`9#e*O^euK& z58CFky$MwSm8F%1_O$6}$D;3Xps25u!n21;C?{E5@OhK^hf~^5({tme{i=1jdCOTw zxsf#v6C(z~s@}M8>?BK~2G?%sKVe`@I@j( z$bNDd=RUlEUJ*N1i6~}G=8OG%aYnUwVWn+`tx?>|&e2VkQn?!&w0Y>fBs<)pxqQ8A zy}Cz7luC&kOrLO1zQv!zw$9oOj3^umM{GDWr`_UD;&m&nxNTDf?i~mgT{4}(|D8Pa zH$3;RT%!LDm%oa5U7Y_7%*9_NnrNhCR)AyGA8OtJ+Q7nrX3(4^O1=?a=q15CZ85V& z))u1+V16>#f9pLGT`f-ToF*Gxv9`4DH}A8E5i^~sfWOVr5~nj za&YG1pZ`%7%=>&*);MveXOfjmuBwma%^Pa@8JZ%*H}x^iL@|;5?px6aT2yS#8k{LB zBs@;ZsnB5agr_Sp_eCYMC{c6rpuM9E!P0If{gN1y(5%`STA}InZzrmv4a#MfcEXs%>DdE7K&X3h8U3n zw%n$=3O9~6K0pYAlC|c{wG1q-&>yo4(+t9>-ID6ns4FB-UEy*wX%@B|Ki8NKT*d^I zCE;kzQY=Dem`8y4!P&xf9+b(qy@)bjw@C~usqh0XWzRJLwaPbmO+DH5go7$7R6 zxiboG-wUU@m5q+o#(~AY}67;z=TOG zx;=ghSep&X3Okx-n+*;)AlUK-|Q@rW2 z_t(xnh?t6RZd75$#mqUPy1VS85!{H=&~9S!v6umAqMkJ3yNk3=Mi}Epa0Z=+rF{X6R*V&cvY$+{7lcQ{avP%*G zasqSe05{-?2jzyU1w|+aAGX}c>Bk!yxGp9A+a4g`@LWmQLKO-w1^Brv(r*^v6i+F( z|Apn57omdjJ1d+ZaQdMUP1e7Qv~pa7Wz$}AZ+0e&JirKk<{rL5N{O$*v$4-S>vPFl z@J29niBR5F{y3?YEsbDw*GK?{3uE>O3?L3{ydz~k?12dZrJ+m2^7ksWduPF0{m&(D zfagfAiTu^oZCBtD@%K(Nfi+8PfG8Gx#7?@Iq)-$o2i{4N-;#8_@s}KbQ7ddM1PB)Y zU_LBjm+Ot0#Ryak$`cRy+)0(AOofj?i;tThpvyIoD?FEXQt)cnb?(mzP5}z{QJE5` z7_oVe(BPE|ZHMgr0cI)(r?=^w8HUCrQZ zvs6+>@cT*rH3$`#VHLB_`&D;OE7{E_3HRV0b4>Dfap=6rPWE|%DH0#1r-wgyD0Rqx z!bAUkR0-yg3VAe3W-$|(YzUw1JOakw{a-db zE3J8zGW6(oIB=|>gq*9isW+ZIZwA0=u6S`(afjMp4CL+g*XfR5dhVNkkY;vf0}7K= zn#w9BRhLQkGznlJlol@G%fX4eVVRh;W7%*0f{FT9*XM0fOouBY=1Z7`K!O%kh@#!A zATZei$3c@7}#ituqJ-zUXix zFE2;iHTa+nezWCE>u$(Ti!z03^x=?;*`Z-fHTYQa*ynAOWPezmMGT+5iRBMjYBYPW zf26``($HF((lhC&OByMUM$mSPrq#fluA@%!bP4@iUQknZ9kjzcCRz!E)9~MbelKiC zw(t9^P*M<(L96SAa*Ab~a(sDmdVL0FbQjp<&|bBH#jZ>i(oVr^htT-hR=%xKDJqB5 zfKbMsyvx?LB`MCD^}~pjqTiyRhK+f#^FHBKG@v6n)8l^?)eGO4H0$x8+jc0Cj+YKB z+0sWVfDz=hy)Q@szNBQrHNf#t5}?_?rQkhf%u`BU_Je9U~8CTRl2gt^kgeMRU@#ZgSIMF z4}brD9PmwY@TNCa1Hm4SvwaOCLe$UHkNsdB;$n@>+^;AQ#S`!)1yPcsH{Ei!FbnVM z^EvdguU$oft%xfNsA?b9B-Fp^aO@M-sOYxgXK-cX04dx0IX2+~ z>(QwN8_Gr{7EZefOt*^d=ZV0g+ym4kk3|7_iX0(nIeeodBUFoHzLGGv~*d+5h%T z{-mtQO4d8;eV*&S@24(xFs1d-B&26hxZ*_bdn~Q_$Nsq*C$a8=12u}2c)sTP;Xk>8 zXJMWO+wS_``Dw8b;IR@m*zS@1C(EEcI4owm;3jZJq2{GzL-!fju2ho+^TD$yDV}=7|N_mth zqvZ&S6yI-#P9r6*}#} z@mRe*Wjwd^o1NnwslUGVM3#xK0D6a>1e9bJogql*D_fV`hMi8eaghn2 zv^n4tOrw+RMZAfTGZ!3}7to~n2xAJ%zQt)%{6VqY4+G1w(JB;>?bnfj8fC2v$Hww& z_G45iK}kkNH_h_5dLiJhrln8p0jFzuX0lpK4FF!#XPxx&*0_R$VrMaE$&9OYk9iGL z7Fgwi4O-dCHLuDnoCjumS@!!oz18zat5u;LPHq#x-%fNaY?j7kPk|+_Fo@Wb55V*% zroC9wGsQlXMu_D^_sq00d`dCwaIPcI&pmgMS=39X&8@*+f9HoW4798>gry6OfJ1{Y zdbqF!(%LC3aDCn>@*+!&$1P^V48f}lWzg0kgS5Vy^+8~&ssN!37K>sOC}E|UZeVMCzgsV+JqD47zDTBPRNyKQFUAOWhJUL%{(bw6RHa zVr|^#RrJKc2e|@#*D;}ll5^pMGRk;IUUllxyW^kpzw*Hv+?FF2PHWhQlF1G^zG?fm zy6(x4yGq2W>ARhS^u)RvsE)3({Bbq>r?x3$#~{WQSai8IC7uHZx29BfN;q9*{@rTl z(S?Yq9p&AU0YUE!^SagDxum3X5A~2Je&dSEqseiP-u@-mp%S@QPp<9{AK!`(?M^*= zs71WW3vT1L3A80W)pPA`NAkDddc^Tuw2^ZO-e5(jniVN!b48}h#RiIUEn<^{))9x( zhFUlir|$Lz*~*Dm{Gac~ho*O@MnBt#!7iC9U#F+H3WzR(VFl}Cb3@I|m8}&lYe5ffMlMGZ%^hvlbrBDY# zOakUwSWDA_5&JYgeH(S>vz=e1O)pEzp@u;lJB%=sH?MiV^klyYaf3 z|GmXa+xxVQYoy<-8A*t|Tv-3QxN{)Om8R@ttC!9lEO6r^AINjUrpR{w5^0xsSHC7n z7~>brEvF{euUL^QyMBU|8u#P-$9^?V%rELGoXaJ8u$V>H+L|Yx5H9D3z=YU6>W^oe zG)SASHl8SHM{W%)G?v#eliUiZ**1G;Yhk>RF|LB>yqP7r3fF~#z2!tPhRz=gWz3MoutmF5 zCj3gOvs%EmlUla#{-G+CR8k|ZZ=^YpWBe}Ji34(9DlJ{jhnnHi&vPz8KNNaBq+ez& z#-F^L)occu=d&aMyh_j9dTZKJ1hx2YppMrbw?=o1U7iTKy>7_Z3>?wwD{8Od?ikZmxzds513DHHrxDF72k0hGW>i%PiDw1%70X^5MKT^4x{S1akTC zW9(|r23$e@q=PAFN-;~&Wsf@Zmxt|mFR*Sd{Yn{@+~D-iQWzPR2`pU8lCQIOqi_i_ z`llryO8P%7`BB5lQSzClZjAXTd&B`2GHk7TL!C9eh7g8v*jT@VD*W5Xc1Jh|vU>iX z4d|fVq<^$6zmg=DN6o9E-uUfZXDv2;*0vH2i)RRO4kO0*uj8sXh_dEKNmKpY5{toa zRm%PyI)5f11ULMvxyurX{apB(TbCcz^JGv5Tw;DYow0NBL&<6OCDwB$wZ=e8Ny;3% z5Y8y5%h~mW)K60&02E%A|!Z70ms7p+{{rQ*E?#)9|tDaL^#K4XlO5 zFm@?h+qd+NHMc9%`SI}D{ zRAE(qySaF00tEK%I_hzRJE%T*je-zmTxaHCJA#V*LFzeGKf}Ej1aQCMDWWcsfO{A{ z*S6#j+FRE0ztOQ?=h~wsBmM+-Dm53)evBCVctntLO;pF$4D}sT7l>R#2 z*h$YGMwJkt1dgcqlT(&|Xfz&yzO}ZHY=+xbdi5{{;teQP-_b}3MLRIeX`TfRe?N^I zV$867$k^v~j;FaZl0huGr7Y`MwFK;DES+20qQ4J5Dge}?3zB!Gd`h*F2#0Jv-mhQ< zbcDy;+fYFlLhQz*U+yXE=jqS|l48KV`{Wwi$?dJ0$s`<5d}W5ncGES9Vy!ULpFDW! zb=uFCIZmb7B5Iz=q(felPXxUN9_dyOc4wSeNs6N%BgrmYh#qR@(bjVu|K*{!Qh33g z1W$>2s)fXT?>Y)^R0C^#KZIoT09xz3Y=^IMQC%oKxW1^18{LEV-2r#g=S8f)T#(?d ziVituF1L7q%w3j-d#JBOuH(3Fh-0)W;~e{Oo}LOU(o-7?kf&eRu%{kVEW$!k<8H5b zwd{~!2FAOBUS_hKBsa4Uw*^e2^lDNdk|PAgSuUIDUev+rj>oS#thl&~b8kydUv5wj5P}$RJ`qVJZ8SIlBOQ1Bu5s%v5hjc0{KfUku)J#r-`MJ&ng5z(l>2> z{4v)22X9x#Ca-x(F2@sRq|8O%watr^vEO!v>}drkGh!$jS31fo$w*`i@qD6B)~iED zonIF%mCxQonU&$F5g21@ks5)unC9Tp)q8}T{%uk(rKidD!N*(FQpRVQiGohUHO5nR zT^RG1*s*=Mby{p%xz~QA8sJs;i#a4<*BvM-3mxW1fta3em4I~$N*nS+3ar5!&YiiF zo-N=h-NK+q(>uel%=iFuuE#3>3}29E5R#~ZMVAH!|4^bW;VqDFUv#ImS@BW#{%TV# zpV@x8BmDbdb*Um7bE5R-{u+*0w)MGPpAQqdEvEhrv(Cz`Ela*jeadd+=xp7JG2R-p zE8!X#n8L$86-1gnqkC}M|MW2tMt5I{7VK0z$3@yGMKGA@E@zqq11E46dxIfW+dx4p`KL7OLuG4t!K{MI&Fl#kXaz`o_YI&oD37V_ zQ9b>nwSF>MXaH2A#iu2%7g&y}f5PYj-xAA9Rjqz11!${Uo<^vp9MQXQ7%fu|=j%0| zx1ZQ%ASp3(%wju~K2_0Hk+=~)=oWAFAUqGhdypDdQ}>}OKGevnCFJ+gdt2jsH3d1Q zSB^XRTtljW5UiMcYwVY0QhA%xRMmz{xKPbxXr1HV#h7y@lKKIdvb#xg>BKz19Lpu3(7Kztgzy)UD4f@18+0SN z*2?XnASjcWe}?t0wo097pj&o|E|*gu%E=a@l$CjtT*#mICcKusGZI02r%I^LgeZ$n zX4Oo??Oj{MSB3Cuy#*f5TnIMWd(hOHN2iy|{x*HE#L{>)abOOz5-tbZK7q`;^oiyI z{E$mN`y~dCWTHpC9^t z?nBsjOT}&DDA~Sm-UD8j&Rk1B9;c37Adn#|Yq}-_B+%I_Ka-kbf>yT2`bb?i&)=kN z%(`t_y|$z&F)2gG$jmIINzKlsNz?FCy0z4vI2S-0wb!jOBO0v_dfOLqfxfXt&MtB;YKQT$Kb}Q;5FO5}55^&>4O7lpY?^Uq>@(585wd zmqVLhR4Jm0a4?W6^T)}1;*b1aA7+jc{c1ppZVVc%AeT}B4`DtofA2< zTox}M7oZjlnwyYuPdpt~2-}sp?JWa9L98XawSFx07(nX~U=)?ex1 zIq-A-7x<{~_i2XY_Y$3Ya8bA#%6;?zbH5(0GH1T}6^ZVRO9JPS!VNGeZ(q=(Bc(CN zATP`bVe{t{PCbD5Bht=*@oE0k1vK%gTrwemm=|eJSQv-+VV8R1yon6|BH`o z1w@&NU>zpAdt~J1Vtkfm<_9?AF#tShMh+$kBZx0B{;?5QnrF0!hU LmSPFiBJ_U%+FQ}0 literal 20821 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 Date: Mon, 2 Dec 2024 02:18:37 -0800 Subject: [PATCH 19/35] Attempt using hatch publish command (#264) --- .github/workflows/publish-py.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-py.yml index f72cc55d..f3ade2f4 100644 --- a/.github/workflows/publish-py.yml +++ b/.github/workflows/publish-py.yml @@ -21,10 +21,11 @@ jobs: python-version: "3.x" - name: Install dependencies run: pip install --upgrade pip hatch uv - - name: Build and publish + - name: Build Package + run: hatch build --clean + - name: Publish to PyPI env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - hatch build --clean - twine upload dist/* + HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }} + HATCH_INDEX_REPO: reactpy-django + run: hatch publish --yes From d663d12f0de59653d9c8b9c9c252fc565712c3ee Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 2 Dec 2024 02:25:48 -0800 Subject: [PATCH 20/35] Remove HATCH_INDEX_REPO from CI (#265) --- .github/workflows/publish-py.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-py.yml index f3ade2f4..ae1ace7f 100644 --- a/.github/workflows/publish-py.yml +++ b/.github/workflows/publish-py.yml @@ -27,5 +27,4 @@ jobs: env: HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }} HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }} - HATCH_INDEX_REPO: reactpy-django run: hatch publish --yes From c8b4ec08ab7c701a443e8fff8e734a5e76700857 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Mon, 2 Dec 2024 04:11:58 -0800 Subject: [PATCH 21/35] Synchronize CI with other reactive-python repos (#266) --- .github/workflows/publish-develop-docs.yml | 5 +++-- .github/workflows/publish-latest-docs.yml | 5 +++-- .github/workflows/{publish-py.yml => publish-python.yml} | 3 --- .github/workflows/test-python.yml | 2 +- docs/mkdocs.yml | 2 +- pyproject.toml | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) rename .github/workflows/{publish-py.yml => publish-python.yml} (76%) diff --git a/.github/workflows/publish-develop-docs.yml b/.github/workflows/publish-develop-docs.yml index 11a7fa23..00172d4f 100644 --- a/.github/workflows/publish-develop-docs.yml +++ b/.github/workflows/publish-develop-docs.yml @@ -19,10 +19,11 @@ jobs: python-version: 3.x - name: Install dependencies run: pip install --upgrade pip hatch uv - - name: Publish Develop Docs + - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - hatch run docs:deploy_develop + - name: Publish Develop Docs + run: hatch run docs:deploy_develop concurrency: group: publish-docs diff --git a/.github/workflows/publish-latest-docs.yml b/.github/workflows/publish-latest-docs.yml index 697b10da..41ced54d 100644 --- a/.github/workflows/publish-latest-docs.yml +++ b/.github/workflows/publish-latest-docs.yml @@ -19,10 +19,11 @@ jobs: python-version: 3.x - name: Install dependencies run: pip install --upgrade pip hatch uv - - name: Publish ${{ github.event.release.name }} Docs + - name: Configure Git run: | git config user.name github-actions git config user.email github-actions@github.com - hatch run docs:deploy_latest ${{ github.ref_name }} + - name: Publish ${{ github.event.release.name }} Docs + run: hatch run docs:deploy_latest ${{ github.ref_name }} concurrency: group: publish-docs diff --git a/.github/workflows/publish-py.yml b/.github/workflows/publish-python.yml similarity index 76% rename from .github/workflows/publish-py.yml rename to .github/workflows/publish-python.yml index ae1ace7f..93fbc969 100644 --- a/.github/workflows/publish-py.yml +++ b/.github/workflows/publish-python.yml @@ -1,6 +1,3 @@ -# This workflows will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - name: Publish Python on: diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index 8faca864..cdc0b89e 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -11,7 +11,7 @@ on: - cron: "0 0 * * *" jobs: - python: + python-source: runs-on: ubuntu-latest strategy: matrix: diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 100b669b..bde5ac08 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -124,6 +124,6 @@ site_description: It's React, but in Python. Now with Django integration. copyright: '©
Reactive Python and affiliates. ' repo_url: https://github.com/reactive-python/reactpy-django site_url: https://reactive-python.github.io/reactpy-django -repo_name: ReactPy Django (GitHub) +repo_name: ReactPy Django edit_uri: edit/main/docs/src docs_dir: src diff --git a/pyproject.toml b/pyproject.toml index dbb94c21..6366a2f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,7 +195,7 @@ fix = ["cd src/js && bun install", "cd src/js && bun run format"] ######################### [tool.ruff] -extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", ".nox/*", "build/*"] +extend-exclude = ["*/migrations/*", ".venv/*", ".eggs/*", "build/*"] line-length = 120 format.preview = true lint.extend-ignore = [ From 5596d8d050096bbf0135b3cf6237bb27dd1d7d29 Mon Sep 17 00:00:00 2001 From: Mark Bakhit Date: Tue, 10 Dec 2024 20:58:49 -0800 Subject: [PATCH 22/35] Django form component (#267) --- CHANGELOG.md | 6 +- README.md | 1 + docs/examples/html/django_form_bootstrap.html | 11 + docs/examples/python/django_form.py | 10 + docs/examples/python/django_form_bootstrap.py | 9 + docs/examples/python/django_form_class.py | 5 + .../examples/python/django_form_on_success.py | 21 + docs/examples/python/example/forms.py | 4 + docs/src/dictionary.txt | 1 + docs/src/reference/components.md | 106 +++- docs/src/reference/hooks.md | 4 +- docs/src/reference/settings.md | 16 +- pyproject.toml | 8 +- src/js/src/index.tsx | 63 +++ src/js/src/types.ts | 5 + src/reactpy_django/components.py | 69 ++- src/reactpy_django/config.py | 5 + src/reactpy_django/forms/__init__.py | 0 src/reactpy_django/forms/components.py | 141 +++++ src/reactpy_django/forms/transforms.py | 486 ++++++++++++++++++ src/reactpy_django/forms/utils.py | 31 ++ src/reactpy_django/hooks.py | 4 +- src/reactpy_django/types.py | 19 + src/reactpy_django/utils.py | 25 +- tests/test_app/components.py | 2 +- tests/test_app/forms/__init__.py | 0 tests/test_app/forms/components.py | 94 ++++ tests/test_app/forms/forms.py | 50 ++ tests/test_app/forms/urls.py | 11 + tests/test_app/forms/views.py | 21 + tests/test_app/middleware.py | 13 +- tests/test_app/settings_multi_db.py | 1 + tests/test_app/settings_single_db.py | 1 + tests/test_app/templates/admin/login.html | 6 + .../test_app/templates/async_event_form.html | 26 + tests/test_app/templates/bootstrap_form.html | 28 + .../templates/bootstrap_form_template.html | 6 + tests/test_app/templates/form.html | 26 + tests/test_app/templates/model_form.html | 26 + tests/test_app/templates/sync_event_form.html | 26 + tests/test_app/tests/test_components.py | 299 ++++++++++- tests/test_app/tests/utils.py | 6 + tests/test_app/urls.py | 1 + 43 files changed, 1664 insertions(+), 29 deletions(-) create mode 100644 docs/examples/html/django_form_bootstrap.html create mode 100644 docs/examples/python/django_form.py create mode 100644 docs/examples/python/django_form_bootstrap.py create mode 100644 docs/examples/python/django_form_class.py create mode 100644 docs/examples/python/django_form_on_success.py create mode 100644 docs/examples/python/example/forms.py create mode 100644 src/reactpy_django/forms/__init__.py create mode 100644 src/reactpy_django/forms/components.py create mode 100644 src/reactpy_django/forms/transforms.py create mode 100644 src/reactpy_django/forms/utils.py create mode 100644 tests/test_app/forms/__init__.py create mode 100644 tests/test_app/forms/components.py create mode 100644 tests/test_app/forms/forms.py create mode 100644 tests/test_app/forms/urls.py create mode 100644 tests/test_app/forms/views.py create mode 100644 tests/test_app/templates/admin/login.html create mode 100644 tests/test_app/templates/async_event_form.html create mode 100644 tests/test_app/templates/bootstrap_form.html create mode 100644 tests/test_app/templates/bootstrap_form_template.html create mode 100644 tests/test_app/templates/form.html create mode 100644 tests/test_app/templates/model_form.html create mode 100644 tests/test_app/templates/sync_event_form.html diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c848e4..f819750b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,15 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] -- Nothing (yet)! +### Added + +- Automatically convert Django forms to ReactPy forms via the new `reactpy_django.components.django_form` component! ## [5.1.1] - 2024-12-02 ### Fixed -- Fixed regression in v5.1.0 where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. +- Fixed regression from the previous release where components would sometimes not output debug messages when `settings.py:DEBUG` is enabled. ### Changed diff --git a/README.md b/README.md index 89d1fb11..f60e7a2d 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ - [Multiple root components](https://reactive-python.github.io/reactpy-django/latest/reference/template-tag/) - [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 form to ReactPy component conversion](https://reactive-python.github.io/reactpy-django/latest/reference/components/#django-form) - [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/examples/html/django_form_bootstrap.html b/docs/examples/html/django_form_bootstrap.html new file mode 100644 index 00000000..6aba84ca --- /dev/null +++ b/docs/examples/html/django_form_bootstrap.html @@ -0,0 +1,11 @@ +{% load django_bootstrap5 %} + + +{% bootstrap_css %} +{% bootstrap_javascript %} + + +{% bootstrap_form form %} +{% bootstrap_button button_type="submit" content="OK" %} +{% bootstrap_button button_type="reset" content="Reset" %} diff --git a/docs/examples/python/django_form.py b/docs/examples/python/django_form.py new file mode 100644 index 00000000..51960db1 --- /dev/null +++ b/docs/examples/python/django_form.py @@ -0,0 +1,10 @@ +from reactpy import component, html + +from example.forms import MyForm +from reactpy_django.components import django_form + + +@component +def basic_form(): + children = [html.input({"type": "submit"})] + return django_form(MyForm, bottom_children=children) diff --git a/docs/examples/python/django_form_bootstrap.py b/docs/examples/python/django_form_bootstrap.py new file mode 100644 index 00000000..449e1cc4 --- /dev/null +++ b/docs/examples/python/django_form_bootstrap.py @@ -0,0 +1,9 @@ +from reactpy import component + +from example.forms import MyForm +from reactpy_django.components import django_form + + +@component +def basic_form(): + return django_form(MyForm, form_template="bootstrap_form.html") diff --git a/docs/examples/python/django_form_class.py b/docs/examples/python/django_form_class.py new file mode 100644 index 00000000..e556295e --- /dev/null +++ b/docs/examples/python/django_form_class.py @@ -0,0 +1,5 @@ +from django import forms + + +class MyForm(forms.Form): + username = forms.CharField(label="Username") diff --git a/docs/examples/python/django_form_on_success.py b/docs/examples/python/django_form_on_success.py new file mode 100644 index 00000000..d8b6927c --- /dev/null +++ b/docs/examples/python/django_form_on_success.py @@ -0,0 +1,21 @@ +from reactpy import component, hooks, html +from reactpy_router import navigate + +from example.forms import MyForm +from reactpy_django.components import django_form +from reactpy_django.types import FormEventData + + +@component +def basic_form(): + submitted, set_submitted = hooks.use_state(False) + + def on_submit(event: FormEventData): + """This function will be called when the form is successfully submitted.""" + set_submitted(True) + + if submitted: + return navigate("/homepage") + + children = [html.input({"type": "submit"})] + return django_form(MyForm, on_success=on_submit, bottom_children=children) diff --git a/docs/examples/python/example/forms.py b/docs/examples/python/example/forms.py new file mode 100644 index 00000000..8d3eefc0 --- /dev/null +++ b/docs/examples/python/example/forms.py @@ -0,0 +1,4 @@ +from django import forms + + +class MyForm(forms.Form): ... diff --git a/docs/src/dictionary.txt b/docs/src/dictionary.txt index 1b4ce080..d2ff722d 100644 --- a/docs/src/dictionary.txt +++ b/docs/src/dictionary.txt @@ -48,3 +48,4 @@ linter linters linting formatters +bootstrap_form diff --git a/docs/src/reference/components.md b/docs/src/reference/components.md index 4186af42..26feda67 100644 --- a/docs/src/reference/components.md +++ b/docs/src/reference/components.md @@ -156,7 +156,7 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? info "Existing limitations" - There are currently several limitations of using `#!python view_to_component` that may be resolved in a future version. + There are currently several limitations of using `#!python view_to_component` that will be [resolved in a future version](https://github.com/reactive-python/reactpy-django/issues/269). - Requires manual intervention to change HTTP methods to anything other than `GET`. - ReactPy events cannot conveniently be attached to converted view HTML. @@ -292,12 +292,12 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. ??? info "Existing limitations" - There are currently several limitations of using `#!python view_to_iframe` that may be resolved in a future version. + There are currently several limitations of using `#!python view_to_iframe` which may be [resolved in a future version](https://github.com/reactive-python/reactpy-django/issues/268). - No built-in method of signalling events back to the parent component. - - All provided `#!python *args` and `#!python *kwargs` must be serializable values, since they are encoded into the URL. + - All provided `#!python args` and `#!python kwargs` must be serializable values, since they are encoded into the URL. - The `#!python iframe` will always load **after** the parent component. - - CSS styling for `#!python iframe` elements tends to be awkward/difficult. + - CSS styling for `#!python iframe` elements tends to be awkward. ??? question "How do I use this for Class Based Views?" @@ -381,6 +381,104 @@ Compatible with sync or async [Function Based Views](https://docs.djangoproject. --- +## Django Form + +Automatically convert a Django form into a ReactPy component. + +Compatible with both [standard Django forms](https://docs.djangoproject.com/en/stable/topics/forms/#building-a-form) and [ModelForms](https://docs.djangoproject.com/en/stable/topics/forms/modelforms/). + +=== "components.py" + + ```python + {% include "../../examples/python/django_form.py" %} + ``` + +=== "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + +??? example "See Interface" + + **Parameters** + + | Name | Type | Description | Default | + | --- | --- | --- | --- | + | `#!python form` | `#!python type[Form | ModelForm]` | The form to convert. | N/A | + | `#!python on_success` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form is successfully submitted. | `#!python None` | + | `#!python on_error` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when the form submission fails. | `#!python None` | + | `#!python on_receive_data` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called before newly submitted form data is rendered. | `#!python None` | + | `#!python on_change` | `#!python AsyncFormEvent | SyncFormEvent | None` | A callback function that is called when a form field is modified by the user. | `#!python None` | + | `#!python auto_save` | `#!python bool` | If `#!python True`, the form will automatically call `#!python save` on successful submission of a `#!python ModelForm`. This has no effect on regular `#!python Form` instances. | `#!python True` | + | `#!python extra_props` | `#!python dict[str, Any] | None` | Additional properties to add to the `#!html
` element. | `#!python None` | + | `#!python extra_transforms` | `#!python Sequence[Callable[[VdomDict], Any]] | None` | A list of functions that transforms the newly generated VDOM. The functions will be repeatedly called on each VDOM node. | `#!python None` | + | `#!python form_template` | `#!python str | None` | The template to use for the form. If `#!python None`, Django's default template is used. | `#!python None` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run event callback functions in thread sensitive mode. This mode only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python top_children` | `#!python Sequence[Any]` | Additional elements to add to the top of the form. | `#!python tuple` | + | `#!python bottom_children` | `#!python Sequence[Any]` | Additional elements to add to the bottom of the form. | `#!python tuple` | + | `#!python key` | `#!python Key | None` | A key to uniquely identify this component which is unique amongst a component's immediate siblings. | `#!python None` | + + **Returns** + + | Type | Description | + | --- | --- | + | `#!python Component` | A ReactPy component. | + +??? info "Existing limitations" + + The following fields are currently incompatible with `#!python django_form`: `#!python FileField`, `#!python ImageField`, `#!python SplitDateTimeField`, and `#!python MultiValueField`. + + Compatibility for these fields will be [added in a future version](https://github.com/reactive-python/reactpy-django/issues/270). + +??? question "How do I style these forms with Bootstrap?" + + You can style these forms by using a form styling library. In the example below, it is assumed that you have already installed [`django-bootstrap5`](https://pypi.org/project/django-bootstrap5/). + + After installing a form styling library, you can then provide ReactPy a custom `#!python form_template` parameter. This parameter allows you to specify a custom HTML template to use to render this the form. + + Note that you can also set a global default for `form_template` by using [`settings.py:REACTPY_DEFAULT_FORM_TEMPLATE`](./settings.md#reactpy_default_form_template). + + === "components.py" + + ```python + {% include "../../examples/python/django_form_bootstrap.py" %} + ``` + + === "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + + === "bootstrap_form.html" + + ```jinja + {% include "../../examples/html/django_form_bootstrap.html" %} + ``` + +??? question "How do I handle form success/errors?" + + You can react to form state by providing a callback function to any of the following parameters: `#!python on_success`, `#!python on_error`, `#!python on_receive_data`, and `#!python on_change`. + + These functions will be called when the form is submitted. + + In the example below, we will use the `#!python on_success` parameter to change the URL upon successful submission. + + === "components.py" + + ```python + {% include "../../examples/python/django_form_on_success.py" %} + ``` + + === "forms.py" + + ```python + {% include "../../examples/python/django_form_class.py" %} + ``` + +--- + ## Django CSS Allows you to defer loading a CSS stylesheet until a component begins rendering. This stylesheet must be stored within [Django's static files](https://docs.djangoproject.com/en/stable/howto/static-files/). diff --git a/docs/src/reference/hooks.md b/docs/src/reference/hooks.md index 65bf1727..5826a7b0 100644 --- a/docs/src/reference/hooks.md +++ b/docs/src/reference/hooks.md @@ -46,7 +46,7 @@ Query functions can be sync or async. | --- | --- | --- | --- | | `#!python query` | `#!python Callable[FuncParams, Awaitable[Inferred]] | Callable[FuncParams, Inferred]` | A function that executes a query and returns some data. | N/A | | `#!python kwargs` | `#!python dict[str, Any] | None` | Keyword arguments to passed into the `#!python query` function. | `#!python None` | - | `#!python thread_sensitive` | `#!python bool` | Whether to run your query function in thread sensitive mode. This mode only applies to sync query functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run your query function in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | | `#!python postprocessor` | `#!python AsyncPostprocessor | SyncPostprocessor | None` | A callable that processes the query `#!python data` before it is returned. The first argument of postprocessor function must be the query `#!python data`. All proceeding arguments are optional `#!python postprocessor_kwargs`. This postprocessor function must return the modified `#!python data`. | `#!python None` | | `#!python postprocessor_kwargs` | `#!python dict[str, Any] | None` | Keyworded arguments passed into the `#!python postprocessor` function. | `#!python None` | @@ -188,7 +188,7 @@ Mutation functions can be sync or async. | Name | Type | Description | Default | | --- | --- | --- | --- | | `#!python mutation` | `#!python Callable[FuncParams, bool | None] | Callable[FuncParams, Awaitable[bool | None]]` | A callable that performs Django ORM create, update, or delete functionality. If this function returns `#!python False`, then your `#!python refetch` function will not be used. | N/A | - | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This mode only applies to sync mutation functions, and is turned on by default due to Django ORM limitations. | `#!python True` | + | `#!python thread_sensitive` | `#!python bool` | Whether to run the mutation in thread sensitive mode. This setting only applies to sync functions, and is turned on by default due to Django ORM limitations. | `#!python True` | | `#!python refetch` | `#!python Callable[..., Any] | Sequence[Callable[..., Any]] | None` | A query function (the function you provide to your `#!python use_query` hook) or a sequence of query functions that need a `#!python refetch` if the mutation succeeds. This is useful for refreshing data after a mutation has been performed. | `#!python None` | **Returns** diff --git a/docs/src/reference/settings.md b/docs/src/reference/settings.md index 6b1c78c4..94c9d8b6 100644 --- a/docs/src/reference/settings.md +++ b/docs/src/reference/settings.md @@ -34,7 +34,7 @@ The prefix used for all ReactPy WebSocket and HTTP URLs. **Example Value(s):** `#!python "example_project.postprocessor"`, `#!python None` -Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. +Dotted path to the default postprocessor function used by the [`use_query`](./hooks.md#use-query) hook. Postprocessor functions can be async or sync. Here is an example of a sync postprocessor function: @@ -48,6 +48,18 @@ Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to disable --- +### `#!python REACTPY_DEFAULT_FORM_TEMPLATE` + +**Default:** `#!python None` + +**Example Value(s):** `#!python "my_templates/bootstrap_form.html"` + +File path to the default form template used by the [`django_form`](./components.md#django-form) component. + +This file path must be valid to Django's [template finder](https://docs.djangoproject.com/en/stable/topics/templates/#support-for-template-engines). + +--- + ### `#!python REACTPY_AUTH_BACKEND` **Default:** `#!python "django.contrib.auth.backends.ModelBackend"` @@ -131,7 +143,7 @@ This setting is incompatible with [`daphne`](https://github.com/django/daphne). 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. +This setting is currently in early release, 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. --- diff --git a/pyproject.toml b/pyproject.toml index 6366a2f5..c25929bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ extra-dependencies = [ "twisted", "tblib", "servestatic", + "django-bootstrap5", ] matrix-name-format = "{variable}-{value}" @@ -140,7 +141,12 @@ pythonpath = [".", "tests/"] ################################ [tool.hatch.envs.django] -extra-dependencies = ["channels[daphne]>=4.0.0", "twisted", "servestatic"] +extra-dependencies = [ + "channels[daphne]>=4.0.0", + "twisted", + "servestatic", + "django-bootstrap5", +] [tool.hatch.envs.django.scripts] runserver = [ diff --git a/src/js/src/index.tsx b/src/js/src/index.tsx index 51a387f3..742ca79f 100644 --- a/src/js/src/index.tsx +++ b/src/js/src/index.tsx @@ -2,6 +2,21 @@ import { ReactPyDjangoClient } from "./client"; import React from "react"; import ReactDOM from "react-dom"; import { Layout } from "@reactpy/client/src/components"; +import { DjangoFormProps } from "./types"; + +/** + * Interface used to bind a ReactPy node to React. + */ +export function bind(node) { + return { + create: (type, props, children) => + React.createElement(type, props, ...children), + render: (element) => { + ReactDOM.render(element, node); + }, + unmount: () => ReactDOM.unmountComponentAtNode(node), + }; +} export function mountComponent( mountElement: HTMLElement, @@ -79,3 +94,51 @@ export function mountComponent( // Start rendering the component ReactDOM.render(, client.mountElement); } + +export function DjangoForm({ + onSubmitCallback, + formId, +}: DjangoFormProps): null { + React.useEffect(() => { + const form = document.getElementById(formId) as HTMLFormElement; + + // Submission event function + const onSubmitEvent = (event) => { + event.preventDefault(); + const formData = new FormData(form); + + // Convert the FormData object to a plain object by iterating through it + // If duplicate keys are present, convert the value into an array of values + const entries = formData.entries(); + const formDataArray = Array.from(entries); + const formDataObject = formDataArray.reduce((acc, [key, value]) => { + if (acc[key]) { + if (Array.isArray(acc[key])) { + acc[key].push(value); + } else { + acc[key] = [acc[key], value]; + } + } else { + acc[key] = value; + } + return acc; + }, {}); + + onSubmitCallback(formDataObject); + }; + + // Bind the event listener + if (form) { + form.addEventListener("submit", onSubmitEvent); + } + + // Unbind the event listener when the component dismounts + return () => { + if (form) { + form.removeEventListener("submit", onSubmitEvent); + } + }; + }, []); + + return null; +} diff --git a/src/js/src/types.ts b/src/js/src/types.ts index eea8a866..79b06375 100644 --- a/src/js/src/types.ts +++ b/src/js/src/types.ts @@ -18,3 +18,8 @@ export type ReactPyDjangoClientProps = { prerenderElement: HTMLElement | null; offlineElement: HTMLElement | null; }; + +export interface DjangoFormProps { + onSubmitCallback: (data: Object) => void; + formId: string; +} diff --git a/src/reactpy_django/components.py b/src/reactpy_django/components.py index d9ed0e6a..7e821c1c 100644 --- a/src/reactpy_django/components.py +++ b/src/reactpy_django/components.py @@ -1,3 +1,5 @@ +"""This file contains Django related components. Most of these components utilize wrappers to fix type hints.""" + from __future__ import annotations import json @@ -14,6 +16,7 @@ from reactpy.types import ComponentType, Key, VdomDict from reactpy_django.exceptions import ViewNotRegisteredError +from reactpy_django.forms.components import _django_form from reactpy_django.html import pyscript from reactpy_django.utils import ( generate_obj_name, @@ -26,8 +29,11 @@ if TYPE_CHECKING: from collections.abc import Sequence + from django.forms import Form, ModelForm from django.views import View + from reactpy_django.types import AsyncFormEvent, SyncFormEvent + def view_to_component( view: Callable | View | str, @@ -114,6 +120,64 @@ def django_js(static_path: str, key: Key | None = None): return _django_js(static_path=static_path, key=key) +def django_form( + form: type[Form | ModelForm], + *, + on_success: AsyncFormEvent | SyncFormEvent | None = None, + on_error: AsyncFormEvent | SyncFormEvent | None = None, + on_receive_data: AsyncFormEvent | SyncFormEvent | None = None, + on_change: AsyncFormEvent | SyncFormEvent | None = None, + auto_save: bool = True, + extra_props: dict[str, Any] | None = None, + extra_transforms: Sequence[Callable[[VdomDict], Any]] | None = None, + form_template: str | None = None, + thread_sensitive: bool = True, + top_children: Sequence[Any] = (), + bottom_children: Sequence[Any] = (), + key: Key | None = None, +): + """Converts a Django form to a ReactPy component. + + Args: + form: The form to convert. + + Keyword Args: + on_success: A callback function that is called when the form is successfully submitted. + on_error: A callback function that is called when the form submission fails. + on_receive_data: A callback function that is called before newly submitted form data is rendered. + on_change: A callback function that is called when a form field is modified by the user. + auto_save: If `True`, the form will automatically call `save` on successful submission of \ + a `ModelForm`. This has no effect on regular `Form` instances. + extra_props: Additional properties to add to the `html.form` element. + extra_transforms: A list of functions that transforms the newly generated VDOM. \ + The functions will be repeatedly called on each VDOM node. + form_template: The template to use for the form. If `None`, Django's default template is used. + thread_sensitive: Whether to run event callback functions in thread sensitive mode. \ + This mode only applies to sync functions, and is turned on by default due to Django \ + ORM limitations. + top_children: Additional elements to add to the top of the form. + bottom_children: Additional elements to add to the bottom of the form. + key: A key to uniquely identify this component which is unique amongst a component's \ + immediate siblings. + """ + + return _django_form( + form=form, + on_success=on_success, + on_error=on_error, + on_receive_data=on_receive_data, + on_change=on_change, + auto_save=auto_save, + extra_props=extra_props or {}, + extra_transforms=extra_transforms or [], + form_template=form_template, + thread_sensitive=thread_sensitive, + top_children=top_children, + bottom_children=bottom_children, + key=key, + ) + + def pyscript_component( *file_paths: str, initial: str | VdomDict | ComponentType = "", @@ -238,6 +302,8 @@ def _cached_static_contents(static_path: str) -> str: if not abs_path: msg = f"Could not find static file {static_path} within Django's static files." raise FileNotFoundError(msg) + if isinstance(abs_path, (list, tuple)): + abs_path = abs_path[0] # Fetch the file from cache, if available last_modified_time = os.stat(abs_path).st_mtime @@ -259,7 +325,8 @@ def _pyscript_component( root: str = "root", ): rendered, set_rendered = hooks.use_state(False) - uuid = uuid4().hex.replace("-", "") + uuid_ref = hooks.use_ref(uuid4().hex.replace("-", "")) + uuid = uuid_ref.current initial = vdom_or_component_to_string(initial, uuid=uuid) executor = render_pyscript_template(file_paths, uuid, root) diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py index 3f46c48b..f4434c4f 100644 --- a/src/reactpy_django/config.py +++ b/src/reactpy_django/config.py @@ -126,3 +126,8 @@ "REACTPY_CLEAN_USER_DATA", True, ) +REACTPY_DEFAULT_FORM_TEMPLATE: str | None = getattr( + settings, + "REACTPY_DEFAULT_FORM_TEMPLATE", + None, +) diff --git a/src/reactpy_django/forms/__init__.py b/src/reactpy_django/forms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/reactpy_django/forms/components.py b/src/reactpy_django/forms/components.py new file mode 100644 index 00000000..d19c0bbb --- /dev/null +++ b/src/reactpy_django/forms/components.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Union, cast +from uuid import uuid4 + +from channels.db import database_sync_to_async +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_django.forms.transforms import ( + convert_html_props_to_reactjs, + convert_textarea_children_to_prop, + infer_key_from_attributes, + intercept_anchor_links, + set_value_prop_on_select_element, + transform_value_prop_on_input_element, +) +from reactpy_django.forms.utils import convert_boolean_fields, convert_multiple_choice_fields +from reactpy_django.types import AsyncFormEvent, FormEventData, SyncFormEvent +from reactpy_django.utils import ensure_async + +if TYPE_CHECKING: + from collections.abc import Sequence + + from reactpy.core.types import VdomDict + +DjangoForm = export( + module_from_file("reactpy-django", file=Path(__file__).parent.parent / "static" / "reactpy_django" / "client.js"), + ("DjangoForm"), +) + + +@component +def _django_form( + form: type[Form | ModelForm], + on_success: AsyncFormEvent | SyncFormEvent | None, + on_error: AsyncFormEvent | SyncFormEvent | None, + on_receive_data: AsyncFormEvent | SyncFormEvent | None, + on_change: AsyncFormEvent | SyncFormEvent | None, + auto_save: bool, + extra_props: dict, + extra_transforms: Sequence[Callable[[VdomDict], Any]], + form_template: str | None, + thread_sensitive: bool, + top_children: Sequence, + bottom_children: Sequence, +): + from reactpy_django import config + + uuid_ref = hooks.use_ref(uuid4().hex.replace("-", "")) + top_children_count = hooks.use_ref(len(top_children)) + bottom_children_count = hooks.use_ref(len(bottom_children)) + submitted_data, set_submitted_data = hooks.use_state({} or None) + rendered_form, set_rendered_form = hooks.use_state(cast(Union[str, None], None)) + uuid = uuid_ref.current + + # Validate the provided arguments + if len(top_children) != top_children_count.current or len(bottom_children) != bottom_children_count.current: + msg = "Dynamically changing the number of top or bottom children is not allowed." + raise ValueError(msg) + if not isinstance(form, (type(Form), type(ModelForm))): + msg = ( + "The provided form must be an uninitialized Django Form. " + "Do NOT initialize your form by calling it (ex. `MyForm()`)." + ) + raise TypeError(msg) + + # Initialize the form with the provided data + initialized_form = form(data=submitted_data) + form_event = FormEventData( + form=initialized_form, submitted_data=submitted_data or {}, set_submitted_data=set_submitted_data + ) + + # Validate and render the form + @hooks.use_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: + await database_sync_to_async(initialized_form.full_clean)() + success = not initialized_form.errors.as_data() + if success and on_success: + await ensure_async(on_success, thread_sensitive=thread_sensitive)(form_event) + if not success and on_error: + await ensure_async(on_error, thread_sensitive=thread_sensitive)(form_event) + if success and auto_save and isinstance(initialized_form, ModelForm): + await ensure_async(initialized_form.save)() + set_submitted_data(None) + + set_rendered_form( + await ensure_async(initialized_form.render)(form_template or config.REACTPY_DEFAULT_FORM_TEMPLATE) + ) + + async def on_submit_callback(new_data: dict[str, Any]): + """Callback function provided directly to the client side listener. This is responsible for transmitting + the submitted form data to the server for processing.""" + convert_multiple_choice_fields(new_data, initialized_form) + convert_boolean_fields(new_data, initialized_form) + + if on_receive_data: + new_form_event = FormEventData( + form=initialized_form, submitted_data=new_data, set_submitted_data=set_submitted_data + ) + await ensure_async(on_receive_data, thread_sensitive=thread_sensitive)(new_form_event) + + if submitted_data != new_data: + set_submitted_data(new_data) + + async def _on_change(_event): + """Event that exist solely to allow the user to detect form changes.""" + if on_change: + await ensure_async(on_change, thread_sensitive=thread_sensitive)(form_event) + + if not rendered_form: + return None + + return html.form( + extra_props + | { + "id": f"reactpy-{uuid}", + # Intercept the form submission to prevent the browser from navigating + "onSubmit": event(lambda _: None, prevent_default=True), + "onChange": _on_change, + }, + DjangoForm({"onSubmitCallback": on_submit_callback, "formId": f"reactpy-{uuid}"}), + *top_children, + utils.html_to_vdom( + rendered_form, + convert_html_props_to_reactjs, + convert_textarea_children_to_prop, + set_value_prop_on_select_element, + transform_value_prop_on_input_element, + intercept_anchor_links, + infer_key_from_attributes, + *extra_transforms, + strict=False, + ), + *bottom_children, + ) diff --git a/src/reactpy_django/forms/transforms.py b/src/reactpy_django/forms/transforms.py new file mode 100644 index 00000000..2d527209 --- /dev/null +++ b/src/reactpy_django/forms/transforms.py @@ -0,0 +1,486 @@ +# TODO: Almost everything in this module should be moved to `reactpy.utils._mutate_vdom()`. +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from reactpy.core.events import EventHandler, to_event_handler_function + +if TYPE_CHECKING: + from reactpy.core.types import VdomDict + + +def convert_html_props_to_reactjs(vdom_tree: VdomDict) -> VdomDict: + """Transformation that standardizes the prop names to be used in the component.""" + # On each node, replace the 'attributes' key names with the standardized names. + if "attributes" in vdom_tree: + vdom_tree["attributes"] = {_normalize_prop_name(k): v for k, v in vdom_tree["attributes"].items()} + + return vdom_tree + + +def convert_textarea_children_to_prop(vdom_tree: VdomDict) -> VdomDict: + """Transformation that converts the text content of a