Skip to content

Property side-effects with @lt.on_set#331

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

Property side-effects with @lt.on_set#331
rwb27 wants to merge 9 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.04%

Your code coverage diff: 0.04% ▴

Uncovered files and lines
FileLines
src/labthings_fastapi/properties.py486, 504, 584, 1039, 1043, 1139, 1162, 1578

@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
Comment thread src/labthings_fastapi/properties.py Outdated
Comment thread src/labthings_fastapi/properties.py Outdated
Comment thread src/labthings_fastapi/properties.py Outdated
Comment thread tests/test_property.py
Comment thread tests/test_property.py
Comment thread tests/test_property.py

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Currently, I think there should be an error if an on_set function returns a value that's not valid against the property's model. That probably isn't very helpful in what it says, I should take a look.

I don't think it's possible to distinguish between not returning a value and returning None. If None is a valid value, I don't think there's a sensible runtime check that would work.

I will take a look at the validation error that gets raised and see whether it could have a helpful message added if there's an on_set function present.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I have implemented (and tested) two checks:

  1. I now test that the function has a return type annotation. I don't check it matches the property (that's complicated by deferred annotations) but it should mean mypy enforces that it has to return a value, if mypy is in use.
  2. I check if the on_set function modified the value. If so, I check if the modified value is None. If that happens, I log a warning. I'm not 100% sure I like this - I would welcome thoughts.

I suppose I could ask the function to return (True, value) to make it easier to spot - but I'm not sure I like that either...

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 like the checks that have been implemented. One other idea I had is to change what the function is expected to return, so that an implicit None becomes an immediate, catchable error (i.e. force all on-set functions to return a specific dataclass or something). Instead of asking users to make on_set return a raw value or None, we could require them to return a specific class.

Here is a kind of example:

from dataclasses import dataclass
from typing import Any

@dataclass
class SetValue:
    value: Any

# Inside the descriptor:
result = self.on_set_func(obj, value)

if result is None:
    raise ValueError(
        f"You forgot to return a value in {self.name}.on_set! "
        "You must return SetValue(new_value) or SetValue(None)."
    )

if not isinstance(result, SetValue):
    raise TypeError(f"on_set must return a SetValue instance, got {type(result)}.")

value = result.value

SetValue(None) means "I explicitly want to set this to None." An implicit None raises a loud, helpful error. The downside is a slightly more verbose API for the user.

Just an idea, maybe not a useful one! I think we keep the return type-hinting check regardless, I think thats super useful

Copy link
Copy Markdown
Collaborator Author

@rwb27 rwb27 Jun 3, 2026

Choose a reason for hiding this comment

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

What about changing both the name and the mode of operation to look a bit like a Pydantic "wrap validator"?

class MyThing(lt.Thing):
    intprop: int = lt.property(default=0)

    @lt.wrap_setter("intprop")
    def _wrap_set_intprop(self, value: int, setter: lt.Setter[int]) -> None:
        # my logic goes here (exceptions prevent the value changing
        setter(value)
        # I can add more logic here if I want it to run after the value is set

lt.Setter[int] would be a callable accepting an int, and when it's called it would set the value. That way, it's clear which code executes before and after setting the value.

The documentation ought to make it clear that you must either call the setter (exactly once) or raise an exception. That way, it's clear when we have a logic error. I think the new name is likely less confusing, and removes any ambiguity around when it's called in relation to the value changing.

PS I've edited this comment a few times to remove stream-of-consciousness wittering. Sorry if you saw a previous version.

Comment thread docs/source/properties.rst Outdated
@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.

rwb27 added 3 commits June 2, 2026 21:33
I think these are all pretty uncontroversial and resolve most
of the comments.

I'll save checking it does return a value for the next commit.
This should help catch on_set functions that don't return a value.
@rwb27 rwb27 requested a review from bprobert97 June 2, 2026 21:13
Comment thread tests/test_property.py Outdated
Co-authored-by: Beth <167304066+bprobert97@users.noreply.github.com>
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