From e884352a724d7d49a1a3f5aaa5808ced69db72e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 20:09:10 +0000 Subject: [PATCH 1/4] Cache Var.to()/guess_type() registry lookups Every Var.to()/guess_type() call scanned a reversed copy of _var_subclasses with safe_issubclass per entry - ~70% of the cost of a single a + b operation (32 safe_issubclass calls, ~8.8us). The registry only grows at import time, so the type->entry resolution is now three functools.cache'd lookups (python-type conversion, guess_type python types, Var-subclass output), each preserving the reversed-scan priority. Registering a new Var subclass clears the caches so later entries still take priority for types they claim. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Jy8uHH11KircGa2MbTrR8g --- .../reflex-base/src/reflex_base/vars/base.py | 116 ++++++++++++++---- tests/units/vars/test_base.py | 24 ++++ 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index f7212bef821..6bd5819e6d9 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -113,6 +113,78 @@ class VarSubclassEntry: _var_literal_subclasses: list[tuple[type[LiteralVar], VarSubclassEntry]] = [] +@functools.cache +def _var_subclass_for_conversion(python_type: GenericType) -> VarSubclassEntry | None: + """Find the registry entry ``Var.to`` maps a python type to. + + Later-registered entries take priority, matching the reversed scan the + cache replaces. The registry only grows at import time; registration + clears this cache (see ``Var.__init_subclass__``). + + Args: + python_type: The (origin-normalized) python type to look up. + + Returns: + The matching entry, or ``None`` if no entry matches. + """ + for var_subclass in reversed(_var_subclasses): + if python_type in var_subclass.python_types or safe_issubclass( + python_type, var_subclass.python_types + ): + return var_subclass + return None + + +@functools.cache +def _var_subclass_matching_python_types( + python_types: tuple[GenericType, ...], +) -> VarSubclassEntry | None: + """Find the registry entry whose python types cover all ``python_types``. + + Used by ``Var.guess_type`` with the (origin-normalized) inner types of + the var type — a 1-tuple for plain types, the union members otherwise. + Later-registered entries take priority; registration clears this cache. + + Args: + python_types: The python types that must all match one entry. + + Returns: + The matching entry, or ``None`` if no entry matches. + """ + for var_subclass in reversed(_var_subclasses): + if all( + safe_issubclass(python_type, var_subclass.python_types) + for python_type in python_types + ): + return var_subclass + return None + + +@functools.cache +def _var_subclass_for_var_output(output: type) -> VarSubclassEntry | None: + """Find the registry entry for a ``Var``-subclass conversion target. + + Later-registered entries take priority; registration clears this cache. + + Args: + output: The ``Var`` subclass passed to ``Var.to``. + + Returns: + The matching entry, or ``None`` if no entry matches. + """ + for var_subclass in reversed(_var_subclasses): + if safe_issubclass(output, var_subclass.var_subclass): + return var_subclass + return None + + +def _clear_var_subclass_lookup_caches() -> None: + """Drop cached registry lookups after a new Var subclass registers.""" + _var_subclass_for_conversion.cache_clear() + _var_subclass_matching_python_types.cache_clear() + _var_subclass_for_var_output.cache_clear() + + _AppWrap = TypeVar("_AppWrap", bound="BaseComponent") @@ -587,6 +659,7 @@ class ToVarOperation(ToOperation, cls): ToVarOperation.__name__ = new_to_var_operation_name _var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types)) + _clear_var_subclass_lookup_caches() def __post_init__(self): """Post-initialize the var. @@ -913,11 +986,9 @@ def to( fixed_output_type = get_origin(output) or output # If the first argument is a python type, we map it to the corresponding Var type. - for var_subclass in _var_subclasses[::-1]: - if fixed_output_type in var_subclass.python_types or safe_issubclass( - fixed_output_type, var_subclass.python_types - ): - return self.to(var_subclass.var_subclass, output) + conversion_entry = _var_subclass_for_conversion(fixed_output_type) + if conversion_entry is not None: + return self.to(conversion_entry.var_subclass, output) if fixed_output_type is None: return get_to_operation(NoneVar).create(self) # pyright: ignore [reportReturnType] @@ -927,16 +998,16 @@ def to( return self.to(ObjectVar, output) if isinstance(output, type): - for var_subclass in _var_subclasses[::-1]: - if safe_issubclass(output, var_subclass.var_subclass): - current_var_type = self._var_type - if current_var_type is Any: - new_var_type = var_type - else: - new_var_type = var_type or current_var_type - return var_subclass.to_var_subclass.create( # pyright: ignore [reportReturnType] - value=self, _var_type=new_var_type - ) + output_entry = _var_subclass_for_var_output(output) + if output_entry is not None: + current_var_type = self._var_type + if current_var_type is Any: + new_var_type = var_type + else: + new_var_type = var_type or current_var_type + return output_entry.to_var_subclass.create( # pyright: ignore [reportReturnType] + value=self, _var_type=new_var_type + ) # If we can't determine the first argument, we just replace the _var_type. if not safe_issubclass(output, Var) or var_type is None: @@ -1003,12 +1074,9 @@ def guess_type(self) -> Var: for inner_type in non_optional_inner_types ] - for var_subclass in _var_subclasses[::-1]: - if all( - safe_issubclass(t, var_subclass.python_types) - for t in fixed_inner_types - ): - return self.to(var_subclass.var_subclass, self._var_type) + union_entry = _var_subclass_matching_python_types(tuple(fixed_inner_types)) + if union_entry is not None: + return self.to(union_entry.var_subclass, self._var_type) if can_use_in_object_var(var_type): return self.to(ObjectVar, self._var_type) @@ -1026,9 +1094,9 @@ def guess_type(self) -> Var: if fixed_type is None: return self.to(None) - for var_subclass in _var_subclasses[::-1]: - if safe_issubclass(fixed_type, var_subclass.python_types): - return self.to(var_subclass.var_subclass, self._var_type) + guessed_entry = _var_subclass_matching_python_types((fixed_type,)) + if guessed_entry is not None: + return self.to(guessed_entry.var_subclass, self._var_type) if can_use_in_object_var(fixed_type): return self.to(ObjectVar, self._var_type) diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index a8075357fa7..e4d7e363e3c 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -42,6 +42,30 @@ def test_figure_out_type(value, expected): assert figure_out_type(value) == expected +def test_var_subclass_registration_invalidates_lookup_caches() -> None: + """A Var subclass registered after lookups were cached takes priority. + + ``Var.to`` / ``Var.guess_type`` dispatch through cached registry lookups; + registering a new Var subclass must drop those caches so the new (higher + priority) entry wins for types it claims. + """ + from reflex_base.vars.base import Var + from reflex_base.vars.sequence import StringVar + + class FancyTestStr(str): + """A str subtype that later gets its own Var subclass.""" + + assert isinstance(Var(_js_expr="a").to(FancyTestStr), StringVar) + + class FancyTestStrVar(Var, python_types=FancyTestStr): + """Var subclass claiming FancyTestStr.""" + + assert isinstance(Var(_js_expr="a").to(FancyTestStr), FancyTestStrVar) + assert isinstance( + Var(_js_expr="a", _var_type=FancyTestStr).guess_type(), FancyTestStrVar + ) + + def test_computed_var_replace() -> None: class StateTest(State): @computed_var(cache=True) From b0cc850ae11d7486c1403af4044a4ae0dfc8ba43 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 20:09:48 +0000 Subject: [PATCH 2/4] Add news fragment for Var registry lookup cache Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Jy8uHH11KircGa2MbTrR8g --- packages/reflex-base/news/6742.performance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/6742.performance.md diff --git a/packages/reflex-base/news/6742.performance.md b/packages/reflex-base/news/6742.performance.md new file mode 100644 index 00000000000..04e1bc18b3d --- /dev/null +++ b/packages/reflex-base/news/6742.performance.md @@ -0,0 +1 @@ +`Var.to()` and `Var.guess_type()` resolve their target Var subclass through cached registry lookups instead of scanning the full registry with `safe_issubclass` on every call (~70% of the cost of constructing a var operation). From 750bdb8e285edd6ea1fe6e02b51a45f88bbc7c8b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 02:17:32 +0000 Subject: [PATCH 3/4] Route all Var subclass registrations through cache invalidation The manual ReflexURLVar registry append bypassed the lookup-cache clear added for __init_subclass__ registrations, so a lookup cached before reflex.istate.data imports could keep resolving ReflexURL to a stale entry. All appends now go through _register_var_subclass_entry. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Jy8uHH11KircGa2MbTrR8g --- .../reflex-base/src/reflex_base/vars/base.py | 20 +++++++++++++++++-- reflex/istate/data.py | 4 ++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 6bd5819e6d9..35359a3c735 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -185,6 +185,21 @@ def _clear_var_subclass_lookup_caches() -> None: _var_subclass_for_var_output.cache_clear() +def _register_var_subclass_entry(entry: VarSubclassEntry) -> None: + """Register a Var subclass entry and invalidate cached lookups. + + Every append to ``_var_subclasses`` must go through here — including + manual registrations like ``ReflexURLVar`` — since a bare append would + leave previously cached lookups returning stale results for types the + new entry claims. + + Args: + entry: The entry to append to the registry. + """ + _var_subclasses.append(entry) + _clear_var_subclass_lookup_caches() + + _AppWrap = TypeVar("_AppWrap", bound="BaseComponent") @@ -658,8 +673,9 @@ class ToVarOperation(ToOperation, cls): ) ToVarOperation.__name__ = new_to_var_operation_name - _var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types)) - _clear_var_subclass_lookup_caches() + _register_var_subclass_entry( + VarSubclassEntry(cls, ToVarOperation, python_types) + ) def __post_init__(self): """Post-initialize the var. diff --git a/reflex/istate/data.py b/reflex/istate/data.py index ee99eab4952..d0f044e54d0 100644 --- a/reflex/istate/data.py +++ b/reflex/istate/data.py @@ -15,7 +15,7 @@ Var, VarData, VarSubclassEntry, - _var_subclasses, + _register_var_subclass_entry, cached_property_no_lock, ) from reflex_base.vars.object import ObjectItemOperation, ObjectVar @@ -333,7 +333,7 @@ def create( # than ToOperation so _js_expr can render as {original}?.["href"]. The registry # entry still accepts it because .to()/guess_type() only call .create(...), # which has a compatible signature. -_var_subclasses.append( +_register_var_subclass_entry( VarSubclassEntry(ReflexURLVar, ReflexURLCastedVar, (ReflexURL,)) # pyright: ignore[reportArgumentType] ) From af2b18c83aa2d034200a7ae8f3f5a9f77f2bbbe0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 02:19:24 +0000 Subject: [PATCH 4/4] Add root news fragment for Var registry lookup cache Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Jy8uHH11KircGa2MbTrR8g --- news/6742.performance.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6742.performance.md diff --git a/news/6742.performance.md b/news/6742.performance.md new file mode 100644 index 00000000000..9bd09c9be2e --- /dev/null +++ b/news/6742.performance.md @@ -0,0 +1 @@ +`Var.to()` and `Var.guess_type()` resolve their target Var subclass through cached registry lookups instead of scanning the full registry with `safe_issubclass` on every call.