Skip to content

Support hybrid_property on ObjectVar for nested dataclasses#6619

Open
masenf wants to merge 6 commits into
mainfrom
claude/relaxed-cerf-Z110q
Open

Support hybrid_property on ObjectVar for nested dataclasses#6619
masenf wants to merge 6 commits into
mainfrom
claude/relaxed-cerf-Z110q

Conversation

@masenf

@masenf masenf commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Type of change

  • New feature (non-breaking change which adds functionality)

Description

This PR extends hybrid_property support to work seamlessly with ObjectVar attribute access on nested dataclasses, Pydantic models, and SQLAlchemy models—not just State classes.

Key changes:

  1. Moved HybridProperty to reflex_base: Relocated hybrid_property.py from reflex/experimental/ to reflex_base/vars/ to make it available at the var level where it's needed for ObjectVar resolution.

  2. ObjectVar descriptor resolution: Modified ObjectVar.__getattr__ to detect and resolve HybridProperty descriptors on the underlying type. When a hybrid property is accessed (e.g., State.info.a_b), the property's frontend logic is evaluated with the object var substituted as self, enabling consistent var-level semantics.

  3. HybridProperty._get_var extraction: Refactored the var-resolution logic into a dedicated _get_var method that accepts either a class or an ObjectVar as the owner, allowing the property to work uniformly across both direct class access and nested object var access.

  4. Comprehensive test coverage: Added unit tests verifying hybrid property resolution on bare classes, Pydantic models, SQLAlchemy models, and dataclasses. Included integration tests demonstrating re-rendering when nested dataclass fields are updated.

Example usage:

@dataclass
class Info:
    a: str
    b: str
    
    @hybrid_property
    def a_b(self) -> str:
        return f"{self.a} - {self.b}"

class State(rx.State):
    info: Info = Info(a="a", b="b")

# Frontend renders as: "a - b"
rx.text(State.info.a_b)

Closes #6617

Testing

  • Added unit tests in tests/units/vars/test_object.py covering hybrid property resolution on all object types
  • Added integration tests in tests/integration/test_hybrid_properties.py verifying frontend rendering and re-rendering on state updates
  • Existing tests pass with new functionality

https://claude.ai/code/session_01DKFiYGnWRQG8wMNKFW7obm

claude added 2 commits June 5, 2026 20:25
`rx._x.hybrid_property` previously only resolved as a frontend var when
accessed directly on a `State` class. Accessing it through an object var
(e.g. `State.info.a_b` where `info` is a dataclass, pydantic model or
SQLAlchemy model) raised `VarAttributeError`.

`ObjectVar.__getattr__` now detects a `HybridProperty` defined on the
underlying type and evaluates its frontend logic with the object var
substituted as `self`, so it renders with the same Var-access semantics
as accessing the hybrid property directly on the state. This works
uniformly across bare classes, pydantic models, SQLAlchemy models and
dataclasses, since they are all treated as object vars.

`HybridProperty` moved to `reflex_base.vars.hybrid_property` (so the var
system can reference it without an inverted dependency) and is still
re-exported from `reflex.experimental.hybrid_property`.

Fixes #6617

https://claude.ai/code/session_01DKFiYGnWRQG8wMNKFW7obm
Import `hybrid_property` directly from `reflex_base.vars.hybrid_property`
in `reflex.experimental.__init__` instead of going through a one-line
re-export module. `from reflex.experimental import hybrid_property` and
`rx._x.hybrid_property` are unchanged.

https://claude.ai/code/session_01DKFiYGnWRQG8wMNKFW7obm
@masenf masenf requested a review from a team as a code owner June 5, 2026 21:08
@codspeed-hq

codspeed-hq Bot commented Jun 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 26 untouched benchmarks
⏩ 8 skipped benchmarks1


Comparing claude/relaxed-cerf-Z110q (8454a2c) with main (932f20f)

Open in CodSpeed

Footnotes

  1. 8 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends HybridProperty support to ObjectVar attribute access on nested dataclasses, Pydantic models, and SQLAlchemy models by moving hybrid_property.py into reflex_base and adding a _get_var method that accepts either a class or an ObjectVar as the owner.

  • ObjectVar.__getattr__ refactored: calls get_attribute_descriptor once to detect a HybridProperty and short-circuits to _get_var(self), substituting the object var as self so the property's frontend logic runs with var-level semantics.
  • get_attribute_descriptor helper added: centralises the getattr/NotImplementedError pattern so both the HybridProperty check and get_attribute_access_type can share a single lookup; get_attribute_access_type gains an optional descriptor parameter to accept the pre-computed result.
  • HybridProperty.__get__ changed: class-level access on non-BaseState types now returns the descriptor itself instead of calling fget(owner), which prevents confusing results when accessing the property directly on a plain dataclass or Pydantic model.

Confidence Score: 5/5

Safe to merge; the logic change is well-tested and the refactoring preserves all existing dispatch paths in ObjectVar.getattr.

The branch-reordering in ObjectVar.getattr is behavior-preserving (TypedDicts are still excluded from the first arm by their Mapping inheritance and route to the same elif handler as before). The _get_var extraction is straightforward and backed by unit tests across all four object types plus an integration test. No data-loss or incorrect-var-generation paths were found.

packages/reflex-base/src/reflex_base/vars/hybrid_property.py — the lazy from reflex.state import BaseState import inside get creates an upward runtime dependency from reflex_base into reflex; packages/reflex-base/src/reflex_base/utils/types.py — the descriptor parameter is shadowed by a local in the SQLAlchemy block.

Important Files Changed

Filename Overview
packages/reflex-base/src/reflex_base/vars/hybrid_property.py Refactored to extract _get_var and changed __get__ to return the descriptor itself on non-BaseState class access; introduces a lazy upward import of reflex.state.BaseState from within the reflex_base package.
packages/reflex-base/src/reflex_base/vars/object.py Refactored ObjectVar.__getattr__ to detect and short-circuit HybridProperty descriptors; the reordering of the type-dispatch branches is behavior-preserving (TypedDicts still route to the elif arm via Mapping exclusion).
packages/reflex-base/src/reflex_base/utils/types.py Added get_attribute_descriptor helper and an optional descriptor parameter to get_attribute_access_type; the SQLAlchemy block still reassigns a local descriptor variable that now shadows the new parameter (pre-existing naming collision, not a new bug).
reflex/experimental/init.py Import of hybrid_property redirected from the deleted local module to reflex_base.vars.hybrid_property; the public surface (rx._x.hybrid_property and from reflex.experimental import hybrid_property) is preserved.
tests/units/vars/test_object.py Added comprehensive unit tests for HybridProperty on all four object types (bare, pydantic, SQLAlchemy, dataclass), class-level access semantics, and the specific issue regression case.
tests/integration/test_hybrid_properties.py Extended with a nested Info dataclass, state update event, and frontend rendering/re-rendering assertions for the new object-var hybrid property path.

Reviews (3): Last reviewed commit: "Merge branch 'main' into claude/relaxed-..." | Re-trigger Greptile

Comment thread packages/reflex-base/src/reflex_base/vars/hybrid_property.py Outdated
Comment thread reflex/experimental/__init__.py
claude added 2 commits June 5, 2026 23:00
…non-states

HybridProperty.__get__ produced a frontend var for any class-level access,
which only makes sense on a state (whose class attributes are vars). On a
plain class accessed directly — e.g. `Info.a_b` on a dataclass, not through
an object var — it ran the getter with the class as `self`, raising
AttributeError (no field default) or returning a value built from class
defaults. It now returns the descriptor itself, like a normal property.

Var access through an object var (`State.info.a_b`) is unaffected: it is
resolved by ObjectVar.__getattr__ via _get_var, not __get__.

https://claude.ai/code/session_01DKFiYGnWRQG8wMNKFW7obm
ObjectVar.__getattr__ ran inspect.getattr_static on every attribute access to
detect a HybridProperty on the underlying type — a pure-Python MRO walk on a hot
path, ~15x slower than getattr for ordinary field access. Now that
HybridProperty.__get__ returns the descriptor itself for non-state class access,
a plain getattr surfaces it directly, so the static lookup is no longer needed.

https://claude.ai/code/session_01DKFiYGnWRQG8wMNKFW7obm
@masenf

masenf commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

@greptile

@benedikt-bartscher

Copy link
Copy Markdown
Contributor

This is great, thanks @masenf !

FarhanAliRaza and others added 2 commits June 18, 2026 00:35
Extract the class-attribute resolution into get_attribute_descriptor so
ObjectVar attribute access resolves the descriptor once and reuses it for
both HybridProperty detection and get_attribute_access_type, instead of
looking it up twice. Also hoist the HybridProperty import to module level
and flatten the branching in __getattr__.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@rx._x.hybrid_property should work on a model/dataclass

4 participants