Skip to content

Serve class-defined rx attributes without resolving the node - #1230

Open
philippjfr wants to merge 1 commit into
mainfrom
fix/rx-getattribute-class-attrs
Open

philippjfr wants to merge 1 commit into
mainfrom
fix/rx-getattribute-class-attrs

Conversation

@philippjfr

@philippjfr philippjfr commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Description

rx.__getattribute__ handles every public, non-rx name by checking disposal, then returning a getattr clone if the node is lazy, then resolving the node if it is dirty, and only after that checking whether the name is defined on the class. The resolve was only ever needed to build the list of attributes on the wrapped value, but because of the ordering a property or method defined on an rx subclass forced a full compute of a dirty node just to be read, hasattr(node, name) computed too, and on a lazy node the class attribute was swapped for a deferred getattr clone. __setattr__ never resolves, so reads and writes were asymmetric: a downstream subclass that applies N input masks through a property computes a cold node N times, and a typo's KeyError only shows up after a compute.

Names defined on type(self) or any class in its MRO now return straight after the disposal check. Class attributes already took precedence over value attributes in the old code (name not in super().__dir__()), so the only behaviour change is that these reads no longer resolve and no longer turn into lazy clones. Notes on the edge cases:

  • Registered accessors (rx._accessors) are left out of the fast path and go through the existing code unchanged, so the predicate still runs against a resolved current. None are class attributes on rx, but a name registered as an accessor still wins over a class attribute of the same name, as before. Accessors already memoized in the instance __dict__ are not class-defined and also keep their current path.
  • Disposal still raises for a class-attribute read. A subclass property most likely reads node state, and this matches what happens today. _-prefixed names were never checked and still aren't.
  • In base rx the only public class-defined names besides rx are the register_* classmethods, which nobody expects to proxy to the value.
import param

class Annotated(param.rx):
    @property
    def annotation(self):
        return 'annotation'

calls = []
def fn(x):
    calls.append(x)
    return x

src = Annotated(1)
node = src.rx.pipe(fn)
src.rx.value = 2
node.annotation
print(calls)  # before: [2], after: []

print(type(Annotated(1, lazy=True).annotation))  # before: Annotated, after: str

Tests in TestSubclassAttributes cover property, method, classmethod and hasattr reads on a dirty node (no upstream call), a lazy node returning the property value, the disposed-node error, accessor predicates still seeing the resolved value, and value attributes still proxying and resolving (rx('abc').upper(), attribute of an object, deferred method on a lazy node). pytest tests/testreactive.py passes (598 tests) and pixi run test-unit reports 2057 passed. pixi run test-unit exits non-zero on both this branch and main because of unraisable socket and event-loop __del__ warnings raised after the session, and tests/testimports.py::test_no_blocklist_imports fails in a local conda env (dev3.14) on main too, so neither failure comes from this change.

AI Disclosure

Tool & Model: Claude Code + claude-opus-5-5
Usage: Claude Code read the method and its git history, wrote the change and the tests, ran the test suite, and drafted this description.

  • I have tested all AI-generated content in my PR.
  • I take responsibility for all AI-generated content in my PR.

Checklist

  • Tests added and are passing

rx.__getattribute__ resolved a dirty node, or returned a lazy getattr
clone, before checking whether the name was defined on the class. A
property or method on an rx subclass therefore forced a compute just to
be read, and hasattr() did too. Class-defined names now short-circuit
after the disposal check; registered accessors keep their existing path.

Assisted-by: Claude Code:claude-opus-5-5
@codecov

codecov Bot commented Oct 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.48%. Comparing base (af94e0c) to head (4ad4745).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1230   +/-   ##
=======================================
  Coverage   89.48%   89.48%           
=======================================
  Files           9        9           
  Lines        6666     6668    +2     
=======================================
+ Hits         5965     5967    +2     
  Misses        701      701           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
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.

1 participant