Skip to content

Property side-effects with @lt.on_set#331

Open
rwb27 wants to merge 5 commits into
mainfrom
side-effects-for-properties
Open

Property side-effects with @lt.on_set#331
rwb27 wants to merge 5 commits into
mainfrom
side-effects-for-properties

Conversation

@rwb27
Copy link
Copy Markdown
Collaborator

@rwb27 rwb27 commented May 5, 2026

This adds basic side-effects to properties, along with some tests.

Methods may be decorated with @lt.on_set. The method will then be run whenever the property is set.

To-do:

  • Add to public API docs
  • Add to conceptual docs on data properties, and link from functional properties
  • Test this against DataSetting as well as properties
  • Add at least one example to a feature branch in the OFM codebase

This is intended to allow for basic validation or synchronisation, where the full power of a functional property is not needed. It runs before validation (if enabled), to allow coercion and to catch None in case someone's forgotten to return a value from on_set. This could become configurable in the future.

It may be desirable to also allow a similar decorator to add a validator to the Pydantic model, but that might require more thought, and is not for this PR.

Closes #237

@barecheck
Copy link
Copy Markdown

barecheck Bot commented May 5, 2026

Barecheck - Code coverage report

Total: 97.03%

Your code coverage diff: 0.03% ▴

Uncovered files and lines
FileLines
src/labthings_fastapi/properties.py485, 503, 583, 1019, 1023, 1119, 1142, 1558

@rwb27
Copy link
Copy Markdown
Collaborator Author

rwb27 commented May 6, 2026

From @julianstirling out-of-band:

  • Simulation camera updates the canvas on certain propery changes
  • Also when we have mapping thingslots we check if the key is available in the mapping when we change our selectors

@rwb27 rwb27 force-pushed the side-effects-for-properties branch from 0c864e0 to 1d8bd1c Compare May 6, 2026 10:35
@rwb27 rwb27 added this to the v0.3.0 milestone May 7, 2026
@rwb27 rwb27 force-pushed the side-effects-for-properties branch 2 times, most recently from 2e460ba to 3045760 Compare May 26, 2026 13:15
rwb27 added 5 commits May 27, 2026 13:42
This adds basic side-effects to properties, along with some tests.
Running the `on_set` function before validating the property (if enabled) has a few advantages:
* If the user has forgotten to return a value, it's more likely to catch the `None` if it's invalid.
* It allows user code
This adds `on_set` to the public API docs and to the conceptual page
on properties.

It also tidies up a couple of decorators in the public API docs.
This now matches the test code.

I've also improved testing and error checking, so that naming errors
give a custom exception, and said exception should happen earlier
with a stack trace pointing to the problematic method.

I've added tests of the on_set descriptor's __get__ method too, to ensure it works as expected.
@rwb27 rwb27 force-pushed the side-effects-for-properties branch from c7834cf to 7e4535d Compare May 27, 2026 12:42
@rwb27 rwb27 mentioned this pull request May 28, 2026
See the description at :ref:`properties_on_set` for an example.

This decorator causes a method to be called whenever a property
is set. The method must return the value (and may modify it), but
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
is set. The method must return the value (and may modify it), but
is set. The method must return the value (and may modify it), or raise an exception, but

# Note: this is also checked in __set_name__, but it raises a more helpful
# error if it's checked here.
msg = f"On-set function '{property_name}' overwrites its property: "
msg += "rename it."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this separate from the rest of the msg?

prop = getattr(owner, self.property_name, None)
if not isinstance(prop, DataProperty):
msg = "On-set functions may only be attached to data properties. "
msg += f"'{self.property_name}' is not a data property"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this separate from the rest of the msg?

Comment thread tests/test_property.py
class Example(lt.Thing):
intprop: int = prop_or_setting(default=0)

shadow: int = lt.property(default=0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment explaining what shadow is?

Comment thread tests/test_property.py
Comment on lines +725 to +726
with pytest.raises(ValueError, match="Can't be negative"):
thing.intprop = -1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we then test that thing.intprop and thing.shadow are still 42? If an error is raised, the previous value should remain

Comment thread tests/test_property.py
assert thing._on_set_intprop.args == (thing,)


def test_bad_on_set_definitions():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding some comments within each of the sub-tests here would be useful


There are a few important points to note when using `lt.on_set` in your code:

* Your function *must* return a value, which will be used as the property's value. This allows `lt.on_set` to coerce values to valid ones.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are any errors or warning messages raised if the function doesn't return a value in the on_set function?
Might be helpful to a user, and we can write a test for it

* Your function will run every time the property is set, meaning it should complete quickly. If this function takes longer than a second, it is likely to cause HTTP timeouts.
* It's ok to communicate with hardware, but you are likely to need to acquire any locks you need manually.
* If global locking is enabled, the global lock will already have been acquired when your function is run: there's no need to acquire it again.
* You do not need to "remember" the value as you would for a regular Python property - the data property already takes care of that.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this bullet point is trying to say?

@rwb27
Copy link
Copy Markdown
Collaborator Author

rwb27 commented Jun 1, 2026

Very helpful suggestion from @julianstirling: we should define (and implement, and test) what happens if an exception is raised by on_set.

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.

Add a side_effect option to data properties.

3 participants