attributes: @attr decorator sugar over the getter/setter constructors - #423
Conversation
Adds the `@attr` decorator from ADR 0018: a controller declares an attribute by decorating the method that reads it, with `@x.setter` for the writer half, mirroring `@property`. The datatype comes from the getter's return annotation (unwrapping `Update[T]`), the getter's docstring summary becomes the description, and decorator keyword arguments are the attribute's metadata, validated against the datatype. The optional leading positional is a `Polled`/`NotPolled` schedule, so the declarative and procedural spellings share one vocabulary; a bare `@attr` is read once at connect, as a bare `getter=` is. Binding follows `@command`/`@scan`: the class body holds an `UnboundAttr` declaration and each controller instance binds a fresh `AttrR`/`AttrRW` of its own, so nothing is deepcopied from a class-scope prototype. `UnboundAttr` is a non-data descriptor so that a decorated attribute reads as the attribute it becomes rather than the declaration - `UnboundAttrRW` carries the `AttrRW` typing. Adds the "FastCS for PyTango users" docs page. Closes #397 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SiGhLM9QRKpnQykdmMfLsh
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## refactor #423 +/- ##
============================================
+ Coverage 91.25% 92.63% +1.38%
============================================
Files 72 70 -2
Lines 2892 3298 +406
============================================
+ Hits 2639 3055 +416
+ Misses 253 243 -10 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ng specific exceptions; instead, reraise.
…ne line in pytest.raises
shihab-dls
left a comment
There was a problem hiding this comment.
Pushed a few changes. Now happy with the final shape, so approving on my end.
| Type checkers special-case the builtin `property` but not decorators that | ||
| imitate it, so pyright reports the getter as *obscured by a declaration of the | ||
| same name*, and mypy as *already defined*. The two declarations are deliberate, | ||
| so silence it at the getter: |
There was a problem hiding this comment.
@shihab-dls I'm tempted to copy PyTango in this rather than @property so we don't need to ignore pyright.
If we say the name should be set_voltage, then the decorator sets the setter on the AttrRW, but returns the setter function.
That means controller.voltage is an AttrRW, but controller.set_voltage is a method that sets controller.voltage. What do you think?
There was a problem hiding this comment.
100%. I'm actually now re-reviewing this after looking at #425, and realized we need to make this change. I'll add this as a comment on that PR.
Closes #397
An attribute can now be declared by decorating the method that reads it, with
@x.setterfor the writer half - the PyTango hello-world, written over thegetter=/setter=constructors from #392 rather than beside them.Scope
@attris a decorator only, in two forms: bare@attrand parameterised@attr(Polled(period=0.5), units="V"). Getter only builds anAttrR; getter +@x.setterbuilds anAttrRW. There is no@attr_r/@attr_rw, no free-functionattr()factory, and no write-only decorator -AttrW(setter=…)stays longhand.Update[T], and supports theArray1D/Tablespellings. Not annotating it fails at decoration with a message naming the getter.description=wins.Unpack[Meta](the superset, since the datatype is not known until the getter is read) and runtime-validated against the datatype by the constructor, so@attr(precision=3)on a-> strgetter raises naming the field, datatype and attribute.Polled/NotPolledobjects the procedural form wraps its getter in, per ADR 0018's table. A bare@attris read once at connect, exactly as a baregetter=is.@command/@scan: the class body holds anUnboundAttrdeclaration, andBaseController._bind_attrsbuilds a freshAttrR/AttrRWper instance with the getter and setter bound to it. Nothing is deepcopied from a class-scope prototype, so two controllers never share an attribute or write to each other's device.__init__, so a laterself.voltage = AttrR(...)hits the existing_check_for_name_clash. A matchingvoltage: AttrR[float]hint is validated against the decorated attribute by the existing hinted-attribute check.docs/how-to/fastcs-for-pytango-users.md, pairing each spelling with its PyTango equivalent and saying when to reach for@attrrather than the constructor or (when it lands, ControllerFiller — declarative/procedural split #394) the filler.Instructions to reviewer on how to test:
uv run pytest tests/test_attr_decorator.py -v@attr, serve it over EPICS CA, and confirm the PVs carry the docstring asDESCand the decorator'sunits/precision.Checks for reviewer
@attrneeds# pyright: ignore[reportRedeclaration]on the getter. This is the one wart, and it is inherent to the@x.setterspelling rather than to this implementation: type checkers special-case the builtinpropertyand nothing else, so twodef voltagein one class body is an error (pyright: obscured by a declaration of the same name; mypy: already defined). I measured the alternatives before choosing:UnboundAttris a plain non-data descriptor whose__get__is typed to returnAttrR[T], andUnboundAttrRW's returnsAttrRW[T].self.voltage.set(...),.readback,.setpointand the datatype all check correctly at every use site; the cost is one suppression comment per read-write declaration, which the docs page spells out.UnboundAttrfrompropertysilences the diagnostic completely - but pyright then evaluates the whole pair through its property path, andc.voltagecomes out asAttrR[Unknown]:.set()is unknown and even the datatype is lost. That trades one comment at the declaration for a cast at every use, which is why I did not take it.I have kept the ADR's spelling; say if you would rather have the property derivation, or a different spelling that avoids the collision (
@voltage.writeron a differently-named method), and it is a small commit either way.@attr(Polled(period=0.5)), notPolled(0.5). ADR 0018 writes the shorthandPolled(0.5), butPolled's first positional is its getter andperiodis keyword-only (Polled(protocol.get_temperature, period=0.2)is the procedural form throughout the demo and docs), soPolled(0.5)would silently bind 0.5 as the getter. Rather than reorderPolled's fields and rewrite every procedural call site,@attrtakes a barePolled(period=0.5), and rejects a schedule that already carries a getter. Say if you would ratherPolledgrew a period-first spelling.DESCis 40 characters), so a getter with a summary line and further explanation contributes only the summary. Say if you want the whole docstring.Notes
UnboundAttrrefuses to be read before it is bound, so_bind_attrsreaches the declaration withinspect.getattr_staticrather thangetattr. The error a user gets from touching a decorated attribute beforeController.__init__has run names the attribute and says why..setterreturns a newUnboundAttrRWrather than mutating in place, soclass Child(Base)declaring a setter for a getter it inherited does not also giveBasea setter. Covered by a test.uv run --locked tox -e pre-commit,type-checking, both green in full. For thetestsenv, this sandbox can't rundocs(needs outbound network -conf.pyfetchesswitcher.json) or the PVA/p4p-backed tests (RuntimeError: Address family not supported by protocol), the same known limitation noted on demo: use ControllerVector for temperature ramp sub-controllers #409/demo: cut-down Eiger REST sim + introspectable controller example #410/demo: convert temperature controller to getter/setter style #411/attributes: getter/setter IO rework, remove AttributeIORef/AttributeIO #412/attributes: replace the DataType family with python types and*Metatyped dicts #418/methods: typed commands — positional arguments and a return value #419/controllers: ControllerRunner, plus native timestamps and severity on attributes #420. Excluding those,pytest src tests --ignore=tests/benchmarkingpasses 437/447, with only the same 10 pre-existing p4p/socket-family failures, which I confirmed are identical onrefactoritself by running them in a worktree off the base commit. I also ran the docs build offline with theswitcher.jsonfetch stubbed out: it succeeds, and every warning it reports is a missing-intersphinx-inventory artifact of having no network, present on unrelated existing modules too - none come from the new module or the new page. Real CI coversdocsand PVA.🤖 Generated with Claude Code
https://claude.ai/code/session_01SiGhLM9QRKpnQykdmMfLsh
Generated by Claude Code