-
Notifications
You must be signed in to change notification settings - Fork 94
feat: Support instantiation with multibind #277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
39dda31
320f23b
59dc6a5
4bc95be
d469f2a
ae199e6
7a0b3de
93bbd4a
cc4b8d0
2b88182
90c0ae0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ | |
cast, | ||
Dict, | ||
Generic, | ||
get_args, | ||
Iterable, | ||
List, | ||
Optional, | ||
|
@@ -244,6 +245,10 @@ class UnknownArgument(Error): | |
"""Tried to mark an unknown argument as noninjectable.""" | ||
|
||
|
||
class InvalidInterface(Error): | ||
"""Cannot bind to the specified interface.""" | ||
|
||
|
||
class Provider(Generic[T]): | ||
"""Provides class instances.""" | ||
|
||
|
@@ -355,7 +360,7 @@ class MultiBindProvider(ListOfProviders[List[T]]): | |
return sequences.""" | ||
|
||
def get(self, injector: 'Injector') -> List[T]: | ||
return [i for provider in self._providers for i in provider.get(injector)] | ||
return [i for provider in self._providers for i in _ensure_iterable(provider.get(injector))] | ||
|
||
|
||
class MapBindProvider(ListOfProviders[Dict[str, T]]): | ||
|
@@ -368,6 +373,16 @@ def get(self, injector: 'Injector') -> Dict[str, T]: | |
return map | ||
|
||
|
||
@private | ||
class KeyValueProvider(Provider[Dict[str, T]]): | ||
def __init__(self, key: str, inner_provider: Provider[T]) -> None: | ||
self._key = key | ||
self._provider = inner_provider | ||
|
||
def get(self, injector: 'Injector') -> Dict[str, T]: | ||
return {self._key: self._provider.get(injector)} | ||
|
||
|
||
_BindingBase = namedtuple('_BindingBase', 'interface provider scope') | ||
|
||
|
||
|
@@ -468,7 +483,7 @@ def bind( | |
def multibind( | ||
self, | ||
interface: Type[List[T]], | ||
to: Union[List[T], Callable[..., List[T]], Provider[List[T]]], | ||
to: Union[List[Union[T, Type[T]]], Callable[..., List[T]], Provider[List[T]], Type[T]], | ||
scope: Union[Type['Scope'], 'ScopeDecorator', None] = None, | ||
) -> None: # pragma: no cover | ||
pass | ||
|
@@ -477,7 +492,7 @@ def multibind( | |
def multibind( | ||
self, | ||
interface: Type[Dict[K, V]], | ||
to: Union[Dict[K, V], Callable[..., Dict[K, V]], Provider[Dict[K, V]]], | ||
to: Union[Dict[K, Union[V, Type[V]]], Callable[..., Dict[K, V]], Provider[Dict[K, V]]], | ||
scope: Union[Type['Scope'], 'ScopeDecorator', None] = None, | ||
) -> None: # pragma: no cover | ||
pass | ||
|
@@ -489,22 +504,25 @@ def multibind( | |
|
||
A multi-binding contributes values to a list or to a dictionary. For example:: | ||
|
||
binder.multibind(List[str], to=['some', 'strings']) | ||
binder.multibind(List[str], to=['other', 'strings']) | ||
injector.get(List[str]) # ['some', 'strings', 'other', 'strings'] | ||
binder.multibind(list[Interface], to=A) | ||
binder.multibind(list[Interface], to=[B, C()]) | ||
injector.get(list[Interface]) # [<A object at 0x1000>, <B object at 0x2000>, <C object at 0x3000>] | ||
|
||
binder.multibind(Dict[str, int], to={'key': 11}) | ||
binder.multibind(Dict[str, int], to={'other_key': 33}) | ||
injector.get(Dict[str, int]) # {'key': 11, 'other_key': 33} | ||
binder.multibind(dict[str, Interface], to={'key': A}) | ||
binder.multibind(dict[str, Interface], to={'other_key': B}) | ||
injector.get(dict[str, Interface]) # {'key': <A object at 0x1000>, 'other_key': <B object at 0x2000>} | ||
|
||
.. versionchanged:: 0.17.0 | ||
Added support for using `typing.Dict` and `typing.List` instances as interfaces. | ||
Deprecated support for `MappingKey`, `SequenceKey` and single-item lists and | ||
dictionaries as interfaces. | ||
|
||
:param interface: typing.Dict or typing.List instance to bind to. | ||
:param to: Instance, class to bind to, or an explicit :class:`Provider` | ||
subclass. Must provide a list or a dictionary, depending on the interface. | ||
:param interface: A generic list[T] or dict[str, T] type to bind to. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to use PEP 585 styled type hints. Support for them was introduced in Python 3.9. While this project still supports Python 3.8, I suspect it's only a matter of time until that support gets dropped since 3.8 has reached end-of-life. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that's fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here in the docstring, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I confirmed this. |
||
|
||
:param to: A list/dict to bind to, where the values are either instances or classes implementing T. | ||
Can also be an explicit :class:`Provider` or a callable that returns a list/dict. | ||
For lists, this can also be a class implementing T (e.g. multibind(list[T], to=A)) | ||
|
||
:param scope: Optional Scope in which to bind. | ||
""" | ||
if interface not in self._bindings: | ||
|
@@ -524,7 +542,27 @@ def multibind( | |
binding = self._bindings[interface] | ||
provider = binding.provider | ||
assert isinstance(provider, ListOfProviders) | ||
provider.append(self.provider_for(interface, to)) | ||
|
||
if isinstance(provider, MultiBindProvider) and isinstance(to, list): | ||
try: | ||
element_type = get_args(_punch_through_alias(interface))[0] | ||
except IndexError: | ||
raise InvalidInterface( | ||
f"Use typing.List[T] or list[T] to specify the element type of the list" | ||
) | ||
for element in to: | ||
provider.append(self.provider_for(element_type, element)) | ||
elif isinstance(provider, MapBindProvider) and isinstance(to, dict): | ||
try: | ||
value_type = get_args(_punch_through_alias(interface))[1] | ||
except IndexError: | ||
raise InvalidInterface( | ||
f"Use typing.Dict[K, V] or dict[K, V] to specify the value type of the dict" | ||
) | ||
for key, value in to.items(): | ||
provider.append(KeyValueProvider(key, self.provider_for(value_type, value))) | ||
else: | ||
provider.append(self.provider_for(interface, to)) | ||
|
||
def install(self, module: _InstallableModuleType) -> None: | ||
"""Install a module into this binder. | ||
|
@@ -696,6 +734,12 @@ def _is_specialization(cls: type, generic_class: Any) -> bool: | |
return origin is generic_class or issubclass(origin, generic_class) | ||
|
||
|
||
def _ensure_iterable(item_or_list: Union[T, List[T]]) -> List[T]: | ||
if isinstance(item_or_list, list): | ||
return item_or_list | ||
return [item_or_list] | ||
|
||
|
||
def _punch_through_alias(type_: Any) -> type: | ||
if ( | ||
sys.version_info < (3, 10) | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed these examples to illustrate the use of classes and objects, rather than strings. This improves consistency with the
bind
documentation.IMO, people reach for DI libraries to simplify object construction. Injecting strings or other primitive values, while possible, is less beneficial, at least in my experience.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree! The current handling of primitive values has always felt like a side effect to me, so removing that from the documentation is fine by me.