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
189from __future__ import annotations
3324
3425@runtime_checkable
3526class 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 )
0 commit comments