Giter Club home page Giter Club logo

injector's Introduction

Injector - Python dependency injection framework, inspired by Guice

image Coverage Status

Introduction

While dependency injection is easy to do in Python due to its support for keyword arguments, the ease with which objects can be mocked and its dynamic nature, a framework for assisting in this process can remove a lot of boiler-plate from larger applications. That's where Injector can help. It automatically and transitively provides dependencies for you. As an added benefit, Injector encourages nicely compartmentalised code through the use of modules.

If you're not sure what dependency injection is or you'd like to learn more about it see:

The core values of Injector are:

  • Simplicity - while being inspired by Guice, Injector does not slavishly replicate its API. Providing a Pythonic API trumps faithfulness. Additionally some features are omitted because supporting them would be cumbersome and introduce a little bit too much "magic" (member injection, method injection).

    Connected to this, Injector tries to be as nonintrusive as possible. For example while you may declare a class' constructor to expect some injectable parameters, the class' constructor remains a standard constructor – you may instantiate the class just the same manually, if you want.

  • No global state – you can have as many Injector instances as you like, each with a different configuration and each with different objects in different scopes. Code like this won't work for this very reason:

      class MyClass:
          @inject
          def __init__(t: SomeType):
              # ...
    
      MyClass()

    This is simply because there's no global Injector to use. You need to be explicit and use Injector.get, Injector.create_object or inject MyClass into the place that needs it.

  • Cooperation with static type checking infrastructure – the API provides as much static type safety as possible and only breaks it where there's no other option. For example the Injector.get method is typed such that injector.get(SomeType) is statically declared to return an instance of SomeType, therefore making it possible for tools such as mypy to type-check correctly the code using it.

  • The client code only knows about dependency injection to the extent it needs –  inject, Inject and NoInject are simple markers that don't really do anything on their own and your code can run just fine without Injector orchestrating things.

How to get Injector?

Injector works with CPython 3.8+ and PyPy 3 implementing Python 3.8+.

A Quick Example

>>> from injector import Injector, inject
>>> class Inner:
...     def __init__(self):
...         self.forty_two = 42
...
>>> class Outer:
...     @inject
...     def __init__(self, inner: Inner):
...         self.inner = inner
...
>>> injector = Injector()
>>> outer = injector.get(Outer)
>>> outer.inner.forty_two
42

Or with dataclasses if you like:

from dataclasses import dataclass
from injector import Injector, inject
class Inner:
    def __init__(self):
        self.forty_two = 42

@inject
@dataclass
class Outer:
    inner: Inner

injector = Injector()
outer = injector.get(Outer)
print(outer.inner.forty_two)  # Prints 42

A Full Example

Here's a full example to give you a taste of how Injector works:

>>> from injector import Module, provider, Injector, inject, singleton

We'll use an in-memory SQLite database for our example:

>>> import sqlite3

And make up an imaginary RequestHandler class that uses the SQLite connection:

>>> class RequestHandler:
...   @inject
...   def __init__(self, db: sqlite3.Connection):
...     self._db = db
...
...   def get(self):
...     cursor = self._db.cursor()
...     cursor.execute('SELECT key, value FROM data ORDER by key')
...     return cursor.fetchall()

Next, for the sake of the example, we'll create a configuration type:

>>> class Configuration:
...     def __init__(self, connection_string):
...         self.connection_string = connection_string

Next, we bind the configuration to the injector, using a module:

>>> def configure_for_testing(binder):
...     configuration = Configuration(':memory:')
...     binder.bind(Configuration, to=configuration, scope=singleton)

Next we create a module that initialises the DB. It depends on the configuration provided by the above module to create a new DB connection, then populates it with some dummy data, and provides a Connection object:

>>> class DatabaseModule(Module):
...   @singleton
...   @provider
...   def provide_sqlite_connection(self, configuration: Configuration) -> sqlite3.Connection:
...     conn = sqlite3.connect(configuration.connection_string)
...     cursor = conn.cursor()
...     cursor.execute('CREATE TABLE IF NOT EXISTS data (key PRIMARY KEY, value)')
...     cursor.execute('INSERT OR REPLACE INTO data VALUES ("hello", "world")')
...     return conn

(Note how we have decoupled configuration from our database initialisation code.)

Finally, we initialise an Injector and use it to instantiate a RequestHandler instance. This first transitively constructs a sqlite3.Connection object, and the Configuration dictionary that it in turn requires, then instantiates our RequestHandler:

>>> injector = Injector([configure_for_testing, DatabaseModule()])
>>> handler = injector.get(RequestHandler)
>>> tuple(map(str, handler.get()[0]))  # py3/py2 compatibility hack
('hello', 'world')

We can also verify that our Configuration and SQLite connections are indeed singletons within the Injector:

>>> injector.get(Configuration) is injector.get(Configuration)
True
>>> injector.get(sqlite3.Connection) is injector.get(sqlite3.Connection)
True

You're probably thinking something like: "this is a large amount of work just to give me a database connection", and you are correct; dependency injection is typically not that useful for smaller projects. It comes into its own on large projects where the up-front effort pays for itself in two ways:

  1. Forces decoupling. In our example, this is illustrated by decoupling our configuration and database configuration.
  2. After a type is configured, it can be injected anywhere with no additional effort. Simply @inject and it appears. We don't really illustrate that here, but you can imagine adding an arbitrary number of RequestHandler subclasses, all of which will automatically have a DB connection provided.

Footnote

This framework is similar to snake-guice, but aims for simplification.

© Copyright 2010-2013 to Alec Thomas, under the BSD license

injector's People

Contributors

515hikaru avatar alecthomas avatar davidparsson avatar dependabot[bot] avatar erikced avatar fuglede avatar gregeremeev avatar iley avatar jonathanmach avatar jstasiak avatar jwilk avatar ljnsn avatar m3drano avatar np-kyokyo avatar openglfreak avatar ozamosi avatar pavlomorozov avatar pohmelie avatar rutsky avatar sanjaysiddhanti avatar sobolevn avatar synapticarbors avatar thedrow avatar tobni avatar vuonghv avatar wooyek avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

injector's Issues

inj.get(AssistedBuilder['object_bind']).build() not working since 0.11.0

On Injector 0.9.1 we can do the following:

class DatabaseDriversModule(Module):
    def configure(self, binder):
        binder.bind('database_driver_sqlserver', to=Sqlserver, scope=singleton)

class DBFactory(object):
    def get_driver(self, driver, host, user, pass):
        injector = Injector([DatabaseDriversModule])
        return injector.get(AssistedBuilder('database_driver_' + driver.lower())).build(host, user, pass)

but the same code (changing "()" for "[]") isn't working on Injector >= 0.11.0:

File "/Library/Python/2.7/site-packages/slippinj-1.2.2-py2.7.egg/slippinj/databases/db_factory.py", line 26, in get_driver
    inj.get(AssistedBuilder['database_driver_' + driver.lower()]).build()
  File "/Library/Python/2.7/site-packages/injector-0.11.1-py2.7.egg/injector.py", line 1203, in build
    binding = binder.get_binding(None, key)
  File "/Library/Python/2.7/site-packages/injector-0.11.1-py2.7.egg/injector.py", line 459, in get_binding
    binding = self.create_binding(key.interface)
  File "/Library/Python/2.7/site-packages/injector-0.11.1-py2.7.egg/injector.py", line 395, in create_binding
    provider = self.provider_for(interface, to)
  File "/Library/Python/2.7/site-packages/injector-0.11.1-py2.7.egg/injector.py", line 430, in provider_for
    elif isinstance(interface, (tuple, type)) and isinstance(to, interface):
  File "/Library/Python/2.7/site-packages/typing.py", line 178, in __instancecheck__
    raise TypeError("Forward references cannot be used with isinstance().")
TypeError: Forward references cannot be used with isinstance().

It works when explicitly setting the class (AssistedBuilder[Sqlserver]) but not when using binds. The reason seems to be related to the _ForwardRef that the injector.py is returning:

0.9.1
inj.get(AssistedBuilder('database_driver_' + driver.lower())) -> _AssistedBuilder(interface='database_driver_sqlserver', cls=None, callable=None)

0.11.0
inj.get(AssistedBuilder['database_driver_' + driver.lower()]) -> injector.AssistedBuilder<~T>[_ForwardRef('database_driver_sqlserver’)]

Most of our classes are built like this, could you give us some guidance about it?

Keyword arguments to @inject in future releases

I just read the changelog for 0.11.0 and saw that keyword arguments to the @inject decorator will be removed in favor of type annotations in a future release. Does that mean that you plan to drop Python 2.x support completely? If not what is the reason for dropping Python2/3 compatibility?

Question: Possible to have "named" instances

Hi there, first of all thanks for providing a great project here, coming from c# background, its been the backbone of all of my python projects so far.

At the moment I have a bit of an interesting problem to resolve and wondering if this is possible.
I have created a service layer project which takes a database object, which normally i just inject etc.

However for this new project, I have to show an overall system, that talks to two of these databases, each for a different company, but they have the same structure.

Since I have it separated out as a service project layer, I'm struggling to see how I can inject in my singleton database objects. ( two different ones) and how to differentiate when I want to talk to database a or database b.

Any Ideas?

Thanks

runtest.py is not in distribution package

Hi, I think in Pypi the distribution package doesn't contain runtest.py. This causes running test on distribution package failed. Is there any reason tests are excluded from distribution?

Issues with Python 3.6

I just updated from Python 3.5.2 to 3.6.0 and I'm having some issues with injector. It seems that when using injector with the new annotation syntax, Python 3.6 introduces some changes that make @injector.inject incompatible. See an example stack trace here:

  File "${HOME}/${PROJECT}/${SCRATCH}/execroot/simulation/bazel-out/local-py3-fastbuild/bin/main.runfiles/__main__/main.py", line 15, in main
    runner()
  File "${HOME}/${PROJECT}/simulation/simulation.py", line 140, in runner
    simulator = custom_injector.get(Simulation)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 707, in get
    result = scope_instance.get(key, binding.provider).get(self)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 142, in get
    return injector.create_object(self._cls)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 751, in create_object
    (), additional_kwargs, e, self._stack,)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 71, in reraise
    raise exception.with_traceback(tb)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 744, in create_object
    init(instance, **additional_kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 1079, in inject
    kwargs=kwargs
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 843, in call_with_injection
    owner_key=self_.__class__ if self_ is not None else callable.__module__,
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 58, in wrapper
    return function(*args, **kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 886, in args_to_inject
    instance = self.get(key.interface)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 707, in get
    result = scope_instance.get(key, binding.provider).get(self)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 58, in wrapper
    return function(*args, **kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 546, in get
    provider = InstanceProvider(provider.get(self.injector))
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 142, in get
    return injector.create_object(self._cls)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 751, in create_object
    (), additional_kwargs, e, self._stack,)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 71, in reraise
    raise exception.with_traceback(tb)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 744, in create_object
    init(instance, **additional_kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 1079, in inject
    kwargs=kwargs
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 843, in call_with_injection
    owner_key=self_.__class__ if self_ is not None else callable.__module__,
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 58, in wrapper
    return function(*args, **kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 886, in args_to_inject
    instance = self.get(key.interface)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 707, in get
    result = scope_instance.get(key, binding.provider).get(self)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 58, in wrapper
    return function(*args, **kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 546, in get
    provider = InstanceProvider(provider.get(self.injector))
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 142, in get
    return injector.create_object(self._cls)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 751, in create_object
    (), additional_kwargs, e, self._stack,)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 71, in reraise
    raise exception.with_traceback(tb)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 744, in create_object
    init(instance, **additional_kwargs)
  File "${HOME}/${PROJECT}/simulation/activity_distribution.py", line 343, in __init__
    do_merge=False, trace_file='trace_file')
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 1079, in inject
    kwargs=kwargs
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 843, in call_with_injection
    owner_key=self_.__class__ if self_ is not None else callable.__module__,
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 58, in wrapper
    return function(*args, **kwargs)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 886, in args_to_inject
    instance = self.get(key.interface)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 707, in get
    result = scope_instance.get(key, binding.provider).get(self)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 142, in get
    return injector.create_object(self._cls)
  File "${HOME}/${VIRTUALENV}/lib/python3.6/site-packages/injector.py", line 728, in create_object
    instance = cls.__new__(cls)
  File "/usr/local/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/python3.6/typing.py", line 1126, in __new__
    return _generic_new(cls.__next_in_mro__, cls, *args, **kwds)
  File "/usr/local/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/python3.6/typing.py", line 1095, in _generic_new
    obj.__init__(*args, **kwds)
TypeError: __init__() missing 2 required positional arguments: 'injector' and 'target'

The Python binary comes from Homebrew and it is running on macOS 10.12.2, although that should not be a problem. The exact same code runs smoothly on Python 3.5.2. I'm trying to debug further.

Factory isn't called during injection

Greetings!

I'm actually relatively new to Python in general so I'm having trouble using a factory for the logger (so I can add handlers, set the log level, etc).

Here's the simple state:

from injector import Module, Key, provider, Injector, inject, singleton
from logging import Logger

import logging

class TestApp:
    @inject
    def __init__(self, logger: Logger):
        self.logger = logger

    def run(self):
        self.logger.info('Foo')

@singleton
class LoggerFactory():
    def __init__(self):
    # Constructor
        self.logger = None

    @provider
    def build(self) -> Logger:
    # Build an instance of the logger
        if self.logger is not None:
            return self.logger

        self.logger = logging.getLogger('testapp')

        return self.logger

def bootstrapServices(binder):
    binder.bind(LoggerFactory, to=Logger)

if __name__ == '__main__':
    injector = Injector(bootstrapServices)
    application = injector.get(TestApp)
    application.run()

I expect the factory to be called for dependency requests for Logger but instead an error is thrown: injector.CallError: Call to Logger.__init__() failed: __init__() missing 1 required positional argument: 'name' (injection stack: [<class '__main__.TestApp'>])

Thanks for your work!

When using BoundKey inner injections are ignored

The following test demonstrates the problem:

>>> from injector import *
>>> class A(object):
...     @inject(x=InstanceProvider(0))
...     def __init__(self, x=-1, a=-2):
...             print x, a
... 
>>> inj = Injector()
>>> inj.get(A)
0 -2
<__main__.A object at 0x1f977d0>
>>> inj.get(BoundKey(A, a=InstanceProvider(1)))
-1 1
<__main__.A object at 0x1f97850>

The expected output in the last case should be 0 1, right?

@noninjectable() accepts invalid arguments

@noninjectable accepts invalid arguments. Consider the example below:

class A:
    @inject
    @noninjectable('b, c')
    def __init__(self, b: str, c: int):
        self.b = b
        self.c = c

It may seem valid, but 'b, c' should be 'b', 'c'. It would be great if an exception would be raised if @noninjectable has been passed arguments that does not exist on the called method/function. Currently it's easy to make mistakes, especially for types that have defined default values such as str and int (which also typically are the ones you'd like to have non-injectable).

Do we (still) need binding key annotations?

It seems to me that binding annotations

  1. Don't work with @inject
  2. Introduce unnecessary complexity into the API, I haven't used them even once (look at point 1)

This is connected to readme/doc simplification that I've been thinking about lately.

Some ideas on implementation

Hi there, Just some background on myself, I've been a c# guy for quite a few years, but have just started to move across to python the last couple of months.

I started on python 3, and am used to using other DI frameworks in c#, using constructor injection.

I have a library which does all of my business logic, and uses use_annotations, to inject. All works fine.
One of my objects I pass in is my Database object.
I now have a need to use the same library ,but for two different databases. Each database is for a different company, but I'll need to merge data from both.

I just can't seem to get my head around how I can achieve this. At this stage I'm using flask injector, and for example have the following setup:

def configure(binder):
    db = CLDB(db_username=app.config['QUANTUM_USERNAME'],
              dsn=app.config['QUANTUM_DSN'],
              db_password=app.config['QUANTUM_PASSWORD'],
              logger=app.logger,
              quantum_username=app.config['QUANTUM_MASQUERADE_USER'])

    # set it up as a singleton.
    binder.bind(CLDB,
                to=db,
                scope=singleton
                )

injector = Injector(auto_bind=True, 
                             modules=[configure], 
                             use_annotations=True)
FlaskInjector(app=app, 
                      injector=injector)

then I have a separate library that has classes like:

class MSCIssuer:

    def __init__(self,
                 db: CLDB,
                 emailer: Emailer,
                 logger: Logger):
        self._db = db
        self._emailer = emailer
        self.logger = logger

now say in my merged project I want to instantiate two instances of MSCIssuer.

One with CLDB object 1, and one with CLDB object 2, each with different configurations.

Since I haven't used guice before I'm kind of stuck :(

Thank you.

Deprecate @inject on Module.configure()

Hey @jstasiak,

I've found over time that injecting arguments to configure() methods/functions is often a bad idea, particularly for SequenceKey or MappingKey keys, or anything that depends on them.

The reason for this is that the value of the keys become dependent on the order of module initialization, as only modules that have already been configured will contribute to the keys. This results in non-deterministic behavior and bugs that are difficult to track down.

eg.

from injector import Injector, SequenceKey, inject


A = SequenceKey('A')


def conf(binder):
    binder.multibind(A, to=[])


@inject(a=A)
def conf1(binder, a):
    print a
    binder.multibind(A, to=[1])


@inject(a=A)
def conf2(binder, a):
    print a
    binder.multibind(A, to=[2])


Injector([conf, conf1, conf2])
Injector([conf, conf2, conf1])

So, my proposal is that we deprecate support for injected configure() and also module constructors (for the same reason) in the next release, then eventually explicitly deny support.

Thoughts?

Alec

New release soon?

It's been almost two years since the last release and there have been numerous bugfixes since then. I'd like to start using this in a project, but I'd very much prefer if there was a more recent release that encompasses all of these first.

Bug fix release

Can I have 13cad70 (tag 0.7.9) released on PyPI? I pushed it in separate branch, I'll port it to master when I have a moment.

Using default argument values in 0.13?

In previous versions (0.12.x) of Injector, we've used the bindings in @inject to allow using the default values when injecting objects automatically, and using the AssistedBuilder when overriding is wanted:

class MyClass:
    @inject(other=OtherClass)
    def __init__(self, other: OtherClass, title: str = 'Default title'):

In 0.13.x the ability to provide bindings like this has been removed. Is there any recommended way to make Injector use the default values, that also works when creating the object without using Injector?

Generalise calling injected functions/methods

Currently, injected methods and functions are called using some ad-hoc code in Injector.args_to_inject() and the @inject decorator. This should be generalised so that it can be eg. used to inject args into configure() function modules.

Injecting using mixins?

I've got an application where classes can inherit from multiple mixins, which have at least one dependency and may also have various helper methods. Using the @inject decorator on the class, this seemed rather straight forward:

from injector import Injector, inject

class SomeThing: pass
class OtherThing: pass

@inject(thing=SomeThing)
class ThingMixin: pass

@inject(other=OtherThing)
class OtherThingMixin: pass

class UserClass(ThingMixin, OtherThingMixin):
	def mymethod(self):
		print(self.thing)
		print(self.other)

This obviously doesn't work in recent versions where @inject on classes has been removed. It doesn't work in 0.10 either: AttributeError: 'UserClass' object has no attribute 'other'

Is there another way to allow this type of design without forcing the author of UserClass to declare every dependency in the constructor?

Multibinder requires instances.

I often use multi-binding where I want to use DI to create instances of the sequence -- hence I can't bind instances directly to the sequence key. My workaround is the utility class below, but it feels like this should be built into the framework (esp. since ListOfProviders is markes @Private)

binder.multibind(Test.SEQ_KEY, ClassProviderList([MyClass])) # Not an instance.

class ClassProviderList(ListOfProviders):
    def __init__(self, classes):
        super(ClassProviderList, self).__init__()
        for cls in classes:
            self.append(ClassProvider(cls))

New release

Hey, can we have tag 0.9.0 release please? Thanks in advance

ThreadLocalScope doesn't clear manually set Keys

Manually bound Keys end up caching a value you might expect to be thread local with ThreadLocalScope. The problem is that for manually bound items, a InstanceProvider(to) is created. Then later ThreadLocalScope does

provider = InstanceProvider(provider.get(self.injector))

Where provider.get just returns that to always. I would have expected something fancier to happen such that thread2 would never see the binding.

TEST_KEY = Key('TEST_KEY')

thread1
injector.binder.bind(TEST_KEY, 'test', threadlocal)
assert(injector.get(TEST_KEY) == 'test')

thread2

assert(injector.get(TEST_KEY) == 'test')

https://github.com/alecthomas/injector/blob/master/injector.py#L442
https://github.com/alecthomas/injector/blob/master/injector.py#L608

Not really sure what the best approach would be. I would have thought that the thread local scope sort of owned the binding created when I did

injector.binder.bind(TEST_KEY, 'test', threadlocal)

Annotation-based injections are ignored in methods

The following test demonstrates the problem (tested on Python 3.3):

>>> from injector import *
>>> inj = Injector(use_annotations=True)
>>> inj.binder.bind(str, "Hi")
>>> class A:
...    def __init__(self, a:str):
...        print(a)
...    def foo(self, a:str):     
...        print(a)
>>> a = inj.create_object(A)
Hi                       
>>> a.foo()                 
TypeError: foo() missing 1 required positional argument: 'a'

The __init__ injection is performed successfully, but the a.foo() call fails. Expected result is:

>>> a.foo()                 
Hi                       

The following works:

>>> from injector import *
>>> inj = Injector(use_annotations=True)
>>> inj.binder.bind(str, "Hi")
>>> class A:
...    def __init__(self, a:str):
...        print(a)
...    @inject(a=str)
...    def foo(self, a):     
...        print(a)
>>> a = inj.create_object(A)
Hi                       
>>> a.foo()                 
Hi                       

flask-restful is a hard dependency

I'm using flask-injector==0.10.1 alone with Flask-Restplus However Flask-Restful is a hard dependecy because otherwise the following error will be thrown:

api_next_1              |   File "/usr/local/lib/python3.6/site-packages/flask_injector.py", line 65, in wrap_fun
api_next_1              |     return wrap_class_based_view(fun, injector)
api_next_1              |   File "/usr/local/lib/python3.6/site-packages/flask_injector.py", line 156, in wrap_class_based_view
api_next_1              |     return wrap_flask_restful_resource(fun, flask_restful_api, injector)
api_next_1              |   File "/usr/local/lib/python3.6/site-packages/flask_injector.py", line 171, in wrap_flask_restful_resource
api_next_1              |     from flask_restful.utils import unpack
api_next_1              | ModuleNotFoundError: No module named 'flask_restful'

TypeError when using ClassAssistedBuilder with @inject

I've been trying to use the ClassAssistedBuilder and the AssistedBuilder as described in the docs, but I'm getting the following exception:

Traceback (most recent call last):
  File "reproduce.py", line 9, in <module>
    class NeedsUserUpdater(object):
  File "reproduce.py", line 11, in NeedsUserUpdater
    def __init__(self, builder: ClassAssistedBuilder[UserUpdater]):
  File "/Users/me/injector-reproduce/.venvs/3.5/lib/python3.5/site-packages/injector.py", line 1093, in inject
    bindings = _infer_injected_bindings(function)
  File "/Users/me/injector-reproduce/.venvs/3.5/lib/python3.5/site-packages/injector.py", line 967, in _infer_injected_bindings
    if _is_specialization(v, Union):
  File "/Users/me/injector-reproduce/.venvs/3.5/lib/python3.5/site-packages/injector.py", line 490, in _is_specialization
    (cls.__origin__ is generic_class or issubclass(cls.__origin__, generic_class))
  File "/usr/local/opt/pyenv/versions/3.5.3/lib/python3.5/typing.py", line 770, in __subclasscheck__
    raise TypeError("Unions cannot be used with issubclass().")
TypeError: Unions cannot be used with issubclass().

This is the code I'm running:

#!/usr/bin/env python3
from injector import ClassAssistedBuilder, Injector, inject


class UserUpdater:
    def __init__(self, user):
        self._user = user

class NeedsUserUpdater(object):
    @inject
    def __init__(self, builder: ClassAssistedBuilder[UserUpdater]):
        self.updater_builder = builder

    def method(self):
        updater = self.updater_builder.build(user=None)


injector = Injector()
instance = injector.get(NeedsUserUpdater)
instance.method()

Am I doing something wrong, or is it not working as intended?

Example from "Testing with Injector" doesn't work

I tried the example from Testing with Injector

import unittest
from injector import Module, with_injector, inject

class UsernameModule(Module):
    def configure(self, binder):
        binder.bind(str, 'Maria')

class TestSomethingClass(unittest.TestCase):
    @with_injector(UsernameModule())
    def setUp(self):
        pass

    @inject(username=str)
    def test_username(self, username):
        self.assertEqual(username, 'Maria')

But it didn't work:

======================================================================
ERROR: test_username (tests.TestSomethingClass)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tavianator/.virtualenvs/py3/lib/python3.6/site-packages/injector.py", line 1153, in inject
    return f(self_, *args, **kwargs)
TypeError: test_username() missing 1 required positional argument: 'username'

This is with injector 0.12.

Why singleton scope doesn't work as expected for create_child_injector

Hi,

I have a use case to take in user inputs (name, description) at runtime at a top-level class, while some lower-level class also needs to depend on user inputs.

While I tried with the below code, it throws Exception: Instantiation of <class 'injector.description'> prohibited - it is derived from BaseKey so most likely you should bind it to something.

But if I remove the @singleton annotation for class B, it works. Can someone explain to me why singleton scope doesn't work in the child_injector?

class base:
    def __init__(self, name):
        def configure(binder):
            binder.bind(Name, to=InstanceProvider(name))

        self.injector = Injector(configure)
        print('base: {}'.format(self.injector.get(Name)))


class A(base):
    def __init__(self, name, description):
        super(A, self).__init__(name)

        def configure(binder):
            binder.bind(Description, to=InstanceProvider(description))

        self.injector = self.injector.create_child_injector(configure)

        print(self.injector.get(B))


@singleton
class B:
    @inject(name=Name, description=Description)
    def __init__(self, name, description):
        self.name = name


A(name='hello', description='world')

===================================================================
Output:

base: hello
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 580, in get
    return self._context[key]
KeyError: (<class '__main__.B'>,)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/yipan/AlexaPrize-Python-Alexa-SDK/src/AlexaPrizeSocialbotCore/src/demo/di.py", line 191, in <module>
    A(name='hello', description='world')
  File "/Users/yipan/AlexaPrize-Python-Alexa-SDK/src/AlexaPrizeSocialbotCore/src/demo/di.py", line 180, in __init__
    print(self.injector.get(B))
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 743, in get
    result = scope_instance.get(key, binding.provider).get(self)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 60, in wrapper
    return function(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 582, in get
    provider = InstanceProvider(provider.get(self.injector))
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 144, in get
    return injector.create_object(self._cls)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 787, in create_object
    init(instance, **additional_kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 1152, in inject
    kwargs=kwargs
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 894, in call_with_injection
    owner_key=self_.__class__ if self_ is not None else callable.__module__,
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 60, in wrapper
    return function(*args, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 937, in args_to_inject
    instance = self.get(key.interface)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 743, in get
    result = scope_instance.get(key, binding.provider).get(self)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 144, in get
    return injector.create_object(self._cls)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 787, in create_object
    init(instance, **additional_kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/injector.py", line 1188, in __init__
    'so most likely you should bind it to something.' % (self.__class__,))
Exception: Instantiation of <class 'injector.description'> prohibited - it is derived from BaseKey so most likely you should bind it to something.

Process finished with exit code 1

code sample on thread scope

Hi,

I am trying to create a thread or http request scope. I read the document, but still have no idea hot to create it. Can you provide some sample code?

Thanks

Can't create PyQt objects annotated with @inject

If a class that inherits from a PyQt class, for example QObject, is annotated with @inject, it can't be created without using Injector. The getattr() in @inject raises a RuntimeError. Here's an example:

from PyQt5.QtCore import QObject
from injector import inject, Injector

class C(QObject):
    @inject
    def __init__(self):
        super().__init__()

injector = Injector()
c = injector.create_object(C)  # This works

c = C()  # This raises a RuntimeError

This is the traceback:

Traceback (most recent call last):
  File "example.py", line 12, in <module>
    c = C()
  File ".../lib/python3.6/site-packages/injector.py", line 1072, in inject
    injector = getattr(self_, '__injector__', None)
RuntimeError: super-class __init__() of type C was never called

I've used PyQt 5.8 and Injector 0.13.2.

Broken pypi 0.5.0 package

The most recent pypi injector package (http://pypi.python.org/packages/source/i/injector/injector-0.5.0.tar.gz#md5=65d032f6b4edb07625900b736c193e42) has no README.md which is needed by setup script to run.

-> % pip install --upgrade injector
Downloading/unpacking injector from http://pypi.python.org/packages/source/i/injector/injector-0.5.0.tar.gz#md5=65d032f6b4edb07625900b736c193e42
  Downloading injector-0.5.0.tar.gz
  Running setup.py egg_info for package injector
    Traceback (most recent call last):
      File "<string>", line 14, in <module>
      File "/home/kuba/ve/build/injector/setup.py", line 20, in <module>
        long_description = open('README.md').read()
    IOError: [Errno 2] No such file or directory: 'README.md'

Injecting partially applied functions is no longer supported.

I get this error when I tried to execute query, do you know why ?

see my code:

#MainProgram.py

from injector import Injector,inject
from QueryGetPersonalValueFromPersonById import QueryGetPersonalValueFromPersonById

class MainProgram:

	def StartProcess(self,id):
		

		injector = Injector()
        query  = injector.get(QueryGetPersonalValueFromPersonById)
		personalFieldValue = query.ReadPersonalFieldFromPersonId(id)
		
		print(personalFieldValue)


if __name__ == "__main__":
	mainProgram = MainProgram()
	mainProgram.StartProcess(5)

#QueryGetPersonalValueFromPersonById.py

from modelbd import Person
from PersonRepository import PersonRepository
from injector import Injector,inject

import numpy
import pandas

class QueryGetPersonalValueFromPersonById:
    
    id = None    
   
    @inject
    def __init__(self,personRepositpory: PersonRepository):        
         self.personRepo = personRepositpory
        
    def ReadPersonalFieldFromPersonId(self):       
         
        _person = self.personRepo.GetById(self.id)
        df = pandas.read_json(_person.PersonalField)
        
        return df

#PersonRepository.py

from base import Session
from modelbd import Person
from injector import Injector,inject


class PersonRepository:
    
    @inject
    def __init__(self,DBSession:Session):
        self.session = DBSession
     
    def GetById(self,id):
         _person = self.session.query(Person).filter_by(Id=id).first()
         return _person
         

#base.py

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

engine = create_engine('mssql+pymssql://:@ZZZ/ZZZ')
Session = sessionmaker(bind=engine)

Base = declarative_base()

#modelbd.py

from sqlalchemy import BigInteger, Column, DateTime, ForeignKey, Index, Integer, LargeBinary, Numeric, String, Table, Unicode, text
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.mssql.base import BIT
from sqlalchemy.ext.declarative import declarative_base


Base = declarative_base()
metadata = Base.metadata

class Person(Base):
    __tablename__ = 'Person'

    Id = Column(BigInteger, primary_key=True)
    Name = Column(String(200, 'Latin1_General_CI_AI'), nullable=False)
    LastName = Column(String(200, 'Latin1_General_CI_AI'))
    PersonalField= Column(String(200, 'Latin1_General_CI_AI'))
    

BoundProvider.__repr__() causes TypeError

BounderProvider.repr() looks like this:

def __repr__(self):
    return 'BoundProvider(%r, %r)' % (
        type(self).__name__, self._injector, self._interface)         

When called this causes

TypeError not all arguments were converted to String

This appears to be because two format arguments are specified (the two %r's) but three are given in the tuple. The way to fix this is to replace the hard-coded 'BoundProvider' with '%r' so the number of arguments match:

def __repr__(self):
    return '%r(%r, %r)' % (
        type(self).__name__, self._injector, self._interface)         

Remove setuptools from dependencies

Package itself do not use setuptools and should not be in install_requires section. Solutions:

  • Move setuptools from install_requires to setup_requires (which is available since 0.6a1)
  • Drop setuptools requirement at all, since your version requirement for setuptools is 0.6b1 and this version was released almost 10 years(!) ago, I hope no one use it nowadays.

Why this counts for us. We build debian packages from python packages and since modern python have setuptools bundled, we do not need to build debian package of setuptools, but since this dependency in injector we have to.

Missing getfullargspec in python 2.7

In Python 2.7, using @provider throws an error, because getfullargspec is None

I tried to add a shim, but since it uses the annotations attribute to determine the return type, this won't work in python 2.7

I am unsure how to fix this.

How to access injector instances in dependency instance

Hi,

I have a use case.

There are two dependencies:

class A(object):
    @inject()
    def init(self):
        pass
    
    def somefunc(self):
          b = B()
class B(object):
    @inject(c=C):
    def init(self, c):
         self.c = c

I get an injector instance
injector = Injector()

Then, I get an instance of A
a = injector.get(A)

I want to create a new instance of B, when I call somefunc of A every time.
How can I use injection to get an instance B in this situation?

Change log considerations

IMO it's unnecessary to have change log in both readme ("Recent Notable Changes", it's supposed to be kind of a change log as far as I understand it) and a separate file - I'd stick with one of them (at this moment I'm thinking separate file will be better).

Also - isn't versioning in CHANGES.md off by one? (it looks like it's on purpose but I'm not sure). For example Auto-convert README.md to RST for PyPi. is placed in 0.7.5 section but I'd say it's reasonable to name the section 0.7.6 instead as the change was introduced by that version.

@inject on an ABCMeta class does not work

Traceback (most recent call last):
  File "app.py", line 43, in <module>
    class Controller(object):
  File "/Users/alec/Projects/injector/injector.py", line 744, in multi_wrapper
    return method_wrapper(something)
  File "/Users/alec/Projects/injector/injector.py", line 701, in method_wrapper
    args = inspect.getargspec(f)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/inspect.py", line 813, in getargspec
    raise TypeError('{!r} is not a Python function'.format(func))
TypeError: <class '__main__.Controller'> is not a Python function

Exception handling

When something isn't bound the following exception is thrown in BaseKey:

raise Exception('Instantiation of %s prohibited ...)

I think this is run in a different Thread, so in my case, the main Flask app isn't able to catch it, so I have no indication (other than logs) that an error has occurred (esp. since injector is lazy -- with no eager option -- so bindings happen after the server has started).

Is there a workaround?

Too hard to find unbound keys.

When something isn't bound it's very difficult to find what is missing. The error looks like this (after 30 lines of injector.py code):

TypeError: init() takes exactly 2 arguments (1 given)

There is no mention to the Key or class that is missing.

It would also be really useful to be able to list all bound (and non-bound) keys.

Support eager singleton

There doesn't seem to be a way to bind eager singletons. The only reference is in the Module's provides method, but this is unreferenced.

def provides(interface, scope=None, eager=False):

Singleton scope leaks instances from child injectors

Show me the code

This test fails, and IMO it shouldn't according to SingletonScope docs, instances are stored per injector so I'd expect this test to pass with no trouble

class TestChildScope(unittest.TestCase):
    def test_child_scope(self):
        TestKey = Key('test')
        TestKey2 = Key('test2')

        def parent_module(binder):
            binder.bind(TestKey, to=object, scope=singleton)

        def first_child_module(binder):
            binder.bind(TestKey2, to=object, scope=singleton)

        def second_child_module(binder):
            binder.bind(TestKey2, to='marker', scope=singleton)

        injector = Injector(modules=[parent_module])
        first_child_injector = injector.create_child_injector(modules=[first_child_module])
        second_child_injector = injector.create_child_injector(modules=[second_child_module])

        self.assertIsNot(first_child_injector.get(TestKey2), second_child_injector.get(TestKey2))

New release

Can we have most recent changes released on PyPI?

Question: Testing with injector

Hi,

Thanks for this great project, it adds really nice separation to my code. But now I want to unittest everything and there are two cases:

  1. In documentation you show that we should use with_injector but it is depreciated and is spewing warnings in my tests
  2. Using with_injector I create mock service and unittest component which uses this service. But I would like to assert that mocked service was called with correct parameters. How can I obtain reference to mocked service? For now it seems I have to access self.__injector__ which seems a little hacky. Is there another way? Is there planned another way?

CallError wrapping catches legitimate call errors

class A(object):
  @inject(a=int)
  def __init__(self, a):
    max()

def configure(binder):
  binder.bind(int, to=13)


i = Injector([configure])
i.get(A)

raises

Traceback (most recent call last):
  File "t.py", line 29, in <module>
    i.get(A)
  File "/Users/alec/Projects/injector/injector.py", line 526, in get
    return scope_instance.get(key, binding.provider).get()
  File "/Users/alec/Projects/injector/injector.py", line 120, in get
    return self.injector.create_object(self._cls)
  File "/Users/alec/Projects/injector/injector.py", line 542, in create_object
    instance.__init__(**additional_kwargs)
  File "/Users/alec/Projects/injector/injector.py", line 720, in inject
    reraise(CallError(self_, f, args, dependencies, e))
  File "/Users/alec/Projects/injector/injector.py", line 52, in reraise
    exec('raise exception.__class__, exception, tb')
  File "/Users/alec/Projects/injector/injector.py", line 718, in inject
    return f(self_, *args, **dependencies)
  File "t.py", line 20, in __init__
    max()
injector.CallError: Call to <__main__.A object at 0x103778950>.__init__(a=13) failed: max expected 1 arguments, got 0

How to prioritize multibinds?

I didn't see any mention of this in the docs and I haven't dug through the code thoroughly so feel free to point me at something I've potentially missed and tell me I've over thought this.

The use case here is allowing certain implementations to have a higher or lower priority based when dealing with non-deterministic registration.

Let's say I have some interface:

class AuthenticationHandler(ABC):
    def authenticate(self, identifier: str, secret: str) -> Optional[User]:
        pass

And two implementations: DefaultAuthHandler and LDAPAuthHandler. This is easy enough to prioritize if I am the only one registering these into the application:

# see injector #45
# these are purposefully decomposed for sake of example
binder.multibind(AuthenticationHandler, to=ClassProviderList([LDAPAuthHandler]))
binder.multibind(AuthenticationHandler, to=ClassProviderList([DefaultAuthHandler]))

However, if I provide a plugin hook for users to provide their own modules, I can no longer guarantee order. At most I can guarantee that either my implementations always come before or after their implementations, but they can't be interleaved.

A user may want to provide an implementation that uses /etc/shadow as an auth source but have it come in between my LDAP and Default implementations.

I'd like to know if there is either support for this in injector. Barring that, it would seem that a custom provider would be the most straightforward approach:

class PrioritizedProvider(ClassProvider):
    def __init__(self, cls, priority: int=0):
        super().__init__(cls)
        self.priority = priority

However, this would also require being able to provide a custom provider inside multibind that can handle sorting these providers, something along the lines of:

_default_key = lambda provider: getattr(provider, 'priority', 0)

class PrioritizedListOfProviders(ListOfProviders):
    def __init__(self, ordering=None):
        super().__init__()
        self.__sorted = False
        self.__ordering = ordering or _default_key

    def append(self, provider):
        super().append(provider)
        self.__sorted = False

    def get(self, injector):
        if not self.__sorted:
            self._sort_providers()
       return super().get(injector)

    def _sort_providers(self):
        # not thread safe
        self._providers = sorted(self._providers, key=self.__ordering)

But since multibind hard codes its uses of MultiBindProvider and MapBindProvider it's impossible to supply a custom implementation of ListOfProviders (well...not impossible, just gross) and supplying the customized provider to multibind itself would only cause that particular chunk to be prioritized at resolution.

Thoughts? Did I overlook something or is this a potential addition to this library?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.