Skip to content

Commit a83ce0c

Browse files
authored
add url and route_id to routerdata as a replacement for pagedata (#5516)
* add url and route_id to routerdata as a replacement for pagedata * best effort to deprecate `page` * use from_router_data * call dataclasses.fields once
1 parent 09ed66c commit a83ce0c

7 files changed

Lines changed: 176 additions & 94 deletions

File tree

reflex/.templates/web/utils/state.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,10 @@ export const applyEvent = async (event, socket, navigate, params) => {
370370
...Object.fromEntries(new URLSearchParams(window.location.search)),
371371
...params(),
372372
},
373-
asPath: window.location.pathname + window.location.search,
373+
asPath:
374+
window.location.pathname +
375+
window.location.search +
376+
window.location.hash,
374377
};
375378
}
376379

reflex/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1741,7 +1741,7 @@ async def process(
17411741
if (path := router_data.get(constants.RouteVar.PATH))
17421742
else "404"
17431743
).removeprefix("/")
1744-
state.router = RouterData(router_data)
1744+
state.router = RouterData.from_router_data(router_data)
17451745

17461746
# Preprocess the event.
17471747
update = await app._preprocess(state, event)

reflex/istate/data.py

Lines changed: 142 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import dataclasses
44
from collections.abc import Mapping
5+
from typing import TYPE_CHECKING
6+
from urllib.parse import _NetlocResultMixinStr, urlsplit
57

68
from reflex import constants
7-
from reflex.utils import format
9+
from reflex.utils import console, format
810
from reflex.utils.serializers import serializer
911

1012

@@ -45,36 +47,40 @@ class _HeaderData:
4547
)
4648

4749

48-
@dataclasses.dataclass(frozen=True, init=False)
50+
_HEADER_DATA_FIELDS = frozenset(
51+
[field.name for field in dataclasses.fields(_HeaderData)]
52+
)
53+
54+
55+
@dataclasses.dataclass(frozen=True)
4956
class HeaderData(_HeaderData):
5057
"""An object containing headers data."""
5158

52-
def __init__(self, router_data: dict | None = None):
53-
"""Initialize the HeaderData object based on router_data.
59+
@classmethod
60+
def from_router_data(cls, router_data: dict) -> "HeaderData":
61+
"""Create a HeaderData object from the given router_data.
5462
5563
Args:
5664
router_data: the router_data dict.
65+
66+
Returns:
67+
A HeaderData object initialized with the provided router_data.
5768
"""
58-
super().__init__()
59-
if router_data:
60-
fields_names = [f.name for f in dataclasses.fields(self)]
61-
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items():
62-
snake_case_key = format.to_snake_case(k)
63-
if snake_case_key in fields_names:
64-
object.__setattr__(self, snake_case_key, v)
65-
object.__setattr__(
66-
self,
67-
"raw_headers",
68-
_FrozenDictStrStr(
69-
**{
70-
k: v
71-
for k, v in router_data.get(
72-
constants.RouteVar.HEADERS, {}
73-
).items()
74-
if v
75-
}
76-
),
77-
)
69+
return cls(
70+
**{
71+
snake_case_key: v
72+
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items()
73+
if v
74+
and (snake_case_key := format.to_snake_case(k)) in _HEADER_DATA_FIELDS
75+
},
76+
raw_headers=_FrozenDictStrStr(
77+
**{
78+
k: v
79+
for k, v in router_data.get(constants.RouteVar.HEADERS, {}).items()
80+
if v
81+
}
82+
),
83+
)
7884

7985

8086
@serializer(to=dict)
@@ -90,6 +96,35 @@ def serialize_frozen_dict_str_str(obj: _FrozenDictStrStr) -> dict:
9096
return dict(obj._data)
9197

9298

99+
class ReflexURL(str, _NetlocResultMixinStr):
100+
"""A class representing a URL split result."""
101+
102+
if TYPE_CHECKING:
103+
scheme: str
104+
netloc: str
105+
path: str
106+
query: str
107+
fragment: str
108+
109+
def __new__(cls, url: str):
110+
"""Create a new ReflexURL instance.
111+
112+
Args:
113+
url: the URL to split.
114+
115+
Returns:
116+
A new ReflexURL instance.
117+
"""
118+
(scheme, netloc, path, query, fragment) = urlsplit(url)
119+
obj = super().__new__(cls, url)
120+
object.__setattr__(obj, "scheme", scheme)
121+
object.__setattr__(obj, "netloc", netloc)
122+
object.__setattr__(obj, "path", path)
123+
object.__setattr__(obj, "query", query)
124+
object.__setattr__(obj, "fragment", fragment)
125+
return obj
126+
127+
93128
@dataclasses.dataclass(frozen=True)
94129
class PageData:
95130
"""An object containing page data."""
@@ -101,77 +136,115 @@ class PageData:
101136
full_raw_path: str = ""
102137
params: dict = dataclasses.field(default_factory=dict)
103138

104-
def __init__(self, router_data: dict | None = None):
105-
"""Initialize the PageData object based on router_data.
139+
@classmethod
140+
def from_router_data(cls, router_data: dict) -> "PageData":
141+
"""Create a PageData object from the given router_data.
106142
107143
Args:
108144
router_data: the router_data dict.
145+
146+
Returns:
147+
A PageData object initialized with the provided router_data.
109148
"""
110-
if router_data:
111-
object.__setattr__(
112-
self,
113-
"host",
114-
router_data.get(constants.RouteVar.HEADERS, {}).get("origin", ""),
115-
)
116-
object.__setattr__(
117-
self, "path", router_data.get(constants.RouteVar.PATH, "")
118-
)
119-
object.__setattr__(
120-
self, "raw_path", router_data.get(constants.RouteVar.ORIGIN, "")
121-
)
122-
object.__setattr__(self, "full_path", f"{self.host}{self.path}")
123-
object.__setattr__(self, "full_raw_path", f"{self.host}{self.raw_path}")
124-
object.__setattr__(
125-
self, "params", router_data.get(constants.RouteVar.QUERY, {})
126-
)
127-
else:
128-
object.__setattr__(self, "host", "")
129-
object.__setattr__(self, "path", "")
130-
object.__setattr__(self, "raw_path", "")
131-
object.__setattr__(self, "full_path", "")
132-
object.__setattr__(self, "full_raw_path", "")
133-
object.__setattr__(self, "params", {})
149+
host = router_data.get(constants.RouteVar.HEADERS, {}).get("origin", "")
150+
path = router_data.get(constants.RouteVar.PATH, "")
151+
raw_path = router_data.get(constants.RouteVar.ORIGIN, "")
152+
return cls(
153+
host=host,
154+
path=path,
155+
raw_path=raw_path,
156+
full_path=f"{host}{path}",
157+
full_raw_path=f"{host}{raw_path}",
158+
params=router_data.get(constants.RouteVar.QUERY, {}),
159+
)
134160

135161

136-
@dataclasses.dataclass(frozen=True, init=False)
162+
@dataclasses.dataclass(frozen=True)
137163
class SessionData:
138164
"""An object containing session data."""
139165

140166
client_token: str = ""
141167
client_ip: str = ""
142168
session_id: str = ""
143169

144-
def __init__(self, router_data: dict | None = None):
145-
"""Initialize the SessionData object based on router_data.
170+
@classmethod
171+
def from_router_data(cls, router_data: dict) -> "SessionData":
172+
"""Create a SessionData object from the given router_data.
146173
147174
Args:
148175
router_data: the router_data dict.
176+
177+
Returns:
178+
A SessionData object initialized with the provided router_data.
149179
"""
150-
if router_data:
151-
client_token = router_data.get(constants.RouteVar.CLIENT_TOKEN, "")
152-
client_ip = router_data.get(constants.RouteVar.CLIENT_IP, "")
153-
session_id = router_data.get(constants.RouteVar.SESSION_ID, "")
154-
else:
155-
client_token = client_ip = session_id = ""
156-
object.__setattr__(self, "client_token", client_token)
157-
object.__setattr__(self, "client_ip", client_ip)
158-
object.__setattr__(self, "session_id", session_id)
180+
return cls(
181+
client_token=router_data.get(constants.RouteVar.CLIENT_TOKEN, ""),
182+
client_ip=router_data.get(constants.RouteVar.CLIENT_IP, ""),
183+
session_id=router_data.get(constants.RouteVar.SESSION_ID, ""),
184+
)
159185

160186

161-
@dataclasses.dataclass(frozen=True, init=False)
187+
@dataclasses.dataclass(frozen=True)
162188
class RouterData:
163189
"""An object containing RouterData."""
164190

165191
session: SessionData = dataclasses.field(default_factory=SessionData)
166192
headers: HeaderData = dataclasses.field(default_factory=HeaderData)
167-
page: PageData = dataclasses.field(default_factory=PageData)
193+
_page: PageData = dataclasses.field(default_factory=PageData)
194+
url: ReflexURL = dataclasses.field(default=ReflexURL(""))
195+
route_id: str = ""
168196

169-
def __init__(self, router_data: dict | None = None):
170-
"""Initialize the RouterData object.
197+
@property
198+
def page(self) -> PageData:
199+
"""Get the page data.
200+
201+
Returns:
202+
The PageData object.
203+
"""
204+
console.deprecate(
205+
"RouterData.page",
206+
"Use RouterData.url instead",
207+
deprecation_version="0.8.1",
208+
removal_version="0.9.0",
209+
)
210+
return self._page
211+
212+
@classmethod
213+
def from_router_data(cls, router_data: dict) -> "RouterData":
214+
"""Create a RouterData object from the given router_data.
171215
172216
Args:
173217
router_data: the router_data dict.
218+
219+
Returns:
220+
A RouterData object initialized with the provided router_data.
174221
"""
175-
object.__setattr__(self, "session", SessionData(router_data))
176-
object.__setattr__(self, "headers", HeaderData(router_data))
177-
object.__setattr__(self, "page", PageData(router_data))
222+
return cls(
223+
session=SessionData.from_router_data(router_data),
224+
headers=HeaderData.from_router_data(router_data),
225+
_page=PageData.from_router_data(router_data),
226+
url=ReflexURL(
227+
router_data.get(constants.RouteVar.HEADERS, {}).get("origin", "")
228+
+ router_data.get(constants.RouteVar.ORIGIN, "")
229+
),
230+
route_id=router_data.get(constants.RouteVar.PATH, ""),
231+
)
232+
233+
234+
@serializer(to=dict)
235+
def serialize_router_data(obj: RouterData) -> dict:
236+
"""Serialize a RouterData object to a dict.
237+
238+
Args:
239+
obj: the RouterData object.
240+
241+
Returns:
242+
A dict representation of the RouterData object.
243+
"""
244+
return {
245+
"session": obj.session,
246+
"headers": obj.headers,
247+
"page": obj._page,
248+
"url": obj.url,
249+
"route_id": obj.route_id,
250+
}

reflex/state.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,15 +1204,15 @@ def setup_dynamic_args(cls, args: dict[str, str]):
12041204

12051205
def argsingle_factory(param: str):
12061206
def inner_func(self: BaseState) -> str:
1207-
return self.router.page.params.get(param, "")
1207+
return self.router._page.params.get(param, "")
12081208

12091209
inner_func.__name__ = param
12101210

12111211
return inner_func
12121212

12131213
def arglist_factory(param: str):
12141214
def inner_func(self: BaseState) -> list[str]:
1215-
return self.router.page.params.get(param, [])
1215+
return self.router._page.params.get(param, [])
12161216

12171217
inner_func.__name__ = param
12181218

@@ -2466,7 +2466,7 @@ def on_load_internal(self) -> list[Event | EventSpec | event.EventCallback] | No
24662466
"""
24672467
# Do not app._compile()! It should be already compiled by now.
24682468
load_events = prerequisites.get_and_validate_app().app.get_load_events(
2469-
self.router.page.path
2469+
self.router._page.path
24702470
)
24712471
if not load_events:
24722472
self.is_hydrated = True

tests/units/test_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1085,7 +1085,7 @@ def _dynamic_state_event(name, val, **kwargs):
10851085
"token": token,
10861086
**on_load_internal.router_data,
10871087
}
1088-
exp_router = RouterData(exp_router_data)
1088+
exp_router = RouterData.from_router_data(exp_router_data)
10891089
process_coro = process(
10901090
app,
10911091
event=on_load_internal,
@@ -1475,7 +1475,7 @@ def test_app_state_determination():
14751475
assert a3._state is None
14761476

14771477
# Referencing router enables state.
1478-
a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
1478+
a3.add_page(rx.box(rx.text(State.router._page.full_path)), route="/")
14791479
a3._compile_page("index")
14801480
assert a3._state is not None
14811481

0 commit comments

Comments
 (0)