python-injector/injector

Singletons for parent/child injectors are order-dependent

davidparsson opened this issue · 9 comments

Singleton classes are only the same instance for parent and child injectors if they are created on the parent injector first. This seems like a bug to me. Is that correct, or is this by design?

See the following example:

@singleton
class A: pass

@singleton
class B: pass

parent = Injector()
child = Injector(parent=parent)

assert parent.get(A) is child.get(A)  # Passes – Singletons are the same if the parent creates it first
assert child.get(B) is parent.get(B)  # Raises

If this is by design, what's the best workaround? Creating a module for the parent that creates the instance in it's configure() method works, but is very implicit, and creating a @provide method requires duplicating its dependencies.

To clarify: I would expect both asserts to pass, but even more important is that they both behaved the same.

The things is I can't recall right now what problem have I tried to solve with child/parent injectors and the design of that feature has always been somewhat half-baked.

There definitely is a problem with parent/child injector scopes interacting with weird ways.

I think what needs to happen to make this feature nice and dependable is:

  • Gathering some real-world use cases for a feature like that (so we know to what extent parent and child injectors should interact with each other)
  • Figuring out how what Guice does in this scenario

I found this google/guice#1084 (comment) which in essence states:

for child injectors we intentionally try to resolve jit bindings in parents first

Doing that for Injector would make a lot of sense.

We primarily use child injectors to apply different modules and explicit binds, which is really useful.

That said, there are definitely cases that need careful handling here. For example a singleton that has dependencies on a type only provided or overridden by a child injector.

After some consideration, it seems to me the most reasonable thing to do when getting a singleton would be to start looking for a provider, starting at the current child and then upwards among parents. If no provider is found, the parent closest to the "root" that can satisfy all dependencies should create the singleton.

I haven't confirmed that this exact algorithm is used in Guice, but it resonates with what I've seen.

Does that sound reasonable to you as well, @jstasiak, or do we need to investigate more?

If you think that's a reasonable algorithm @jstasiak, I'll try to fix this and submit a PR.

Yeah this description sounds reasonable @davidparsson. On the other hand, I'm kind of wondering if having parent/child injectors is worth it, considering the complexity involved, haha. Are you actively using that feature?

Yes @jstasiak, I think it’s worth it, obviously. Is it the complexity of the implementation your writ worried about?

From a user’s perspective I think it provides a simple and intuitive piece of functionality. We use it quite extensively in a multitude of scenarios, including controlling the lifetime of binds and singletons, or overriding binds temporarily.

Fixed by #216.

@jstasiak, I'd very much appreciate if you could create a new release. Thanks! 🎈