Skip to content

Commit aae9c7a

Browse files
authored
Bump ReactPy, refactor template tag, and pretty WS URLs. (#174)
- Bumped the minimum ReactPy version to `1.0.2`. - Prettier websocket URLs for components that do not have sessions. - Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled.
1 parent daf1232 commit aae9c7a

File tree

6 files changed

+60
-50
lines changed

6 files changed

+60
-50
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ Using the following categories, list your changes in this order:
3434

3535
## [Unreleased]
3636

37-
- Nothing (yet)!
37+
### Changed
38+
39+
- Bumped the minimum ReactPy version to `1.0.2`.
40+
- Prettier websocket URLs for components that do not have sessions.
41+
- Template tag will now only validate `args`/`kwargs` if `settings.py:DEBUG` is enabled.
3842

3943
## [3.4.0] - 2023-08-18
4044

requirements/pkg-deps.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
channels >=4.0.0
22
django >=4.1.0
3-
reactpy >=1.0.0, <1.1.0
3+
reactpy >=1.0.2, <1.1.0
44
aiofile >=3.0
55
dill >=0.3.5
66
orjson >=3.6.0

src/reactpy_django/templatetags/reactpy.py

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django import template
88
from django.http import HttpRequest
99
from django.urls import NoReverseMatch, reverse
10+
from reactpy.core.types import ComponentConstructor
1011

1112
from reactpy_django import config, models
1213
from reactpy_django.exceptions import (
@@ -15,7 +16,7 @@
1516
InvalidHostError,
1617
)
1718
from reactpy_django.types import ComponentParamData
18-
from reactpy_django.utils import check_component_args, func_has_args
19+
from reactpy_django.utils import validate_component_args
1920

2021
try:
2122
RESOLVED_WEB_MODULES_PATH = reverse("reactpy:web_modules", args=["/"]).strip("/")
@@ -55,60 +56,52 @@ def component(
5556
</body>
5657
</html>
5758
"""
58-
59-
# Determine the host
6059
request: HttpRequest | None = context.get("request")
6160
perceived_host = (request.get_host() if request else "").strip("/")
6261
host = (
6362
host
6463
or (next(config.REACTPY_DEFAULT_HOSTS) if config.REACTPY_DEFAULT_HOSTS else "")
6564
).strip("/")
66-
67-
# Check if this this component needs to rendered by the current ASGI app
68-
use_current_app = not host or host.startswith(perceived_host)
69-
70-
# Create context variables
65+
is_local = not host or host.startswith(perceived_host)
7166
uuid = uuid4().hex
7267
class_ = kwargs.pop("class", "")
73-
kwargs.pop("key", "") # `key` is effectively useless for the root node
74-
75-
# Fail if user has a method in their host
76-
if host.find("://") != -1:
77-
protocol = host.split("://")[0]
78-
msg = (
79-
f"Invalid host provided to component. Contains a protocol '{protocol}://'."
80-
)
81-
_logger.error(msg)
82-
return failure_context(dotted_path, InvalidHostError(msg))
83-
84-
# Fetch the component if needed
85-
if use_current_app:
68+
kwargs.pop("key", "") # `key` is useless for the root node
69+
component_has_args = args or kwargs
70+
user_component: ComponentConstructor | None = None
71+
72+
# Validate the host
73+
if host and config.REACTPY_DEBUG_MODE:
74+
try:
75+
validate_host(host)
76+
except InvalidHostError as e:
77+
return failure_context(dotted_path, e)
78+
79+
# Fetch the component
80+
if is_local:
8681
user_component = config.REACTPY_REGISTERED_COMPONENTS.get(dotted_path)
8782
if not user_component:
8883
msg = f"Component '{dotted_path}' is not registered as a root component. "
8984
_logger.error(msg)
9085
return failure_context(dotted_path, ComponentDoesNotExistError(msg))
9186

92-
# Store the component's args/kwargs in the database, if needed
93-
# These will be fetched by the websocket consumer later
94-
try:
95-
if use_current_app:
96-
check_component_args(user_component, *args, **kwargs)
97-
if func_has_args(user_component):
98-
save_component_params(args, kwargs, uuid)
99-
# Can't guarantee args will match up if the component is rendered by a different app.
100-
# So, we just store any provided args/kwargs in the database.
101-
elif args or kwargs:
102-
save_component_params(args, kwargs, uuid)
103-
except Exception as e:
104-
if isinstance(e, ComponentParamError):
87+
# Validate the component
88+
if is_local and config.REACTPY_DEBUG_MODE:
89+
try:
90+
validate_component_args(user_component, *args, **kwargs)
91+
except ComponentParamError as e:
10592
_logger.error(str(e))
106-
else:
93+
return failure_context(dotted_path, e)
94+
95+
# Store args & kwargs in the database (fetched by our websocket later)
96+
if component_has_args:
97+
try:
98+
save_component_params(args, kwargs, uuid)
99+
except Exception as e:
107100
_logger.exception(
108101
"An unknown error has occurred while saving component params for '%s'.",
109102
dotted_path,
110103
)
111-
return failure_context(dotted_path, e)
104+
return failure_context(dotted_path, e)
112105

113106
# Return the template rendering context
114107
return {
@@ -117,7 +110,9 @@ def component(
117110
"reactpy_host": host or perceived_host,
118111
"reactpy_url_prefix": config.REACTPY_URL_PREFIX,
119112
"reactpy_reconnect_max": config.REACTPY_RECONNECT_MAX,
120-
"reactpy_component_path": f"{dotted_path}/{uuid}/",
113+
"reactpy_component_path": f"{dotted_path}/{uuid}/"
114+
if component_has_args
115+
else f"{dotted_path}/",
121116
"reactpy_resolved_web_modules_path": RESOLVED_WEB_MODULES_PATH,
122117
}
123118

@@ -136,3 +131,13 @@ def save_component_params(args, kwargs, uuid):
136131
model = models.ComponentSession(uuid=uuid, params=pickle.dumps(params))
137132
model.full_clean()
138133
model.save()
134+
135+
136+
def validate_host(host: str):
137+
if "://" in host:
138+
protocol = host.split("://")[0]
139+
msg = (
140+
f"Invalid host provided to component. Contains a protocol '{protocol}://'."
141+
)
142+
_logger.error(msg)
143+
raise InvalidHostError(msg)

src/reactpy_django/utils.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,12 +297,7 @@ def django_query_postprocessor(
297297
return data
298298

299299

300-
def func_has_args(func) -> bool:
301-
"""Checks if a function has any args or kwargs."""
302-
return bool(inspect.signature(func).parameters)
303-
304-
305-
def check_component_args(func, *args, **kwargs):
300+
def validate_component_args(func, *args, **kwargs):
306301
"""
307302
Validate whether a set of args/kwargs would work on the given function.
308303

src/reactpy_django/websocket/consumer.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from reactpy.core.serve import serve_layout
2323

2424
from reactpy_django.types import ComponentParamData, ComponentWebsocket
25-
from reactpy_django.utils import db_cleanup, func_has_args
25+
from reactpy_django.utils import db_cleanup
2626

2727
_logger = logging.getLogger(__name__)
2828
backhaul_loop = asyncio.new_event_loop()
@@ -124,7 +124,7 @@ async def run_dispatcher(self):
124124

125125
scope = self.scope
126126
dotted_path = scope["url_route"]["kwargs"]["dotted_path"]
127-
uuid = scope["url_route"]["kwargs"]["uuid"]
127+
uuid = scope["url_route"]["kwargs"].get("uuid")
128128
search = scope["query_string"].decode()
129129
self.recv_queue: asyncio.Queue = asyncio.Queue()
130130
connection = Connection( # For `use_connection`
@@ -151,7 +151,7 @@ async def run_dispatcher(self):
151151

152152
# Fetch the component's args/kwargs from the database, if needed
153153
try:
154-
if func_has_args(component_constructor):
154+
if uuid:
155155
# Always clean up expired entries first
156156
await database_sync_to_async(db_cleanup, thread_sensitive=False)()
157157

src/reactpy_django/websocket/paths.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
from channels.routing import URLRouter # noqa: E402
12
from django.urls import path
23

34
from reactpy_django.config import REACTPY_URL_PREFIX
45

56
from .consumer import ReactpyAsyncWebsocketConsumer
67

78
REACTPY_WEBSOCKET_ROUTE = path(
8-
f"{REACTPY_URL_PREFIX}/<dotted_path>/<uuid>/",
9-
ReactpyAsyncWebsocketConsumer.as_asgi(),
9+
f"{REACTPY_URL_PREFIX}/<dotted_path>/",
10+
URLRouter(
11+
[
12+
path("<uuid>/", ReactpyAsyncWebsocketConsumer.as_asgi()),
13+
path("", ReactpyAsyncWebsocketConsumer.as_asgi()),
14+
]
15+
),
1016
)
1117
"""A URL path for :class:`ReactpyAsyncWebsocketConsumer`.
1218

0 commit comments

Comments
 (0)