218 lines
7.7 KiB
Markdown
218 lines
7.7 KiB
Markdown
Injector - Python dependency injection framework, inspired by Guice
|
||
===================================================================
|
||
|
||
[![image](https://secure.travis-ci.org/alecthomas/injector.svg?branch=master)](https://travis-ci.org/alecthomas/injector)
|
||
[![Coverage Status](https://coveralls.io/repos/github/alecthomas/injector/badge.svg?branch=master)](https://coveralls.io/github/alecthomas/injector?branch=master)
|
||
|
||
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 natura, 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 :ref:`modules <module>`.
|
||
|
||
If you're not sure what dependency injection is or you'd like to learn more about it see:
|
||
|
||
* [The Clean Code Talks - Don't Look For Things! (a talk by Miško Hevery)](
|
||
https://www.youtube.com/watch?v=RlfLCWKxHJ0)
|
||
* [Inversion of Control Containers and the Dependency Injection pattern (an article by Martin Fowler)](
|
||
https://martinfowler.com/articles/injection.html)
|
||
|
||
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 ommitted
|
||
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 instaniate the class just the same manually, if you want.
|
||
|
||
* No global state – you can have as many [Injector](https://injector.readthedocs.io/en/latest/api.html#injector.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:
|
||
|
||
```python
|
||
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](https://injector.readthedocs.io/en/latest/api.html#injector.Injector.get),
|
||
[Injector.create_object](https://injector.readthedocs.io/en/latest/api.html#injector.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](https://injector.readthedocs.io/en/latest/api.html#injector.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](https://github.com/python/mypy) to
|
||
type-check correctly the code using it.
|
||
|
||
### How to get Injector?
|
||
|
||
* GitHub (code repository, issues): https://github.com/alecthomas/injector
|
||
|
||
* PyPI (installable, stable distributions): https://pypi.python.org/pypi/injector. You can install it using pip:
|
||
|
||
```bash
|
||
pip install injector
|
||
```
|
||
|
||
* Documentation: http://injector.readthedocs.org
|
||
* Change log: http://injector.readthedocs.io/en/latest/changelog.html
|
||
|
||
Injector works with CPython 3.5+ and PyPy 3 implementing Python 3.5+.
|
||
|
||
A Quick Example
|
||
---------------
|
||
|
||
|
||
```python
|
||
>>> 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:
|
||
|
||
```python
|
||
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:
|
||
|
||
|
||
```python
|
||
>>> from injector import Module, provider, Injector, inject, singleton
|
||
|
||
```
|
||
|
||
We'll use an in-memory SQLite database for our example:
|
||
|
||
|
||
```python
|
||
>>> import sqlite3
|
||
|
||
```
|
||
|
||
And make up an imaginary `RequestHandler` class that uses the SQLite connection:
|
||
|
||
|
||
```python
|
||
>>> 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:
|
||
|
||
|
||
```python
|
||
>>> class Configuration:
|
||
... def __init__(self, connection_string):
|
||
... self.connection_string = connection_string
|
||
|
||
```
|
||
|
||
Next, we bind the configuration to the injector, using a module:
|
||
|
||
|
||
```python
|
||
>>> 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:
|
||
|
||
|
||
```python
|
||
>>> 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`:
|
||
|
||
|
||
```python
|
||
>>> 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:
|
||
|
||
|
||
```python
|
||
>>> 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
|