Skip to content

Commit

Permalink
Make container.register type hints work with mypy/pyright and not obs…
Browse files Browse the repository at this point in the history
…cure the return type
  • Loading branch information
maldoinc committed Nov 25, 2023
1 parent c5b65c7 commit f57e7f6
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 27 deletions.
10 changes: 10 additions & 0 deletions test/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from wireup.errors import (
DuplicateQualifierForInterfaceError,
DuplicateServiceRegistrationError,
InvalidRegistrationTypeError,
UnknownQualifiedServiceRequestedError,
UnknownServiceRequestedError,
UsageOfQualifierOnUnknownObjectError,
Expand Down Expand Up @@ -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",
)
7 changes: 7 additions & 0 deletions wireup/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
64 changes: 37 additions & 27 deletions wireup/ioc/dependency_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -17,7 +18,6 @@
from .types import (
AnnotatedParameter,
AnyCallable,
AutowireTarget,
ContainerProxyQualifierValue,
EmptyContainerInjectionRequest,
ParameterWrapper,
Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down

0 comments on commit f57e7f6

Please sign in to comment.