From f57e7f66a39a981ce9b8e3710ff6337a837be671 Mon Sep 17 00:00:00 2001 From: Aldo Mateli Date: Sat, 25 Nov 2023 13:34:26 +0000 Subject: [PATCH] Make container.register type hints work with mypy/pyright and not obscure the return type --- test/test_container.py | 10 +++++ wireup/errors.py | 7 ++++ wireup/ioc/dependency_container.py | 64 +++++++++++++++++------------- 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/test/test_container.py b/test/test_container.py index 1017723..1032cea 100644 --- a/test/test_container.py +++ b/test/test_container.py @@ -12,6 +12,7 @@ from wireup.errors import ( DuplicateQualifierForInterfaceError, DuplicateServiceRegistrationError, + InvalidRegistrationTypeError, UnknownQualifiedServiceRequestedError, UnknownServiceRequestedError, UsageOfQualifierOnUnknownObjectError, @@ -499,3 +500,12 @@ def target(a: RandomService, _b: unittest.TestCase = None, _c: datetime.datetime self.assertEqual(self.container.context.dependencies[target].keys(), {"a", "_b", "_c"}) autowired() self.assertEqual(self.container.context.dependencies[target].keys(), {"a"}) + + def test_raises_when_injecting_invalid_types(self): + with self.assertRaises(InvalidRegistrationTypeError) as err: + self.container.register(services) + + self.assertEqual( + str(err.exception), + f"Cannot register {services} with the container. " f"Allowed types are callables and types", + ) diff --git a/wireup/errors.py b/wireup/errors.py index cca5472..412f05e 100644 --- a/wireup/errors.py +++ b/wireup/errors.py @@ -86,3 +86,10 @@ class UsageOfQualifierOnUnknownObjectError(WireupError): def __init__(self, qualifier_value: ContainerProxyQualifierValue) -> None: super().__init__(f"Cannot use qualifier {qualifier_value} on a type that is not managed by the container.") + + +class InvalidRegistrationTypeError(WireupError): + """Raised when attempting to call @container.register with an invalid argument.""" + + def __init__(self, attempted: Any) -> None: + super().__init__(f"Cannot register {attempted} with the container. Allowed types are callables and types") diff --git a/wireup/ioc/dependency_container.py b/wireup/ioc/dependency_container.py index ab10429..7ca5c7c 100644 --- a/wireup/ioc/dependency_container.py +++ b/wireup/ioc/dependency_container.py @@ -2,11 +2,12 @@ import asyncio import functools -from typing import TYPE_CHECKING, Any, Callable, TypeVar +from typing import TYPE_CHECKING, Any, Callable, TypeVar, overload from graphlib2 import TopologicalSorter from wireup.errors import ( + InvalidRegistrationTypeError, UnknownQualifiedServiceRequestedError, UnknownServiceRequestedError, UsageOfQualifierOnUnknownObjectError, @@ -17,7 +18,6 @@ from .types import ( AnnotatedParameter, AnyCallable, - AutowireTarget, ContainerProxyQualifierValue, EmptyContainerInjectionRequest, ParameterWrapper, @@ -86,36 +86,57 @@ def abstract(self, klass: type[__T]) -> type[__T]: return klass + @overload def register( self, - obj: AutowireTarget | None = None, + obj: None = None, *, - qualifier: ContainerProxyQualifierValue = None, + qualifier: ContainerProxyQualifierValue | None = None, lifetime: ServiceLifetime = ServiceLifetime.SINGLETON, - ) -> AutowireTarget | Callable[[AutowireTarget], AutowireTarget]: - """Register a dependency in the container. + ) -> Callable[[__T], __T]: + pass - Use `@register` without parameters on a class or with a single parameter `@register(qualifier=name)` - to register this with a given name when there are multiple implementations of the interface this implements. + @overload + def register( + self, + obj: __T, + *, + qualifier: ContainerProxyQualifierValue | None = None, + lifetime: ServiceLifetime = ServiceLifetime.SINGLETON, + ) -> __T: + pass - Use `@register` on a function to register that function as a factory method which produces an object - that matches its return type. + def register( + self, + obj: __T | None = None, + *, + qualifier: ContainerProxyQualifierValue | None = None, + lifetime: ServiceLifetime = ServiceLifetime.SINGLETON, + ) -> __T | Callable[[__T], __T]: + """Register a dependency in the container. Dependency must be either a class or a factory function. - The container stores all necessary metadata for this class and the underlying class remains unmodified. + * Use as a decorator without parameters @container.register on a factory function or class to register it. + * Use as a decorator with parameters to specify qualifier and lifetime, @container.register(qualifier=...). + * Call it directly with @container.register(some_class_or_factory, qualifier=..., lifetime=...). """ # Allow register to be used either with or without arguments if obj is None: - def decorated(decorated_obj: AutowireTarget) -> AutowireTarget: - self.__register_object(decorated_obj, qualifier, lifetime) - + def decorated(decorated_obj: __T) -> __T: + self.register(decorated_obj, qualifier=qualifier, lifetime=lifetime) return decorated_obj return decorated - self.__register_object(obj, qualifier, lifetime) + if isinstance(obj, type): + self.__service_registry.register_service(obj, qualifier, lifetime) + return obj + + if callable(obj): + self.__service_registry.register_factory(obj, lifetime) + return obj - return obj + raise InvalidRegistrationTypeError(obj) @property def context(self) -> InitializationContext: @@ -127,17 +148,6 @@ def params(self) -> ParameterBag: """Parameter bag associated with this container.""" return self.__params - def __register_object( - self, - obj: AutowireTarget, - qualifier: ContainerProxyQualifierValue, - lifetime: ServiceLifetime, - ) -> None: - if isinstance(obj, type): - self.__service_registry.register_service(obj, qualifier, lifetime) - else: - self.__service_registry.register_factory(obj, lifetime) - def autowire(self, fn: AnyCallable) -> AnyCallable: """Automatically inject resources from the container to the decorated methods.