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,