-
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
feat: Support instantiation with multibind #277
Conversation
… instantiated the the injector
…on-generic list/dict
: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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's fine.
injector/__init__.py
Outdated
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>] |
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.
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.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #277 +/- ##
==========================================
+ Coverage 95.62% 95.84% +0.21%
==========================================
Files 1 1
Lines 572 602 +30
Branches 97 103 +6
==========================================
+ Hits 547 577 +30
Misses 19 19
Partials 6 6 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Thanks for your contribution, and sorry for the slow handling of this. After a quick glance, I think it looks great. I will try to find the time to make a proper review of this and get this merged and released soon. |
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.
All good. Thanks!
I think this drops the ability to multibind a list of types, but I'm fine sacrificing that for this. Much appreciated!
: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 comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's fine.
: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 comment
The reason will be displayed to describe this comment to others. Learn more.
Here in the docstring, dict[str, T]
is mentioned, but it does not have to be a str
, right?
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 confirmed this.
injector/__init__.py
Outdated
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>] |
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.
Now this is merged, but I tried to add a few tests. However, I found that the following did not pass: def test_scopes_applies_to_multibound_type() -> None:
class A:
pass
def configure(binder: Binder) -> None:
binder.bind(A, to=A, scope=singleton)
binder.multibind(List[A], to=A)
injector = Injector([configure])
first_list = injector.get(List[A])
second_list = injector.get(List[A])
assert first_list[0] is second_list[0] But I think I would expect it to. What are your thoughts, @eirikur-nc? |
I'd just like to clarify that I still think it's useful in its current form. |
That's a good point. I'll admit, I was just focusing on getting class instantiation to work. I didn't pay much heed to scopes. The current implementation of binder.multibind(List[Plugin], to=PluginA, scope=singleton)
binder.multibind(List[Plugin], to=PluginB) then the list will be a singleton scoped binder.multibind(List[Plugin], to=PluginA)
binder.multibind(List[Plugin], to=PluginB, scope=singleton) then the list will not be scoped. So I guess there are a few questions that need to be considered
|
I've given this some more thought and I think the sensible thing is to mimic the behavior of Guice, since this library is inspired by its design. Judging from the Guice documentation, it seems like
In other words, the answers to my previous questions are Q: Should it be possible to scope the list/map? Q: Should the scope argument of Q: If a type is scoped ( Would you agree @davidparsson ? To take this forward, we should probably start by writing up an issue. I wouldn't mind taking a stab at this, but I can't really promise when I'll be able to. |
I totally agree. All three points seem reasonable and intuitive to me. I've opened #283, and I'll drop a line there before I start working on it. I think I'm open to releasing this without that, but having the support in place would be ideal. |
And by the way, thanks for your thoughts and research. Much appreciated! |
I kept thinking about this, and I like most aspects of it. One thing I'm not sure of is whether scopes applied to multibound types should apply to the type globally or not. If you're interrested in continuing the discussion, I think we should continue in #283. |
What
Make it possible to multibind to types/classes which then get instantiated when
injector.get(<MultiBoundType>)
is called. Support this both for lists and dictionaries. Update themultibind
documentation accordingly.Example:
Why
In modular software systems, it's often convenient to leverage plug-in patterns to allow a particular module to hook into certain events. The Guice documentation explains this nicely.
Personally, I have used this pattern for lifecycle listeners that get invoked whenever an application starts up and shuts down. Imagine that you have an application that supports different database back-ends and key-value stores. In such a scenario, we typically have separate DI modules for each database system and each key-value store, e.g.
The injector is then configured using the appropriate set of modules, depending on which database and cache we want to employ, e.g.
injector = Injector([PostgreSQLModule, RedisModule])
.Each of these modules can optionally register a lifecycle listener. Here's an example for a listener that closes the connection to redis on shutdown.
The application code then simply asks for all lifecycle listeners and invokes them on startup and shutdown. Here's an example of a FastAPI lifespan that does this
According to @alecthomas (see #121 (comment)) it was always the intention to support this behavior but up until now, users have had to employ workarounds to get this working.
Prior work
This is similar to #197 in that it solves the problem of multibinding to types. However, the API is quite different.
In #197 the proposal is to multibind to a
MultiClassProvider
like soThe implementation in this PR foregoes the need for a special wrapper, allowing one to mix and match classes and instances like so
Moreover, this PR also supports binding to classes without nesting them in lists
It also supports dictionary multibindings
Closes #121