Property side-effects with @lt.on_set#331
Conversation
|
From @julianstirling out-of-band:
|
0c864e0 to
1d8bd1c
Compare
2e460ba to
3045760
Compare
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.
c7834cf to
7e4535d
Compare
| 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 |
There was a problem hiding this comment.
| 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." |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Why is this separate from the rest of the msg?
| class Example(lt.Thing): | ||
| intprop: int = prop_or_setting(default=0) | ||
|
|
||
| shadow: int = lt.property(default=0) |
There was a problem hiding this comment.
Can we add a comment explaining what shadow is?
| with pytest.raises(ValueError, match="Can't be negative"): | ||
| thing.intprop = -1 |
There was a problem hiding this comment.
Could we then test that thing.intprop and thing.shadow are still 42? If an error is raised, the previous value should remain
| assert thing._on_set_intprop.args == (thing,) | ||
|
|
||
|
|
||
| def test_bad_on_set_definitions(): |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
I don't understand what this bullet point is trying to say?
|
Very helpful suggestion from @julianstirling: we should define (and implement, and test) what happens if an exception is raised by |
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:
DataSettingas well as propertiesThis 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
Nonein case someone's forgotten to return a value fromon_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