1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
|
# python-inject [](https://travis-ci.org/ivankorobkov/python-inject)
Dependency injection the python way, the good way.
## Key features
* Fast.
* Thread-safe.
* Simple to use.
* Does not steal class constructors.
* Does not try to manage your application object graph.
* Transparently integrates into tests.
* Autoparams leveraging type annotations.
* Supports type hinting in Python 3.5+.
* Supports Python 3.9+ (`v5.*`), 3.5-3.8 (`v4.*`) and Python 2.7–3.5 (`v3.*`).
* Supports context managers.
## Python Support
| Python | Inject Version |
|---------|----------------|
| 3.9+ | 5.0+ |
| 3.6-3.8 | 4.1+, < 5.0 |
| 3.5 | 4.0 |
| < 3.5 | 3.* |
## Installation
Use pip to install the lastest version:
```bash
pip install inject
```
## Autoparams example
`@inject.autoparams` returns a decorator which automatically injects arguments into a function
that uses type annotations. This is supported only in Python >= 3.5.
```python
@inject.autoparams
def refresh_cache(cache: RedisCache, db: DbInterface):
pass
```
There is an option to specify which arguments we want to inject without attempts of
injecting everything:
```python
@inject.autoparams('cache', 'db')
def sign_up(name, email, cache: RedisCache, db: DbInterface):
pass
```
It is also acceptable to use explicit curly braces notation (`@inject.autoparams()`)
for non-parameterized decorations — it will be treated the same as `@inject.autoparams`.
## Step-by-step example
```python
# Import the inject module.
import inject
# `inject.instance` requests dependencies from the injector.
def foo(bar):
cache = inject.instance(Cache)
cache.save('bar', bar)
# `inject.params` injects dependencies as keyword arguments or positional argument.
# Also you can use @inject.autoparams in Python 3.5, see the example above.
@inject.params(cache=Cache, user=CurrentUser)
def baz(foo, cache=None, user=None):
cache.save('foo', foo, user)
# this can be called in different ways:
# with injected arguments
baz('foo')
# with positional arguments
baz('foo', my_cache)
# with keyword arguments
baz('foo', my_cache, user=current_user)
# `inject.param` is deprecated, use `inject.params` instead.
@inject.param('cache', Cache)
def bar(foo, cache=None):
cache.save('foo', foo)
# `inject.attr` creates properties (descriptors) which request dependencies on access.
class User(object):
cache = inject.attr(Cache)
def __init__(self, id):
self.id = id
def save(self):
self.cache.save('users', self)
@classmethod
def load(cls, id):
return cls.cache.load('users', id)
# Create an optional configuration.
def my_config(binder):
binder.bind(Cache, RedisCache('localhost:1234'))
# Configure a shared injector.
inject.configure(my_config)
# Instantiate User as a normal class. Its `cache` dependency is injected when accessed.
user = User(10)
user.save()
# Call the functions, the dependencies are automatically injected.
foo('Hello')
bar('world')
```
## Context managers
Binding a class to an instance of a context manager (through `bind` or `bind_to_constructor`)
or to a function decorated as a context manager leads to the context manager to be used as is,
not via with statement.
```python
@contextlib.contextmanager
def get_file_sync():
obj = MockFile()
yield obj
obj.destroy()
@contextlib.asynccontextmanager
async def get_conn_async():
obj = MockConnection()
yield obj
obj.destroy()
def config(binder):
binder.bind_to_provider(MockFile, get_file_sync)
binder.bind(int, 100)
binder.bind_to_provider(str, lambda: "Hello")
binder.bind_to_provider(MockConnection, get_conn_sync)
inject.configure(config)
@inject.autoparams()
def example(conn: MockConnection, file: MockFile):
# Connection and file will be automatically destroyed on exit.
pass
```
## Usage with Django
Django can load some modules multiple times which can lead to
`InjectorException: Injector is already configured`. You can use `configure(once=True)` which
is guaranteed to run only once when the injector is absent:
```python
import inject
inject.configure(my_config, once=True)
```
## Testing
In tests use `inject.configure(callable, clear=True)` to create a new injector on setup,
and optionally `inject.clear()` to clean up on tear down:
```python
class MyTest(unittest.TestCase):
def setUp(self):
inject.configure(lambda binder: binder
.bind(Cache, MockCache()) \
.bind(Validator, TestValidator()),
clear=True)
def tearDown(self):
inject.clear()
```
## Composable configurations
You can reuse configurations and override already registered dependencies to fit the needs
in different environments or specific tests.
```python
def base_config(binder):
# ... more dependencies registered here
binder.bind(Validator, RealValidator())
binder.bind(Cache, RedisCache('localhost:1234'))
def tests_config(binder):
# reuse existing configuration
binder.install(base_config)
# override only certain dependencies
binder.bind(Validator, TestValidator())
binder.bind(Cache, MockCache())
inject.configure(tests_config, allow_override=True, clear=True)
```
## Thread-safety
After configuration the injector is thread-safe and can be safely reused by multiple threads.
## Binding types
**Instance** bindings always return the same instance:
```python
redis = RedisCache(address='localhost:1234')
def config(binder):
binder.bind(Cache, redis)
```
**Constructor** bindings create a singleton on injection:
```python
def config(binder):
# Creates a redis cache singleton on first injection.
binder.bind_to_constructor(Cache, lambda: RedisCache(address='localhost:1234'))
```
**Provider** bindings call the provider on injection:
```python
def get_my_thread_local_cache():
pass
def config(binder):
# Executes the provider on each injection.
binder.bind_to_provider(Cache, get_my_thread_local_cache)
```
**Runtime** bindings automatically create singletons on injection, require no configuration.
For example, only the `Config` class binding is present, other bindings are runtime:
```python
class Config(object):
pass
class Cache(object):
config = inject.attr(Config)
class Db(object):
config = inject.attr(Config)
class User(object):
cache = inject.attr(Cache)
db = inject.attr(Db)
@classmethod
def load(cls, user_id):
return cls.cache.load('users', user_id) or cls.db.load('users', user_id)
inject.configure(lambda binder: binder.bind(Config, load_config_file()))
user = User.load(10)
```
## Disabling runtime binding
Sometimes runtime binding leads to unexpected behaviour. Say if you forget
to bind an instance to a class, `inject` will try to implicitly instantiate it.
If an instance is unintentionally created with default arguments it may lead to
hard-to-debug bugs. To disable runtime binding and make sure that only
explicitly bound instances are injected, pass `bind_in_runtime=False` to `inject.configure`.
In this case `inject` immediately raises `InjectorException` when the code
tries to get an unbound instance.
## Keys
It is possible to use any hashable object as a binding key. For example:
```python
import inject
inject.configure(lambda binder: \
binder.bind('host', 'localhost') \
binder.bind('port', 1234))
```
## Why no scopes?
I've used Guice and Spring in Java for a lot of years, and I don't like their scopes.
`python-inject` by default creates objects as singletons. It does not need a prototype scope
as in Spring or NO_SCOPE as in Guice because `python-inject` does not steal your class
constructors. Create instances the way you like and then inject dependencies into them.
Other scopes such as a request scope or a session scope are fragile, introduce high coupling,
and are difficult to test. In `python-inject` write custom providers which can be thread-local,
request-local, etc.
For example, a thread-local current user provider:
```python
import inject
import threading
# Given a user class.
class User(object):
pass
# Create a thread-local current user storage.
_LOCAL = threading.local()
def get_current_user():
return getattr(_LOCAL, 'user', None)
def set_current_user(user):
_LOCAL.user = user
# Bind User to a custom provider.
inject.configure(lambda binder: binder.bind_to_provider(User, get_current_user))
# Inject the current user.
@inject.params(user=User)
def foo(user):
pass
```
## Links
* Project: https://github.com/ivankorobkov/python-inject
## License
Apache License 2.0
## Contributors
* Ivan Korobkov [@ivankorobkov](https://github.com/ivankorobkov)
* Jaime Wyant [@jaimewyant](https://github.com/jaimewyant)
* Sebastian Buczyński [@Enforcer](https://github.com/Enforcer)
* Oleksandr Fedorov [@Fedorof](https://github.com/Fedorof)
* cselvaraj [@cselvaraj](https://github.com/cselvaraj)
* 陆雨晴 [@SixExtreme](https://github.com/SixExtreme)
* Andrew William Borba [@andrewborba10](https://github.com/andrewborba10)
* jdmeyer3 [@jdmeyer3](https://github.com/jdmeyer3)
* Alex Grover [@ajgrover](https://github.com/ajgrover)
* Harro van der Kroft [@wisepotato](https://github.com/wisepotato)
* Samiur Rahman [@samiur](https://github.com/samiur)
* 45deg [@45deg](https://github.com/45deg)
* Alexander Nicholas Costas [@ancostas](https://github.com/ancostas)
* Dmitry Balabka [@dbalabka](https://github.com/dbalabka)
* Dima Burmistrov [@pyctrl](https://github.com/pyctrl)
|