injector icon indicating copy to clipboard operation
injector copied to clipboard

Singleton annotated class instances are separate for interface and implementation binding

Open sudopk opened this issue 3 years ago • 2 comments

Unless I am doing something wrong, I will expect following binding to get single instance, but I am getting two different instances:

Code (python3):

#!/usr/bin/env python3

import abc
import injector

class SomeInterface(metaclass=abc.ABCMeta):
  """Interface with multiple implementations."""

  @abc.abstractmethod
  def some_method(self) -> None:
    pass


@injector.singleton
class SomeImplementation(SomeInterface):
  """One implementation."""

  def __init__(self):
    print(f'Created instance: {self}')

  def some_method(self) -> None:
    pass

class SomeModule(injector.Module):
  """Injector module."""

  def configure(self, binder: injector.Binder) -> None:
    binder.bind(SomeImplementation)
    binder.bind(SomeInterface, to=SomeImplementation)


def test_singleton() -> None:
  inj = injector.Injector((SomeModule(),), auto_bind=False)

  # Following two create different instances. Expected to be same since the implementation is marked singleton
  print(inj.get(SomeInterface))
  print(inj.get(SomeImplementation))

  # These get the two instances created above.
  print(inj.get(SomeInterface))
  print(inj.get(SomeImplementation))


if __name__ == '__main__':
  test_singleton()

Output:

Created instance: <cli.cli_lib.SomeImplementation object at 0x7f69b1bb3ef0>
<cli.cli_lib.SomeImplementation object at 0x7f69b1bb3ef0>
Created instance: <cli.cli_lib.SomeImplementation object at 0x7f69b1bb3e80>
<cli.cli_lib.SomeImplementation object at 0x7f69b1bb3e80>
<cli.cli_lib.SomeImplementation object at 0x7f69b1bb3ef0>
<cli.cli_lib.SomeImplementation object at 0x7f69b1bb3e80>

sudopk avatar Mar 21 '21 19:03 sudopk

As far as I can think, following shouldn't be needed but I have tried, but no use:

  • Add @injector.inject to implementations __init__()
  • Add scope=injector.singleton to both binder.bind statements.

Note that auto_bind=False doesn't make any different; same behavior even if I don't set it and remove the line binder.bind(SomeImplementation)

sudopk avatar Mar 21 '21 19:03 sudopk

You're right and adding @inject to __init__() is not necessary (since there's nothing being injected there) and adding scope=singleton to Binder.bind() won't make a difference (since singleton is already in force, becauses the implementation (SomeImplementation) declares that.

To the question at hand: while possibly counterintuitive in this case this is an expected behavior. Scope is defined per binding (type -> type mapping) and we have two bindings here:

  • SomeInterface -> SomeImplementation
  • SomeImplementation -> SomeImplementation

If you really need this, in short term I suggest a workaround like:



class SomeModule(injector.Module):
    def configure(self, binder: injector.Binder) -> None:
        binder.bind(SomeImplementation)

    @provider
    def provide_someinterface(self, implementation: SomeImplementation) -> SomeInterface:
        return implementation

I'm open to revising this behavior long-term but I have no time to explore the design space and the associated consequences right now.

jstasiak avatar Mar 21 '21 20:03 jstasiak