Skip to content

Commit 192fbcc

Browse files
author
remimd
committed
fix
1 parent e7b8551 commit 192fbcc

3 files changed

Lines changed: 118 additions & 43 deletions

File tree

cq/_core/handler.py

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -91,39 +91,48 @@ class HandlerDecorator[I, O]:
9191

9292
def __call__(
9393
self,
94-
input_or_handler: type[I] | HandlerType[[I], O] | None = None,
94+
input_or_handler_type: type[I] | HandlerType[[I], O] | None = None,
9595
/,
9696
) -> Any:
97-
def decorator(
98-
wrapped: HandlerType[[I], O],
99-
*,
100-
input_type: type[I] | None = None,
101-
) -> HandlerType[[I], O]:
102-
factory = self.injection_module.make_async_factory(wrapped)
103-
input_type = input_type or _resolve_input_type(wrapped)
104-
self.manager.subscribe(input_type, factory)
105-
return wrapped
97+
if input_or_handler_type is None:
98+
return self.__decorator
10699

107-
if input_or_handler is None:
108-
return decorator
100+
elif isclass(input_or_handler_type) and issubclass(
101+
input_or_handler_type,
102+
Handler,
103+
):
104+
return self.__decorator(input_or_handler_type)
109105

110-
elif isclass(input_or_handler) and issubclass(input_or_handler, Handler):
111-
return decorator(input_or_handler)
106+
return partial(self.__decorator, input_type=input_or_handler_type) # type: ignore[arg-type]
112107

113-
else:
114-
return partial(decorator, input_type=input_or_handler) # type: ignore[arg-type]
108+
def __decorator(
109+
self,
110+
wrapped: HandlerType[[I], O],
111+
*,
112+
input_type: type[I] | None = None,
113+
) -> HandlerType[[I], O]:
114+
factory = self.injection_module.make_async_factory(wrapped)
115+
input_type = input_type or _resolve_input_type(wrapped)
116+
self.manager.subscribe(input_type, factory)
117+
return wrapped
115118

116119

117120
def _resolve_input_type[I, O](handler_type: HandlerType[[I], O]) -> type[I]:
118121
fake_handle_method = handler_type.handle.__get__(NotImplemented)
119122
signature = inspect_signature(fake_handle_method, eval_str=True)
120-
parameters = iter(signature.parameters.values())
121-
input_type = next(parameters).annotation
122123

123-
if input_type is Parameter.empty:
124-
raise TypeError("Unable to resolve input type.")
124+
for parameter in signature.parameters.values():
125+
input_type = parameter.annotation
126+
127+
if input_type is Parameter.empty:
128+
break
125129

126-
return input_type
130+
return input_type
131+
132+
raise TypeError(
133+
f"Unable to resolve input type for handler `{handler_type}`, "
134+
"`handle` method must have a type annotation for its first parameter."
135+
)
127136

128137

129138
def _make_handle_function[I, O](

tests/core/test_handler.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import Any, NoReturn
2+
3+
import pytest
4+
5+
from cq._core.handler import HandlerDecorator, SingleHandlerManager
6+
7+
8+
class _Handler:
9+
async def handle(self, input_value: str) -> NoReturn:
10+
raise NotImplementedError
11+
12+
13+
class TestHandlerDecorator:
14+
@pytest.fixture(scope="function")
15+
def handler_decorator(self) -> HandlerDecorator[Any, Any]:
16+
return HandlerDecorator(SingleHandlerManager())
17+
18+
def test_call_with_success_return_wrapped_type(
19+
self,
20+
handler_decorator: HandlerDecorator[Any, Any],
21+
) -> None:
22+
assert handler_decorator(_Handler) is _Handler
23+
24+
def test_call_with_input_type_return_wrapped_type(
25+
self,
26+
handler_decorator: HandlerDecorator[Any, Any],
27+
) -> None:
28+
assert handler_decorator(str)(_Handler) is _Handler
29+
30+
def test_call_with_no_args_return_wrapped_type(
31+
self,
32+
handler_decorator: HandlerDecorator[Any, Any],
33+
) -> None:
34+
assert handler_decorator()(_Handler) is _Handler
35+
36+
def test_call_with_missing_input_type_annotation_raise_type_error(
37+
self,
38+
handler_decorator: HandlerDecorator[Any, Any],
39+
) -> None:
40+
with pytest.raises(TypeError):
41+
42+
@handler_decorator
43+
class Handler:
44+
async def handle(self, input_value): ... # type: ignore[no-untyped-def]
45+
46+
def test_call_with_missing_input_in_handle_raise_type_error(
47+
self,
48+
handler_decorator: HandlerDecorator[Any, Any],
49+
) -> None:
50+
with pytest.raises(TypeError):
51+
52+
@handler_decorator
53+
class Handler:
54+
async def handle(self): ... # type: ignore[no-untyped-def]

uv.lock

Lines changed: 34 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)