Skip to content

Commit b08de4a

Browse files
authored
Merge pull request #856 from dmamelin/rework-webhook
Rework webhook
2 parents 26b7d17 + fa6539a commit b08de4a

13 files changed

Lines changed: 1108 additions & 106 deletions

File tree

‎custom_components/pyscript/decorator.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def __init__(self, ast_ctx: AstEval, eval_func_var: EvalFuncVar) -> None:
250250

251251
def on_func_var_deleted():
252252
if self.status is DecoratorManagerStatus.RUNNING:
253-
self.hass.async_create_task(self.stop())
253+
self.hass.async_create_task(self.safe_await(self.stop()))
254254

255255
weakref.finalize(eval_func_var, on_func_var_deleted)
256256

@@ -261,9 +261,8 @@ async def _call(self, data: DispatchData) -> None:
261261
for handler_dec in handlers:
262262
if await handler_dec.handle_call(data) is False:
263263
self.logger.debug("Calling canceled by %s", handler_dec)
264-
# notify handlers with "None"
265264
for result_handler_dec in result_handlers:
266-
await result_handler_dec.handle_call_result(data, None)
265+
await self.safe_await(result_handler_dec.handle_call_canceled(data))
267266
return
268267
# Fire an event indicating that pyscript is running
269268
# Note: the event must have an entity_id for logbook to work correctly.
@@ -277,10 +276,14 @@ async def _call(self, data: DispatchData) -> None:
277276

278277
try:
279278
result = await data.call_ast_ctx.call_func(self.eval_func, None, **data.func_args)
280-
for result_handler_dec in result_handlers:
281-
await result_handler_dec.handle_call_result(data, result)
282279
except Exception as e:
280+
for result_handler_dec in result_handlers:
281+
await self.safe_await(result_handler_dec.handle_call_exception(data, e))
283282
await self.handle_exception(e)
283+
return
284+
285+
for result_handler_dec in result_handlers:
286+
await self.safe_await(result_handler_dec.handle_call_result(data, result))
284287

285288
async def dispatch(self, data: DispatchData) -> None:
286289
"""Handle a trigger dispatch: run guards, create a context, and invoke the function."""
@@ -290,6 +293,8 @@ async def dispatch(self, data: DispatchData) -> None:
290293
for dec in decorators:
291294
if await dec.handle_dispatch(data) is False:
292295
self.logger.debug("Trigger not active due to %s", dec)
296+
for result_handler_dec in self.get_decorators(CallResultHandlerDecorator):
297+
await self.safe_await(result_handler_dec.handle_call_canceled(data))
293298
return
294299

295300
action_ast_ctx = AstEval(

‎custom_components/pyscript/decorator_abc.py‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
from abc import ABC, abstractmethod
6+
from collections.abc import Awaitable
67
from dataclasses import dataclass, field
78
from enum import StrEnum
89
import logging
@@ -179,8 +180,7 @@ async def start(self):
179180
try:
180181
await decorator.start()
181182
started.append(decorator)
182-
except Exception as err:
183-
self.logger.exception("%s start failed: %s", self, err)
183+
except Exception:
184184
for started_dec in started:
185185
await self._stop_decorator(started_dec)
186186
self.startup_time = None
@@ -209,6 +209,19 @@ async def handle_exception(self, exc: Exception) -> None:
209209
"""Handle a decorator exception."""
210210
self.ast_ctx.log_exception(exc)
211211

212+
async def safe_await(self, coro: Awaitable[Any]) -> None:
213+
"""
214+
Await a coroutine, routing (but not propagating) bugs through ``handle_exception``.
215+
216+
Intended for extension points where a defective subclass shouldn't break
217+
sibling work: the exception surfaces in the same place as user-code errors,
218+
and the caller carries on.
219+
"""
220+
try:
221+
await coro
222+
except Exception as err:
223+
await self.handle_exception(err)
224+
212225
@abstractmethod
213226
async def dispatch(self, data: DispatchData) -> None:
214227
"""Dispatch a trigger call."""
@@ -281,3 +294,11 @@ class CallResultHandlerDecorator(Decorator, ABC):
281294
@abstractmethod
282295
async def handle_call_result(self, data: DispatchData, result: Any) -> None:
283296
"""Handle an action call result."""
297+
298+
async def handle_call_exception(self, data: DispatchData, exc: Exception) -> None:
299+
"""Handle an exception raised by the action call. Default: forward as None result."""
300+
await self.handle_call_result(data, None)
301+
302+
async def handle_call_canceled(self, data: DispatchData) -> None:
303+
"""Handle a canceled action call (skipped by a handler or trigger). Default: forward as None result."""
304+
await self.handle_call_result(data, None)

‎custom_components/pyscript/decorators/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .state import StateActiveDecorator, StateTriggerDecorator
77
from .task import TaskUniqueDecorator
88
from .timing import TimeActiveDecorator, TimeTriggerDecorator
9-
from .webhook import WebhookTriggerDecorator
9+
from .webhook import WebhookHandlerDecorator, WebhookTriggerDecorator
1010

1111
DECORATORS = [
1212
StateTriggerDecorator,
@@ -17,5 +17,6 @@
1717
EventTriggerDecorator,
1818
MQTTTriggerDecorator,
1919
WebhookTriggerDecorator,
20+
WebhookHandlerDecorator,
2021
ServiceDecorator,
2122
]

‎custom_components/pyscript/decorators/base.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from abc import ABC
44
import logging
5-
from typing import Any
5+
from typing import Any, ClassVar
66

77
import voluptuous as vol
88

@@ -16,13 +16,25 @@
1616
class AutoKwargsDecorator(Decorator, ABC):
1717
"""Mixin that copies validated kwargs into instance attributes based on annotations."""
1818

19+
_auto_kw_attrs: ClassVar[frozenset[str]] = frozenset()
20+
21+
def __init_subclass__(cls, **kwargs):
22+
"""Collect names of typed attributes declared on subclasses up to (but not including) Decorator."""
23+
super().__init_subclass__(**kwargs)
24+
attrs: set[str] = set()
25+
for klass in cls.mro():
26+
if klass is Decorator:
27+
break
28+
attrs.update(getattr(klass, "__annotations__", {}))
29+
cls._auto_kw_attrs = frozenset(attrs)
30+
1931
async def validate(self) -> None:
2032
"""Run base validation and materialize annotated kwargs as attributes."""
2133
await super().validate()
2234
for k in self.__class__.kwargs_schema.schema:
2335
if isinstance(k, vol.Marker):
2436
k = k.schema
25-
if k in self.__class__.__annotations__:
37+
if k in self._auto_kw_attrs:
2638
setattr(self, k, self.kwargs.get(k, None))
2739

2840

0 commit comments

Comments
 (0)