From e7def604d5863d0a1fa1b7b742334ab347d136ad Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 03:47:08 -0700
Subject: [PATCH 01/24] host URL configuration
---
CHANGELOG.md | 9 ++-
docs/python/configure-asgi-middleware.py | 4 +-
docs/python/configure-asgi.py | 4 +-
docs/python/settings.py | 9 ++-
docs/src/get-started/installation.md | 12 +++-
src/js/src/index.js | 54 +++++++++++----
src/reactpy_django/__init__.py | 6 +-
src/reactpy_django/checks.py | 66 +++++++++++++------
src/reactpy_django/config.py | 11 +++-
.../templates/reactpy/component.html | 10 +--
src/reactpy_django/templatetags/reactpy.py | 31 ++++++---
src/reactpy_django/utils.py | 10 +--
src/reactpy_django/websocket/paths.py | 12 ++--
tests/test_app/asgi.py | 7 +-
14 files changed, 170 insertions(+), 75 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f8f06d0a..9f794f68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,7 +34,14 @@ Using the following categories, list your changes in this order:
## [Unreleased]
-- Nothing (yet)!
+### Added
+
+- The `component` template tag now accepts `ws_host` and `http_host` arguments to override the default hostnames.
+
+### Deprecated
+
+- `reactpy_django.REACTPY_WEBSOCKET_PATH` is deprecated. Replace with `REACTPY_WEBSOCKET_ROUTE`.
+- `settings.py:REACTPY_WEBSOCKET_URL` is deprecated. Replace with `REACTPY_URL_PREFIX`.
## [3.3.2] - 2023-08-13
diff --git a/docs/python/configure-asgi-middleware.py b/docs/python/configure-asgi-middleware.py
index 3e8e6523..e817ee48 100644
--- a/docs/python/configure-asgi-middleware.py
+++ b/docs/python/configure-asgi-middleware.py
@@ -1,6 +1,6 @@
# Broken load order, only used for linting
from channels.routing import ProtocolTypeRouter, URLRouter
-from reactpy_django import REACTPY_WEBSOCKET_PATH
+from reactpy_django import REACTPY_WEBSOCKET_ROUTE
django_asgi_app = ""
@@ -15,7 +15,7 @@
"websocket": SessionMiddlewareStack(
AuthMiddlewareStack(
URLRouter(
- [REACTPY_WEBSOCKET_PATH],
+ [REACTPY_WEBSOCKET_ROUTE],
)
)
),
diff --git a/docs/python/configure-asgi.py b/docs/python/configure-asgi.py
index b574c684..8081d747 100644
--- a/docs/python/configure-asgi.py
+++ b/docs/python/configure-asgi.py
@@ -10,11 +10,11 @@
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
-from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402
+from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402
application = ProtocolTypeRouter(
{
"http": django_asgi_app,
- "websocket": URLRouter([REACTPY_WEBSOCKET_PATH]),
+ "websocket": URLRouter([REACTPY_WEBSOCKET_ROUTE]),
}
)
diff --git a/docs/python/settings.py b/docs/python/settings.py
index c9a26f5a..ea991de0 100644
--- a/docs/python/settings.py
+++ b/docs/python/settings.py
@@ -5,7 +5,7 @@
# Database ReactPy uses to store session data.
# ReactPy requires a multiprocessing-safe and thread-safe database.
-# DATABASE_ROUTERS is mandatory if REACTPY_DATABASE is configured.
+# Configuring Django's DATABASE_ROUTERS setting is mandatory if using REACTPY_DATABASE.
REACTPY_DATABASE = "default"
DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]
@@ -13,11 +13,10 @@
# Use `0` to prevent component reconnection.
REACTPY_RECONNECT_MAX = 259200
-# The URL for ReactPy to serve the component rendering websocket.
-REACTPY_WEBSOCKET_URL = "reactpy/"
+# The prefix to be used for all ReactPy API URLs.
+REACTPY_URL_PREFIX = "reactpy/"
-# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function,
-# or `None`.
+# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function.
REACTPY_DEFAULT_QUERY_POSTPROCESSOR = "reactpy_django.utils.django_query_postprocessor"
# Dotted path to the Django authentication backend to use for ReactPy components.
diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md
index d9763992..130dcb69 100644
--- a/docs/src/get-started/installation.md
+++ b/docs/src/get-started/installation.md
@@ -44,7 +44,7 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt
??? note "Configure ReactPy settings (Optional)"
- Below are a handful of values you can change within `settings.py` to modify the behavior of ReactPy.
+ Below are the default values for all configurable ReactPy settings within your `settings.py`.
```python linenums="0"
{% include "../../python/settings.py" %}
@@ -62,7 +62,7 @@ Add ReactPy HTTP paths to your `urlpatterns`.
## Step 4: Configure [`asgi.py`](https://docs.djangoproject.com/en/dev/howto/deployment/asgi/)
-Register ReactPy's Websocket using `REACTPY_WEBSOCKET_PATH`.
+Register ReactPy's Websocket using `REACTPY_WEBSOCKET_ROUTE`.
=== "asgi.py"
@@ -95,3 +95,11 @@ Run Django's database migrations to initialize ReactPy-Django's database table.
```bash linenums="0"
python manage.py migrate
```
+
+## Step 6: Check your configuration
+
+Run Django's check command to verify if ReactPy was set up correctly.
+
+```bash linenums="0"
+python manage.py check
+```
diff --git a/src/js/src/index.js b/src/js/src/index.js
index 64684d7a..0b9790d2 100644
--- a/src/js/src/index.js
+++ b/src/js/src/index.js
@@ -1,34 +1,60 @@
import { mountLayoutWithWebSocket } from "@reactpy/client";
// Set up a websocket at the base endpoint
-const LOCATION = window.location;
+let HTTP_PROTOCOL = window.location.protocol;
let WS_PROTOCOL = "";
-if (LOCATION.protocol == "https:") {
- WS_PROTOCOL = "wss://";
+if (HTTP_PROTOCOL == "https:") {
+ WS_PROTOCOL = "wss:";
} else {
- WS_PROTOCOL = "ws://";
+ WS_PROTOCOL = "ws:";
}
-const WS_ENDPOINT_URL = WS_PROTOCOL + LOCATION.host + "/";
export function mountViewToElement(
mountElement,
- reactpyWebsocketUrl,
- reactpyWebModulesUrl,
- maxReconnectTimeout,
- componentPath
+ reactpyWsHost,
+ reactpyHttpHost,
+ reactpyUrlPrefix,
+ reactpyReconnectMax,
+ reactpyComponentPath,
+ reactpyResolvedWebModulesPath
) {
- const WS_URL = WS_ENDPOINT_URL + reactpyWebsocketUrl + componentPath;
- const WEB_MODULE_URL = LOCATION.origin + "/" + reactpyWebModulesUrl;
+ // Determine the Websocket route
+ let wsOrigin;
+ if (reactpyWsHost) {
+ wsOrigin = `${WS_PROTOCOL}//${reactpyWsHost}`;
+ } else {
+ wsOrigin = `${WS_PROTOCOL}//${window.location.host}`;
+ }
+ const websocketUrl = `${wsOrigin}/${reactpyUrlPrefix}/${reactpyComponentPath}`;
+
+ // Determine the HTTP route
+ let httpOrigin;
+ let webModulesPath;
+ if (reactpyHttpHost) {
+ httpOrigin = `${HTTP_PROTOCOL}//${reactpyHttpHost}`;
+ webModulesPath = `${reactpyUrlPrefix}/web_module`;
+ } else {
+ httpOrigin = `${HTTP_PROTOCOL}//${window.location.host}`;
+ if (reactpyResolvedWebModulesPath) {
+ webModulesPath = reactpyResolvedWebModulesPath;
+ } else {
+ webModulesPath = `${reactpyUrlPrefix}/web_module`;
+ }
+ }
+ const webModuleUrl = `${httpOrigin}/${webModulesPath}`;
+
+ // Function that loads the JavaScript web module, if needed
const loadImportSource = (source, sourceType) => {
return import(
- sourceType == "NAME" ? `${WEB_MODULE_URL}${source}` : source
+ sourceType == "NAME" ? `${webModuleUrl}/${source}` : source
);
};
+ // Start rendering the component
mountLayoutWithWebSocket(
mountElement,
- WS_URL,
+ websocketUrl,
loadImportSource,
- maxReconnectTimeout
+ reactpyReconnectMax
);
}
diff --git a/src/reactpy_django/__init__.py b/src/reactpy_django/__init__.py
index fc3940e8..cfbbae80 100644
--- a/src/reactpy_django/__init__.py
+++ b/src/reactpy_django/__init__.py
@@ -3,11 +3,15 @@
import nest_asyncio
from reactpy_django import checks, components, decorators, hooks, types, utils
-from reactpy_django.websocket.paths import REACTPY_WEBSOCKET_PATH
+from reactpy_django.websocket.paths import (
+ REACTPY_WEBSOCKET_PATH,
+ REACTPY_WEBSOCKET_ROUTE,
+)
__version__ = "3.3.2"
__all__ = [
"REACTPY_WEBSOCKET_PATH",
+ "REACTPY_WEBSOCKET_ROUTE",
"hooks",
"components",
"decorators",
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index fc5a89a6..6979e2ef 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -1,8 +1,10 @@
+import contextlib
import sys
from django.contrib.staticfiles.finders import find
from django.core.checks import Error, Tags, Warning, register
from django.template import loader
+from django.urls import NoReverseMatch
@register(Tags.compatibility)
@@ -10,6 +12,7 @@ def reactpy_warnings(app_configs, **kwargs):
from django.conf import settings
from django.urls import reverse
+ from reactpy_django import config
from reactpy_django.config import REACTPY_FAILED_COMPONENTS
warnings = []
@@ -40,7 +43,8 @@ def reactpy_warnings(app_configs, **kwargs):
Warning(
"ReactPy URLs have not been registered.",
hint="""Add 'path("reactpy/", include("reactpy_django.http.urls"))' """
- "to your application's urlpatterns.",
+ "to your application's urlpatterns. If this application does not need "
+ "to render ReactPy components, you add this warning to SILENCED_SYSTEM_CHECKS.",
id="reactpy_django.W002",
)
)
@@ -96,28 +100,50 @@ def reactpy_warnings(app_configs, **kwargs):
)
)
- # Check if REACTPY_WEBSOCKET_URL doesn't end with a slash
- REACTPY_WEBSOCKET_URL = getattr(settings, "REACTPY_WEBSOCKET_URL", "reactpy/")
- if isinstance(REACTPY_WEBSOCKET_URL, str):
- if not REACTPY_WEBSOCKET_URL or not REACTPY_WEBSOCKET_URL.endswith("/"):
- warnings.append(
- Warning(
- "REACTPY_WEBSOCKET_URL did not end with a forward slash.",
- hint="Change your URL to be written in the following format: 'example_url/'",
- id="reactpy_django.W007",
- )
+ # DELETED W007: Check if REACTPY_WEBSOCKET_URL doesn't end with a slash
+ # DELETED W008: Check if REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character
+
+ # Removed Settings
+ if getattr(settings, "REACTPY_WEBSOCKET_URL", None):
+ warnings.append(
+ Warning(
+ "REACTPY_WEBSOCKET_URL has been removed.",
+ hint="Use REACTPY_URL_PREFIX instead.",
+ id="reactpy_django.W009",
)
+ )
- # Check if REACTPY_WEBSOCKET_URL doesn't start with an alphanumeric character
- if not REACTPY_WEBSOCKET_URL or not REACTPY_WEBSOCKET_URL[0].isalnum():
+ # 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("/")}'
+ if reactpy_http_prefix != config.REACTPY_URL_PREFIX:
warnings.append(
Warning(
- "REACTPY_WEBSOCKET_URL did not start with an alphanumeric character.",
- hint="Change your URL to be written in the following format: 'example_url/'",
- id="reactpy_django.W008",
+ "HTTP paths are not prefixed with REACTPY_URL_PREFIX. "
+ "Some ReactPy features may not work as expected.",
+ hint="Use one of the following solutions.\n"
+ "\t1) Utilize REACTPY_URL_PREFIX within your urls.py:\n"
+ f'\t path("{config.REACTPY_URL_PREFIX}/", include("reactpy_django.http.urls"))\n'
+ "\t2) Modify settings.py:REACTPY_URL_PREFIX to match your existing HTTP path:\n"
+ f'\t REACTPY_URL_PREFIX = "{reactpy_http_prefix}/"\n'
+ "\t3) If you not rendering ReactPy components within this Python application, then "
+ "remove HTTP and/or websocket routing.\n",
+ id="reactpy_django.W010",
)
)
+ if not getattr(settings, "REACTPY_URL_PREFIX", "reactpy/"):
+ warnings.append(
+ Warning(
+ "REACTPY_URL_PREFIX should not be empty!",
+ hint="Change your REACTPY_URL_PREFIX to be written in the following format: '/example_url/'",
+ id="reactpy_django.W011",
+ )
+ )
+
return warnings
@@ -154,12 +180,12 @@ def reactpy_errors(app_configs, **kwargs):
)
# All settings in reactpy_django.conf are the correct data type
- if not isinstance(getattr(settings, "REACTPY_WEBSOCKET_URL", ""), str):
+ if not isinstance(getattr(settings, "REACTPY_URL_PREFIX", ""), str):
errors.append(
Error(
- "Invalid type for REACTPY_WEBSOCKET_URL.",
- hint="REACTPY_WEBSOCKET_URL should be a string.",
- obj=settings.REACTPY_WEBSOCKET_URL,
+ "Invalid type for REACTPY_URL_PREFIX.",
+ hint="REACTPY_URL_PREFIX should be a string.",
+ obj=settings.REACTPY_URL_PREFIX,
id="reactpy_django.E003",
)
)
diff --git a/src/reactpy_django/config.py b/src/reactpy_django/config.py
index c0ee14be..c8f2a781 100644
--- a/src/reactpy_django/config.py
+++ b/src/reactpy_django/config.py
@@ -20,13 +20,20 @@
REACTPY_VIEW_COMPONENT_IFRAMES: dict[str, ViewComponentIframe] = {}
-# Configurable through Django settings.py
+# Remove in a future release
REACTPY_WEBSOCKET_URL = getattr(
settings,
"REACTPY_WEBSOCKET_URL",
"reactpy/",
)
-REACTPY_RECONNECT_MAX = getattr(
+
+# Configurable through Django settings.py
+REACTPY_URL_PREFIX: str = getattr(
+ settings,
+ "REACTPY_URL_PREFIX",
+ REACTPY_WEBSOCKET_URL,
+).strip("/")
+REACTPY_RECONNECT_MAX: int = getattr(
settings,
"REACTPY_RECONNECT_MAX",
259200, # Default to 3 days
diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html
index 7dae08eb..de1f9922 100644
--- a/src/reactpy_django/templates/reactpy/component.html
+++ b/src/reactpy_django/templates/reactpy/component.html
@@ -4,16 +4,18 @@
{% firstof reactpy_error "UnknownError" %}: "{% firstof reactpy_dotted_path "UnknownPath" %}"
{% endif %}
{% else %}
-
+
{% endif %}
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index d8e94e2b..b083073d 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -3,13 +3,13 @@
import dill as pickle
from django import template
-from django.urls import reverse
+from django.urls import NoReverseMatch, reverse
from reactpy_django import models
from reactpy_django.config import (
REACTPY_DEBUG_MODE,
REACTPY_RECONNECT_MAX,
- REACTPY_WEBSOCKET_URL,
+ REACTPY_URL_PREFIX,
)
from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError
from reactpy_django.types import ComponentParamData
@@ -19,13 +19,18 @@
func_has_args,
)
-REACTPY_WEB_MODULES_URL = reverse("reactpy:web_modules", args=["x"])[:-1][1:]
+try:
+ RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/")
+except NoReverseMatch:
+ RESOLVED_WEB_MODULES_PATH = ""
register = template.Library()
_logger = getLogger(__name__)
@register.inclusion_tag("reactpy/component.html")
-def component(dotted_path: str, *args, **kwargs):
+def component(
+ dotted_path: str, *args, ws_host: str = "", http_host: str = "", **kwargs
+):
"""This tag is used to embed an existing ReactPy component into your HTML template.
Args:
@@ -33,6 +38,14 @@ def component(dotted_path: str, *args, **kwargs):
*args: The positional arguments to provide to the component.
Keyword Args:
+ ws_host: The host domain to use for the ReactPy websocket connection. If set to None, \
+ the host will be fetched via JavaScript. \
+ Note: You typically will not need to register the ReactPy websocket path on any \
+ application(s) that do not perform component rendering.
+ http_host: The host domain to use for the ReactPy HTTP connection. If set to None, \
+ the host will be fetched via JavaScript. \
+ Note: You typically will not need to register ReactPy HTTP paths on any \
+ application(s) that do not perform component rendering.
**kwargs: The keyword arguments to provide to the component.
Example ::
@@ -85,12 +98,14 @@ def component(dotted_path: str, *args, **kwargs):
# Return the template rendering context
return {
- "class": class_,
- "reactpy_websocket_url": REACTPY_WEBSOCKET_URL,
- "reactpy_web_modules_url": REACTPY_WEB_MODULES_URL,
+ "reactpy_class": class_,
+ "reactpy_uuid": uuid,
+ "reactpy_ws_host": ws_host.strip("/"),
+ "reactpy_http_host": http_host.strip("/"),
+ "reactpy_url_prefix": REACTPY_URL_PREFIX,
"reactpy_reconnect_max": REACTPY_RECONNECT_MAX,
- "reactpy_mount_uuid": uuid,
"reactpy_component_path": f"{dotted_path}/{uuid}/",
+ "reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
}
diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py
index f4e0f8e6..57e9e2a9 100644
--- a/src/reactpy_django/utils.py
+++ b/src/reactpy_django/utils.py
@@ -101,7 +101,6 @@ def _register_component(dotted_path: str) -> Callable:
raise ComponentDoesNotExistError(
f"Error while fetching '{dotted_path}'. {(str(e).capitalize())}."
) from e
- _logger.debug("ReactPy has registered component %s", dotted_path)
return REACTPY_REGISTERED_COMPONENTS[dotted_path]
@@ -204,16 +203,19 @@ def get_components(self, templates: set[str]) -> set[str]:
def register_components(self, components: set[str]) -> None:
"""Registers all ReactPy components in an iterable."""
+ if components:
+ _logger.debug("ReactPy root components:")
for component in components:
try:
- _logger.info("ReactPy preloader has detected component %s", component)
+ _logger.debug("\t+ %s", component)
_register_component(component)
except Exception:
_logger.exception(
"\033[91m"
- "ReactPy failed to register component '%s'! "
+ "ReactPy failed to register component '%s'!\n"
"This component path may not be valid, "
- "or an exception may have occurred while importing."
+ "or an exception may have occurred while importing.\n"
+ "See the traceback below for more information."
"\033[0m",
component,
)
diff --git a/src/reactpy_django/websocket/paths.py b/src/reactpy_django/websocket/paths.py
index afd410c3..039ee5ba 100644
--- a/src/reactpy_django/websocket/paths.py
+++ b/src/reactpy_django/websocket/paths.py
@@ -1,15 +1,17 @@
from django.urls import path
-from reactpy_django.config import REACTPY_WEBSOCKET_URL
+from reactpy_django.config import REACTPY_URL_PREFIX
from .consumer import ReactpyAsyncWebsocketConsumer
-REACTPY_WEBSOCKET_PATH = path(
- f"{REACTPY_WEBSOCKET_URL}//",
+REACTPY_WEBSOCKET_ROUTE = path(
+ f"{REACTPY_URL_PREFIX}///",
ReactpyAsyncWebsocketConsumer.as_asgi(),
)
-
"""A URL path for :class:`ReactpyAsyncWebsocketConsumer`.
-Required in order for ReactPy to know the websocket path.
+Required since the `reverse()` function does not exist for Django Channels, but we need
+to know the websocket path.
"""
+
+REACTPY_WEBSOCKET_PATH = REACTPY_WEBSOCKET_ROUTE
diff --git a/tests/test_app/asgi.py b/tests/test_app/asgi.py
index b49d12ef..e372e42f 100644
--- a/tests/test_app/asgi.py
+++ b/tests/test_app/asgi.py
@@ -11,7 +11,6 @@
from django.core.asgi import get_asgi_application
-
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_app.settings")
# Fetch ASGI application before importing dependencies that require ORM models.
@@ -20,15 +19,13 @@
from channels.auth import AuthMiddlewareStack # noqa: E402
from channels.routing import ProtocolTypeRouter, URLRouter # noqa: E402
from channels.sessions import SessionMiddlewareStack # noqa: E402
-
-from reactpy_django import REACTPY_WEBSOCKET_PATH # noqa: E402
-
+from reactpy_django import REACTPY_WEBSOCKET_ROUTE # noqa: E402
application = ProtocolTypeRouter(
{
"http": http_asgi_app,
"websocket": SessionMiddlewareStack(
- AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_PATH]))
+ AuthMiddlewareStack(URLRouter([REACTPY_WEBSOCKET_ROUTE]))
),
}
)
From 9b2e08ef3baff16c50d8e25d9c6b8d215f44bf44 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 04:33:24 -0700
Subject: [PATCH 02/24] add all venvs to gitignore
---
.gitignore | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index a59a51e4..07d4c0cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,8 +89,8 @@ celerybeat-schedule.*
*.sage.py
# Environments
-.env
-.venv
+.env*/
+.venv*/
env/
venv/
ENV/
From af9b2d8b9d63fd0373d6539c1e92e7b02b856af8 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 04:41:58 -0700
Subject: [PATCH 03/24] more broad daphne warning
---
src/reactpy_django/checks.py | 17 +++++++++++------
tests/test_app/settings.py | 2 +-
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index 6979e2ef..1e7fe7f0 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -49,17 +49,22 @@ def reactpy_warnings(app_configs, **kwargs):
)
)
- # Warn if REACTPY_BACKHAUL_THREAD is set to True on Linux with Daphne
+ # Warn if REACTPY_BACKHAUL_THREAD is set to True with Daphne
if (
sys.argv
- and sys.argv[0].endswith("daphne")
+ and (
+ sys.argv[0].endswith("daphne")
+ or (
+ "runserver" in sys.argv
+ and "daphne" in getattr(settings, "INSTALLED_APPS", [])
+ )
+ )
and getattr(settings, "REACTPY_BACKHAUL_THREAD", False)
- and sys.platform == "linux"
):
warnings.append(
Warning(
- "REACTPY_BACKHAUL_THREAD is enabled but you running with Daphne on Linux. "
- "This configuration is known to be unstable.",
+ "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 webserver.",
id="reactpy_django.W003",
)
@@ -238,7 +243,7 @@ def reactpy_errors(app_configs, **kwargs):
)
# Check for dependencies
- if "channels" not in settings.INSTALLED_APPS:
+ if "channels" not in getattr(settings, "INSTALLED_APPS", []):
errors.append(
Error(
"Django Channels is not installed.",
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 10cd8f6b..8e7fc08f 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -180,4 +180,4 @@
# ReactPy Django Settings
REACTPY_AUTH_BACKEND = "django.contrib.auth.backends.ModelBackend"
-REACTPY_BACKHAUL_THREAD = "test" not in sys.argv
+REACTPY_BACKHAUL_THREAD = "test" not in sys.argv and "runserver" not in sys.argv
From 017be37377165ca67b41b6554e4b26008820c168 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 19:42:55 -0700
Subject: [PATCH 04/24] add changelog
---
CHANGELOG.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f794f68..f2adb559 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,11 @@ Using the following categories, list your changes in this order:
### Added
-- The `component` template tag now accepts `ws_host` and `http_host` arguments to override the default hostnames.
+- The `component` template tag now accepts `ws_host` and `http_host` arguments to override the default host URL, allowing ReactPy websockets and HTTP to be hosted by a completely separate Django application(s).
+
+### Changed
+
+- ReactPy will now provide a warning if HTTP URLs are not using the same URL prefix as websockets.
### Deprecated
From fa547fe20ba49a7b965175108378be19a1960b7f Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 19:43:13 -0700
Subject: [PATCH 05/24] update checks
---
src/reactpy_django/checks.py | 37 ++++++++++++++++++------------------
1 file changed, 19 insertions(+), 18 deletions(-)
diff --git a/src/reactpy_django/checks.py b/src/reactpy_django/checks.py
index 1e7fe7f0..4ee947e2 100644
--- a/src/reactpy_django/checks.py
+++ b/src/reactpy_django/checks.py
@@ -51,16 +51,12 @@ def reactpy_warnings(app_configs, **kwargs):
# Warn if REACTPY_BACKHAUL_THREAD is set to True with Daphne
if (
- sys.argv
- and (
- sys.argv[0].endswith("daphne")
- or (
- "runserver" in sys.argv
- and "daphne" in getattr(settings, "INSTALLED_APPS", [])
- )
+ sys.argv[0].endswith("daphne")
+ or (
+ "runserver" in sys.argv
+ and "daphne" in getattr(settings, "INSTALLED_APPS", [])
)
- and getattr(settings, "REACTPY_BACKHAUL_THREAD", False)
- ):
+ ) and getattr(settings, "REACTPY_BACKHAUL_THREAD", False):
warnings.append(
Warning(
"Unstable configuration detected. REACTPY_BACKHAUL_THREAD is enabled "
@@ -140,6 +136,7 @@ def reactpy_warnings(app_configs, **kwargs):
)
)
+ # Check if REACTPY_URL_PREFIX is empty
if not getattr(settings, "REACTPY_URL_PREFIX", "reactpy/"):
warnings.append(
Warning(
@@ -149,6 +146,18 @@ 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", []
+ ):
+ warnings.append(
+ Warning(
+ "You have not configured runserver to use ASGI.",
+ hint="Add daphne to settings.py:INSTALLED_APPS.",
+ id="reactpy_django.W012",
+ )
+ )
+
return warnings
@@ -242,14 +251,6 @@ def reactpy_errors(app_configs, **kwargs):
)
)
- # Check for dependencies
- if "channels" not in getattr(settings, "INSTALLED_APPS", []):
- errors.append(
- Error(
- "Django Channels is not installed.",
- hint="Add 'channels' to settings.py:INSTALLED_APPS.",
- id="reactpy_django.E009",
- )
- )
+ # DELETED E009: Check if `channels` is in INSTALLED_APPS
return errors
From 772c3ac2296abcfc932f2886eb1e1ee06cc6ccbe Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 20:12:21 -0700
Subject: [PATCH 06/24] change settings docs
---
docs/python/settings.py | 31 -------------------------
docs/src/features/settings.md | 18 ++++++++++----
docs/src/get-started/installation.md | 6 +----
tests/test_app/settings.py | 1 -
tests/test_app/tests/test_components.py | 2 --
5 files changed, 14 insertions(+), 44 deletions(-)
delete mode 100644 docs/python/settings.py
diff --git a/docs/python/settings.py b/docs/python/settings.py
deleted file mode 100644
index ea991de0..00000000
--- a/docs/python/settings.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Cache used to store ReactPy web modules.
-# ReactPy benefits from a fast, well indexed cache.
-# We recommend redis or python-diskcache.
-REACTPY_CACHE = "default"
-
-# Database ReactPy uses to store session data.
-# ReactPy requires a multiprocessing-safe and thread-safe database.
-# Configuring Django's DATABASE_ROUTERS setting is mandatory if using REACTPY_DATABASE.
-REACTPY_DATABASE = "default"
-DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]
-
-# Maximum seconds between reconnection attempts before giving up.
-# Use `0` to prevent component reconnection.
-REACTPY_RECONNECT_MAX = 259200
-
-# The prefix to be used for all ReactPy API URLs.
-REACTPY_URL_PREFIX = "reactpy/"
-
-# Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function.
-REACTPY_DEFAULT_QUERY_POSTPROCESSOR = "reactpy_django.utils.django_query_postprocessor"
-
-# Dotted path to the Django authentication backend to use for ReactPy components.
-# This is only needed if:
-# 1. You are using `AuthMiddlewareStack` and...
-# 2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
-# 3. Your Django user model does not define a `backend` attribute
-REACTPY_AUTH_BACKEND = "django.contrib.auth.backends.ModelBackend"
-
-# Whether to enable rendering ReactPy via a dedicated backhaul thread.
-# This allows the webserver to process traffic while during ReactPy rendering.
-REACTPY_BACKHAUL_THREAD = False
diff --git a/docs/src/features/settings.md b/docs/src/features/settings.md
index 3abd6575..26ca78b7 100644
--- a/docs/src/features/settings.md
+++ b/docs/src/features/settings.md
@@ -6,13 +6,21 @@
## Primary Configuration
-These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy.
+
-=== "settings.py"
+These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy.
- ```python
- {% include "../../python/settings.py" %}
- ```
+| Setting | Default Value | Example Value(s) | Description |
+| --- | --- | --- | --- |
+| `REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache.
We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
+| `REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database ReactPy uses to store session data. ReactPy requires a multiprocessing-safe and thread-safe database.
If configuring `REACTPY_DATABASE`, it is mandatory to also configure `DATABASE_ROUTERS` like such:
`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
+| `REACTPY_RECONNECT_MAX` | `#!python 259200` | `#!python 96000`, `#!python 60`, `#!python 0` | Maximum seconds between reconnection attempts before giving up.
Use `#!python 0` to prevent reconnection. |
+| `REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy websocket and HTTP URLs. |
+| `REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `reactpy_django.hooks.use_query` postprocessor function. |
+| `REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:
1. You are using `AuthMiddlewareStack` and...
2. You are using Django's `AUTHENTICATION_BACKENDS` setting and...
3. Your Django user model does not define a `backend` attribute. |
+| `REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the webserver to process web traffic while during ReactPy rendering.
Vastly improves throughput with web servers such as `hypercorn` and `uvicorn`. |
+
+
??? question "Do I need to modify my settings?"
diff --git a/docs/src/get-started/installation.md b/docs/src/get-started/installation.md
index 130dcb69..bd368ec1 100644
--- a/docs/src/get-started/installation.md
+++ b/docs/src/get-started/installation.md
@@ -44,11 +44,7 @@ In your settings you will need to add `reactpy_django` to [`INSTALLED_APPS`](htt
??? note "Configure ReactPy settings (Optional)"
- Below are the default values for all configurable ReactPy settings within your `settings.py`.
-
- ```python linenums="0"
- {% include "../../python/settings.py" %}
- ```
+ {% include "../features/settings.md" start="" end="" %}
## Step 3: Configure [`urls.py`](https://docs.djangoproject.com/en/dev/topics/http/urls/)
diff --git a/tests/test_app/settings.py b/tests/test_app/settings.py
index 8e7fc08f..be9a4186 100644
--- a/tests/test_app/settings.py
+++ b/tests/test_app/settings.py
@@ -40,7 +40,6 @@
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
- "channels", # Websocket library
"reactpy_django", # Django compatiblity layer for ReactPy
"test_app", # This test application
]
diff --git a/tests/test_app/tests/test_components.py b/tests/test_app/tests/test_components.py
index 0801e28e..fc71091a 100644
--- a/tests/test_app/tests/test_components.py
+++ b/tests/test_app/tests/test_components.py
@@ -10,10 +10,8 @@
from django.db import connections
from django.test.utils import modify_settings
from playwright.sync_api import TimeoutError, sync_playwright
-
from reactpy_django.models import ComponentSession
-
CLICK_DELAY = 250 if os.getenv("GITHUB_ACTIONS") else 25 # Delay in miliseconds.
From 11e42d625f34e923ab9ce260e07853775fdf1f24 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 22:31:28 -0700
Subject: [PATCH 07/24] better func_has_args
---
src/reactpy_django/utils.py | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/reactpy_django/utils.py b/src/reactpy_django/utils.py
index 57e9e2a9..40135a2d 100644
--- a/src/reactpy_django/utils.py
+++ b/src/reactpy_django/utils.py
@@ -299,11 +299,8 @@ def django_query_postprocessor(
def func_has_args(func: Callable) -> bool:
- """Checks if a function has any args or kwarg."""
- signature = inspect.signature(func)
-
- # Check if the function has any args/kwargs
- return str(signature) != "()"
+ """Checks if a function has any args or kwargs."""
+ return bool(inspect.signature(func).parameters)
def check_component_args(func: Callable, *args, **kwargs):
From 078a100af42126b9ee03e8128457552f9eb68d95 Mon Sep 17 00:00:00 2001
From: Archmonger <16909269+Archmonger@users.noreply.github.com>
Date: Wed, 16 Aug 2023 22:46:47 -0700
Subject: [PATCH 08/24] merged everything into one `host_domain` arg
---
CHANGELOG.md | 2 +-
docs/src/features/template-tag.md | 2 +
src/js/src/index.js | 11 +-
.../templates/reactpy/component.html | 3 +-
src/reactpy_django/templatetags/reactpy.py | 105 ++++++++++--------
src/reactpy_django/utils.py | 8 +-
6 files changed, 68 insertions(+), 63 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2adb559..edcc2790 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,7 +36,7 @@ Using the following categories, list your changes in this order:
### Added
-- The `component` template tag now accepts `ws_host` and `http_host` arguments to override the default host URL, allowing ReactPy websockets and HTTP to be hosted by a completely separate Django application(s).
+- The `component` template tag now accepts a `host_domain` argument to override the default host URL, allowing ReactPy websockets and HTTP to be hosted by completely separate Django application(s).
### Changed
diff --git a/docs/src/features/template-tag.md b/docs/src/features/template-tag.md
index 9d4ca18c..4c1f47be 100644
--- a/docs/src/features/template-tag.md
+++ b/docs/src/features/template-tag.md
@@ -20,6 +20,8 @@ The `component` template tag can be used to insert any number of ReactPy compone
| --- | --- | --- | --- |
| `dotted_path` | `str` | The dotted path to the component to render. | N/A |
| `*args` | `Any` | The positional arguments to provide to the component. | N/A |
+ | `host_domain` | `str | None` | The host domain to use for the ReactPy connections. If set to `None`, the host will be automatically configured.
_Note: You typically will not need to register ReactPy HTTP and/or websocket paths on any application(s) that do not perform any component rendering._ | `None` |
+
| `**kwargs` | `Any` | The keyword arguments to provide to the component. | N/A |
**Returns**
diff --git a/src/js/src/index.js b/src/js/src/index.js
index 0b9790d2..326e8da1 100644
--- a/src/js/src/index.js
+++ b/src/js/src/index.js
@@ -11,8 +11,7 @@ if (HTTP_PROTOCOL == "https:") {
export function mountViewToElement(
mountElement,
- reactpyWsHost,
- reactpyHttpHost,
+ reactpyHostDomain,
reactpyUrlPrefix,
reactpyReconnectMax,
reactpyComponentPath,
@@ -20,8 +19,8 @@ export function mountViewToElement(
) {
// Determine the Websocket route
let wsOrigin;
- if (reactpyWsHost) {
- wsOrigin = `${WS_PROTOCOL}//${reactpyWsHost}`;
+ if (reactpyHostDomain) {
+ wsOrigin = `${WS_PROTOCOL}//${reactpyHostDomain}`;
} else {
wsOrigin = `${WS_PROTOCOL}//${window.location.host}`;
}
@@ -30,8 +29,8 @@ export function mountViewToElement(
// Determine the HTTP route
let httpOrigin;
let webModulesPath;
- if (reactpyHttpHost) {
- httpOrigin = `${HTTP_PROTOCOL}//${reactpyHttpHost}`;
+ if (reactpyHostDomain) {
+ httpOrigin = `${HTTP_PROTOCOL}//${reactpyHostDomain}`;
webModulesPath = `${reactpyUrlPrefix}/web_module`;
} else {
httpOrigin = `${HTTP_PROTOCOL}//${window.location.host}`;
diff --git a/src/reactpy_django/templates/reactpy/component.html b/src/reactpy_django/templates/reactpy/component.html
index de1f9922..9015f359 100644
--- a/src/reactpy_django/templates/reactpy/component.html
+++ b/src/reactpy_django/templates/reactpy/component.html
@@ -10,8 +10,7 @@
const mountElement = document.getElementById("{{ reactpy_uuid }}");
mountViewToElement(
mountElement,
- "{{ reactpy_ws_host }}",
- "{{ reactpy_http_host }}",
+ "{{ reactpy_host_domain }}",
"{{ reactpy_url_prefix }}",
"{{ reactpy_reconnect_max }}",
"{{ reactpy_component_path }}",
diff --git a/src/reactpy_django/templatetags/reactpy.py b/src/reactpy_django/templatetags/reactpy.py
index b083073d..64f4f7e4 100644
--- a/src/reactpy_django/templatetags/reactpy.py
+++ b/src/reactpy_django/templatetags/reactpy.py
@@ -3,6 +3,7 @@
import dill as pickle
from django import template
+from django.http import HttpRequest
from django.urls import NoReverseMatch, reverse
from reactpy_django import models
@@ -14,9 +15,9 @@
from reactpy_django.exceptions import ComponentDoesNotExistError, ComponentParamError
from reactpy_django.types import ComponentParamData
from reactpy_django.utils import (
- _register_component,
check_component_args,
func_has_args,
+ register_component,
)
try:
@@ -27,9 +28,13 @@
_logger = getLogger(__name__)
-@register.inclusion_tag("reactpy/component.html")
+@register.inclusion_tag("reactpy/component.html", takes_context=True)
def component(
- dotted_path: str, *args, ws_host: str = "", http_host: str = "", **kwargs
+ context: template.RequestContext,
+ dotted_path: str,
+ *args,
+ host_domain: str | None = None,
+ **kwargs,
):
"""This tag is used to embed an existing ReactPy component into your HTML template.
@@ -38,14 +43,10 @@ def component(
*args: The positional arguments to provide to the component.
Keyword Args:
- ws_host: The host domain to use for the ReactPy websocket connection. If set to None, \
- the host will be fetched via JavaScript. \
- Note: You typically will not need to register the ReactPy websocket path on any \
- application(s) that do not perform component rendering.
- http_host: The host domain to use for the ReactPy HTTP connection. If set to None, \
- the host will be fetched via JavaScript. \
- Note: You typically will not need to register ReactPy HTTP paths on any \
- application(s) that do not perform component rendering.
+ host_domain: The host domain to use for the ReactPy connections. If set to `None`, \
+ the host will be automatically configured. \
+ Note: You typically will not need to register the ReactPy HTTP and/or websocket \
+ paths on any application(s) that do not perform component rendering.
**kwargs: The keyword arguments to provide to the component.
Example ::
@@ -59,49 +60,55 @@ def component(