-
Notifications
You must be signed in to change notification settings - Fork 92
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
Open
eirikur-nc
wants to merge
9
commits into
python-injector:master
Choose a base branch
from
eirikurt:multibind-for-plugins
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
39dda31
Make it possible to register types with multibind, which will then be…
eirikur-nc 320f23b
Augment tests with callable providers
eirikur-nc 59dc6a5
Raise a more helpful error if an attempt is made to multi-bind to a n…
eirikur-nc 4bc95be
refactor a bit
eirikur-nc d469f2a
Update documentation for multibind
eirikur-nc ae199e6
test callable provider for dict multibind as well
eirikur-nc 7a0b3de
Add newlines to avoid horizontal scrolling in docs
eirikur-nc 93bbd4a
rewrite to work around mypy quirk
eirikur-nc cc4b8d0
format
eirikur-nc File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
||
|
||
: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) | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.