Skip to content

Commit 7c6def0

Browse files
fix: add support for tracking dependencies through hybrid properties
1 parent fc44d54 commit 7c6def0

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

packages/reflex-base/src/reflex_base/vars/dep_tracking.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,23 @@ def load_attr_or_method(self, instruction: dis.Instruction) -> None:
185185
except VarValueError:
186186
# If the target state is not a BaseState, we cannot track dependencies on it.
187187
return
188+
# Look up the raw descriptor first via inspect.getattr_static so we can detect
189+
# property-like descriptors (e.g. HybridProperty) that override __get__ to return
190+
# something other than themselves when accessed via the class.
188191
try:
189-
ref_obj = getattr(target_state, instruction.argval)
192+
static_obj = inspect.getattr_static(target_state, instruction.argval)
190193
except AttributeError:
191-
# Not found on this state class, maybe it is a dynamic attribute that will be picked up later.
192-
ref_obj = None
194+
static_obj = None
193195

194-
if isinstance(ref_obj, property) and not isinstance(ref_obj, ComputedVar):
196+
if isinstance(static_obj, property) and not isinstance(static_obj, ComputedVar):
195197
# recurse into property fget functions
196-
ref_obj = ref_obj.fget
198+
ref_obj = static_obj.fget
199+
else:
200+
try:
201+
ref_obj = getattr(target_state, instruction.argval)
202+
except AttributeError:
203+
# Not found on this state class, maybe it is a dynamic attribute that will be picked up later.
204+
ref_obj = None
197205
if callable(ref_obj):
198206
# recurse into callable attributes
199207
self._merge_deps(

tests/integration/test_hybrid_properties.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,6 @@ def test_hybrid_properties(
226226
)
227227

228228
assert full_name.text == "full_name: John"
229-
assert full_name_backend.text == "full_name_backend: John "
229+
# Use textContent to preserve trailing whitespace (Selenium's .text strips it),
230+
# since the backend f-string yields "John " (with trailing space) when last_name is empty.
231+
assert full_name_backend.get_attribute("textContent") == "full_name_backend: John "

tests/units/vars/test_dep_tracking.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,66 @@ def func_with_property(self):
428428
assert tracker.dependencies == expected_deps
429429

430430

431+
def test_hybrid_property_dependencies():
432+
"""Test tracking dependencies through hybrid_property access (without custom .var)."""
433+
from reflex.experimental import hybrid_property
434+
435+
class StateWithHybridProperty(State):
436+
first_name: str = "John"
437+
last_name: str = "Doe"
438+
439+
@hybrid_property
440+
def full_name(self) -> str:
441+
return f"{self.first_name} {self.last_name}"
442+
443+
def func_using_hybrid_property(self):
444+
return self.full_name
445+
446+
tracker = DependencyTracker(
447+
StateWithHybridProperty.func_using_hybrid_property, StateWithHybridProperty
448+
)
449+
450+
# Should recurse into the hybrid_property's fget and track its underlying deps.
451+
expected_deps = {
452+
StateWithHybridProperty.get_full_name(): {"first_name", "last_name"}
453+
}
454+
assert tracker.dependencies == expected_deps
455+
456+
457+
def test_hybrid_property_with_custom_var_dependencies():
458+
"""Test tracking dependencies through hybrid_property access when a custom .var is set.
459+
460+
The dep tracker must still recurse into the Python fget (not the frontend var function),
461+
since computed vars use the backend implementation at runtime.
462+
"""
463+
from reflex.experimental import hybrid_property
464+
from reflex.vars.base import Var
465+
466+
class StateWithHybridPropertyVar(State):
467+
last_name: str = "Doe"
468+
unrelated: str = "ignored"
469+
470+
@hybrid_property
471+
def has_last_name(self) -> str:
472+
return "yes" if self.last_name else "no"
473+
474+
@has_last_name.var
475+
def has_last_name(cls) -> Var[str]:
476+
# Reference an unrelated field here to confirm the tracker uses fget, not this.
477+
return cls.unrelated # pyright: ignore[reportReturnType]
478+
479+
def func_using_hybrid_property(self):
480+
return self.has_last_name
481+
482+
tracker = DependencyTracker(
483+
StateWithHybridPropertyVar.func_using_hybrid_property,
484+
StateWithHybridPropertyVar,
485+
)
486+
487+
expected_deps = {StateWithHybridPropertyVar.get_full_name(): {"last_name"}}
488+
assert tracker.dependencies == expected_deps
489+
490+
431491
def test_no_dependencies():
432492
"""Test functions with no state dependencies."""
433493

0 commit comments

Comments
 (0)