Skip to content

Commit 66dcbe5

Browse files
cleanup
1 parent 5ad6e3e commit 66dcbe5

7 files changed

Lines changed: 171 additions & 268 deletions

File tree

packages/reflex-base/src/reflex_base/registry.py

Lines changed: 56 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
1-
"""A contextual registry for state and event handlers.
1+
"""Contextual registry for state classes, event handlers, and the
2+
:class:`NameResolver` strategy that turns them into user-visible names.
23
3-
The registry owns three kinds of bookkeeping:
4-
5-
* the set of registered ``BaseState`` subclasses and their parent/child topology
6-
* the set of registered ``EventHandler`` instances keyed by their full dotted name
7-
* the **name resolution** strategy that turns a state class or handler name into the
8-
string used by the rest of the framework
9-
10-
The third concern is pluggable through the :class:`NameResolver` protocol. The
11-
default resolver is a no-op (every state and handler keeps its built-in name).
12-
``reflex.minify`` ships a :class:`MinifyNameResolver` that consults
13-
``minify.json``; users can plug in their own (custom prefixes, multi-tenant
14-
aliasing, deterministic test fixtures, etc.) by calling
15-
:meth:`RegistrationContext.set_name_resolver`.
4+
The default resolver is a no-op; ``reflex.minify.MinifyNameResolver``
5+
plugs in a :class:`minify.json`-driven implementation. Install a custom
6+
resolver via :meth:`RegistrationContext.set_name_resolver`.
167
"""
178

189
from __future__ import annotations
@@ -33,16 +24,10 @@
3324

3425
@runtime_checkable
3526
class NameResolver(Protocol):
36-
"""Resolves the user-visible names for state classes and event handlers.
37-
38-
Implementations may apply minification, prefixing, locale-based aliasing,
39-
or any other transformation. Returning ``None`` from either method means
40-
"no override — use the default name".
27+
"""Resolves user-visible names for state classes and event handlers.
4128
42-
The protocol is intentionally tiny so resolvers compose well; see
43-
:class:`DefaultNameResolver` for the no-op base case and
44-
``reflex.minify.MinifyNameResolver`` for the canonical example of a
45-
resolver that consults external configuration.
29+
Return ``None`` to defer to the framework default. See
30+
:class:`DefaultNameResolver` (no-op) and ``reflex.minify.MinifyNameResolver``.
4631
"""
4732

4833
def resolve_state_name(self, state_cls: type[BaseState]) -> str | None:
@@ -119,6 +104,18 @@ def ensure_context(cls) -> Self:
119104
cls._context_var.set(ctx)
120105
return ctx
121106

107+
@classmethod
108+
def try_get(cls) -> Self | None:
109+
"""Return the active context, or ``None`` when none is attached.
110+
111+
Returns:
112+
The registration context instance, or ``None``.
113+
"""
114+
try:
115+
return cls.get()
116+
except LookupError:
117+
return None
118+
122119
@classmethod
123120
def register_base_state(cls, state_cls: type[BaseState]) -> type[BaseState]:
124121
"""Register a base state class with its full name.
@@ -220,10 +217,7 @@ def get_substates(
220217

221218
@staticmethod
222219
def default_state_name(state_cls: type[BaseState]) -> str:
223-
"""Compute the built-in name for a state class (no resolver applied).
224-
225-
This is the snake-cased ``module___ClassName`` form used when no
226-
resolver overrides it.
220+
"""Compute the built-in snake-cased ``module___ClassName`` for a state.
227221
228222
Args:
229223
state_cls: The state class.
@@ -239,14 +233,11 @@ def default_state_name(state_cls: type[BaseState]) -> str:
239233
def get_state_name(self, state_cls: type[BaseState]) -> str:
240234
"""Resolve the user-visible name for a state class.
241235
242-
Asks the installed :class:`NameResolver` first; falls back to
243-
:meth:`default_state_name` when the resolver returns ``None``.
244-
245236
Args:
246237
state_cls: The state class.
247238
248239
Returns:
249-
The resolved name.
240+
The resolved name (or :meth:`default_state_name` fallback).
250241
"""
251242
resolved = self.name_resolver.resolve_state_name(state_cls)
252243
if resolved is not None:
@@ -256,32 +247,25 @@ def get_state_name(self, state_cls: type[BaseState]) -> str:
256247
def get_handler_name(self, state_cls: type[BaseState], handler_name: str) -> str:
257248
"""Resolve the user-visible name for an event handler.
258249
259-
Asks the installed :class:`NameResolver` first; falls back to the
260-
original ``handler_name`` when the resolver returns ``None``.
261-
262250
Args:
263251
state_cls: The state class the handler is attached to.
264252
handler_name: The original (Python) name of the handler.
265253
266254
Returns:
267-
The resolved name.
255+
The resolved name (or ``handler_name`` unchanged).
268256
"""
269257
resolved = self.name_resolver.resolve_handler_name(state_cls, handler_name)
270258
if resolved is not None:
271259
return resolved
272260
return handler_name
273261

274262
def set_name_resolver(self, resolver: NameResolver) -> None:
275-
"""Install ``resolver`` and propagate the new names through the registry.
263+
"""Install ``resolver`` and rebuild the registry under the new names.
276264
277-
Concretely: clears the per-class ``get_name``/``get_full_name``/
278-
``get_class_substate`` lru_caches on every registered state and calls
279-
:meth:`refresh_keys` so the registry's name-keyed dicts reflect what
280-
the new resolver returns.
281-
282-
``resolver`` is stored even on a ``frozen`` dataclass via
283-
``object.__setattr__`` — the field is conceptually mutable while the
284-
rest of the context shape stays immutable.
265+
Clears the per-class ``get_name`` / ``get_full_name`` /
266+
``get_class_substate`` lru_caches and calls :meth:`refresh_keys`.
267+
Uses ``object.__setattr__`` to mutate the frozen ``name_resolver``
268+
slot.
285269
286270
Args:
287271
resolver: The resolver to install. Pass :class:`DefaultNameResolver`
@@ -295,33 +279,47 @@ def set_name_resolver(self, resolver: NameResolver) -> None:
295279
self.refresh_keys()
296280

297281
def refresh_keys(self) -> None:
298-
"""Re-key all registered classes/handlers using current full names.
299-
300-
State minification rewrites ``BaseState.get_name()``, but the registry
301-
uses ``parent.get_full_name()`` as the dict key at registration time.
302-
If the minify config (or env var) changes after a class was registered,
303-
lookups will miss. Call this after ``minify.clear_config_cache()`` to
304-
rebuild the dicts with the current names.
282+
"""Re-key the name-keyed dicts using current ``get_full_name`` values.
305283
306-
Builds the replacement dicts before mutating ``self`` so a failure
307-
partway through (e.g. an unreadable minify.json that makes
308-
``format_event_handler`` raise) leaves the existing registry intact.
284+
Built atomically: the replacement dicts are populated before any of
285+
``self`` is mutated, so a partway failure (e.g. malformed minify.json
286+
making ``format_event_handler`` raise) leaves the existing registry
287+
intact. A console warning is emitted on full-name collisions —
288+
usually a sign of duplicate ids in ``minify.json``.
309289
"""
290+
from reflex.utils import console
310291
from reflex.utils.format import format_event_handler
311292

312293
all_classes = list(self.base_states.values())
313294
new_base_states: dict[str, type[BaseState]] = {}
314295
new_substates: dict[str, set[type[BaseState]]] = {}
315296
for cls in all_classes:
316-
new_base_states[cls.get_full_name()] = cls
297+
full_name = cls.get_full_name()
298+
existing = new_base_states.get(full_name)
299+
if existing is not None and existing is not cls:
300+
console.warn(
301+
f"Two state classes resolve to the same full name "
302+
f"{full_name!r}: {existing!r} and {cls!r}. The first one "
303+
"will be unreachable in the registry. Check minify.json "
304+
"for duplicate ids."
305+
)
306+
new_base_states[full_name] = cls
317307
parent = cls.get_parent_state()
318308
if parent is not None:
319309
new_substates.setdefault(parent.get_full_name(), set()).add(cls)
320310

321311
all_handlers = list(self.event_handlers.values())
322-
new_handlers: dict[str, RegisteredEventHandler] = {
323-
format_event_handler(reg.handler): reg for reg in all_handlers
324-
}
312+
new_handlers: dict[str, RegisteredEventHandler] = {}
313+
for reg in all_handlers:
314+
full_name = format_event_handler(reg.handler)
315+
existing_handler = new_handlers.get(full_name)
316+
if existing_handler is not None and existing_handler is not reg:
317+
console.warn(
318+
f"Two event handlers resolve to the same full name "
319+
f"{full_name!r}. The first one will be unreachable in "
320+
"the registry. Check minify.json for duplicate ids."
321+
)
322+
new_handlers[full_name] = reg
325323

326324
self.base_states.clear()
327325
self.base_states.update(new_base_states)

packages/reflex-base/src/reflex_base/utils/format.py

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,8 @@
1414

1515
if TYPE_CHECKING:
1616
from reflex_base.components.component import ComponentStyle
17-
from reflex_base.event import (
18-
ArgsSpec,
19-
EventChain,
20-
EventHandler,
21-
EventSpec,
22-
EventType,
23-
)
17+
from reflex_base.event import EventChain, EventHandler, EventSpec, EventType
18+
from reflex_base.utils.types import ArgsSpec
2419

2520
WRAP_MAP = {
2621
"{": "}",
@@ -448,18 +443,17 @@ def format_props(*single_props, **key_value_props) -> list[str]:
448443
def get_event_handler_parts(
449444
handler: EventHandler | Callable[..., Any],
450445
) -> tuple[str, str]:
451-
"""Get the state and function name of an event handler.
446+
"""Get the (state, function) name pair for an event handler.
452447
453-
Both the state name (via ``state.get_full_name()``) and the handler name
454-
(via :meth:`~reflex_base.registry.RegistrationContext.get_handler_name`)
455-
pass through the active :class:`~reflex_base.registry.NameResolver`, so
456-
minification — or any other pluggable rewrite — is applied transparently.
448+
Both names pass through the active
449+
:class:`~reflex_base.registry.NameResolver`, so any installed rewrite
450+
(minification, prefixing, etc.) is applied transparently.
457451
458452
Args:
459-
handler: The event handler to get the parts of.
453+
handler: The event handler.
460454
461455
Returns:
462-
The (resolved) state full name and (resolved) handler function name.
456+
``(state_full_name, handler_name)`` — both resolved.
463457
464458
Raises:
465459
TypeError: If the handler is not an EventHandler.
@@ -471,20 +465,14 @@ def get_event_handler_parts(
471465
msg = f"Expected EventHandler, got {type(handler)}"
472466
raise TypeError(msg)
473467

474-
# The Python name of the handler function (e.g. "ClassName.handler_name").
475468
name = handler.fn.__qualname__
476-
477-
# If there's no enclosing state, just return the full qualname.
478469
if handler.state is None:
479470
return ("", name)
480471

481472
state_full_name = handler.state.get_full_name()
482473
func_name = name.rpartition(".")[2]
483-
484-
# Let the registry's resolver have a say (e.g. minified handler names).
485-
try:
486-
ctx = RegistrationContext.get()
487-
except LookupError:
474+
ctx = RegistrationContext.try_get()
475+
if ctx is None:
488476
return (state_full_name, func_name)
489477
return (state_full_name, ctx.get_handler_name(handler.state, func_name))
490478

0 commit comments

Comments
 (0)