Experimental decorator manager #795
Draft
+1,585
−8
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Introduction
Its goal is to discuss the architecture and to try out a new experimental subsystem.
I really appreciate the pyscript project — without it, my home would never have become a
notsosmart home. I have many devices, and doing all automation only with GUI and YAML is not realistic.I am not a Python expert and I will be happy to receive any feedback. There is still a lot of unfinished work, but I decided to pause development and gather external opinions before moving forward.
Motivation
The current trigger system in pyscript is very complex:
trigger.pyis tightly coupled and mixes the logic of all decorators.asyncio.Queueabstraction.@decoratorsandtask.wait_until.High-level overview of the new architecture
@decoratoris implemented as an independent class with a clear lifecycle:validate,start,stopDecoratorManager: validates decorators, starts and stops them, dispatches trigger events@decoratorsandtask.wait_untilvoluptuousDecoratorManagerJust compare the commit that adds
@webhook_triggerto the old system with the standalone file in the new architecture.Integration with the existing system
To show how easy it is to integrate the new system, this PR includes a minimal integration commit:
→ b84cd43
DecoratorManagercan be added without removing the existing trigger systemThis allows the new architecture to be tested and improved without breaking existing behavior.
Tests
24b76bc
In this branch,
DecoratorManageris enabled by default only for tests, and all tests pass with minimal changes .The new code contains several test-specific adjustments:
@servicebehaves slightly differentlyThis was intentional. The priority was not to change existing tests, in order to avoid unnecessary churn. All such places are marked with
FIXME.The new classes in
decorator.pyanddecorator_abc.pyare currently not covered by tests because:Tests will be added later.
Compatibility
I am confident that there are still bugs, but I tried to strictly follow both the documentation and the actual behavior of the existing implementation.
My home instance has been running this branch for about a week:
@state_*,@time_*,@service,@task_unique,task.wait_untilI do not use:
@event_trigger,@mqtt_trigger,@webhook_triggerHowever, these decorators are relatively simple and should be easy to fix if issues are discovered.
No critical issues have been observed so far.
How to try it
By default, the new system registers decorators with the e(experimental, enhanced) prefix .
Legacy decorators continue to work as before.
Do not mix legacy and new decorators within the same function.
The new subsystem can be enabled globally(without prefix) via config.yaml: just add
dm: true.For tests, DM is enabled by default. To disable it:
NODM=1 pytest testsTechnical details
Class Hierarchy
Trigger flow example (
@event_trigger)DecoratorManagerOwns all decorators and controls their lifecycle.
FunctionDecoratorManagerUsed for function
@decorators.WaitUntilDecoratorManagerUsed for task.wait_until. It iterates over all TriggerDecorator instances and, when a trigger is found in the wait_until arguments, automatically resolves all required arguments using Decorator.kwargs_schema.
Timeout handling is simple:
a timeout is implemented by adding
time_trigger("once(now + {timeout}s)").Differences between function calls and
task.wait_untilare handled inside the triggers themselves.DecoratorBase class. Handles argument validation using voluptuous.
TriggerDecorator(@*_trigger)Base class for triggers:
TriggerHandlerDecorator(@*_active)Runs immediately during dispatch. Can stop further processing.
CallHandlerDecoratorIt is very similar to TriggerHandlerDecorator, but it is invoked immediately before the function call. At this point, a new
AstEvalinstance and the Home Assistant context are already available. It can cancel the function call and is used by@task_unique.CallResultHandlerDecoratorRuns after the function finishes.
Currently unused, but could be helpful for:
ExpressionDecoratorHelper for working with ast-expressions.
AutoKwargsDecoratorHelper for automatic kwargs handling. See @webhook_trigger —
local_onlyandmethodsare filled in automatically duringvalidate().Implemented decorators
@event_trigger, @mqtt_trigger, @webhook_trigger
The code was moved and the unnecessary asyncio.Queue layer was removed. The base event.py, mqtt.py, and webhook.py files are not used in the new implementation.
@task_unique
According to the documentation, this is equivalent to task.unique, but I moved over the additional validation logic from trigger.py.
If we strictly followed the documentation, the implementation would be very simple:
@service
This decorator is an outlier. It registers the service before the context starts, so DecoratorManager contains some special handling to make the tests pass.
It also does not participate in *_active or @task_unique, which is why its implementation is relatively large and it calls the method directly.
If we decide to make it behave like other decorators and follow the common rules, the implementation could be significantly simplified.
@state_active, @time_trigger, @time_active
The logic was carefully migrated from
trigger.py, with utility functions imported fromtrigger.py.@state_trigger
Probably the most complex part. I initially migrated the logic from trigger.py, but in the new decorator design the old code felt out of place and overly complicated. I ended up rewriting it completely, and I believe the result is much clearer. Hopefully without bugs 🙂
It still uses State.notify_var_get, State.notify_add, and asyncio.Queue.
@pyscript_compile, @pyscript_executor
Left unchanged.
They differ substantially from other decorators, but there are ideas on how to integrate similar decorators into DM if needed.
Transition plan
What I’m looking for
Feedback on the overall architecture.
It’s very possible that I over-engineered parts of the solution or missed a simpler approach.
Any comments or suggestions are very welcome.