# Autoinject
[![Documentation Status](https://readthedocs.org/projects/autoinject/badge/?version=latest)](https://autoinject.readthedocs.io/en/latest/?badge=latest)
[![CircleCI](https://dl.circleci.com/status-badge/img/gh/turnbullerin/autoinject/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/turnbullerin/autoinject/tree/main)
A clean, simple framework for automatically injecting dependencies into objects and functions
based around Python's type-hinting system. The framework provides caching of injectable objects,
though this may be disabled on a class-by-class basis. It also supports managing independent
caches for different contexts.
## Define Injectable Classes
```python
# Easy mode
from autoinject import injector
@injector.injectable
class MyInjectableClass:
# __init__() should have no additional required arguments
def __init__(self):
pass
# Hard mode, must specify the fully-qualified name of the class,
# but gain control over the arguments
@injector.register("example.MyInjectableClass", os.environ("MY_CONFIG_FILE"))
class MyInjectableClass:
def __init__(self, config_file):
# we receive os.environ("MY_CONFIG_FILE") as config_file here
# positional and keyword arguments to @injector.register() are supported
pass
```
## Inject Objects With Decorators
```python
# Decorate with @injector.inject for functions/methods:
@injector.inject
def inject_me(param1, param2, injected_param: MyInjectableClass):
# injected_param is set to an instance of MyInjectableClass
pass
# Omit the injected parameters when calling it:
inject_me("arg1", "arg2")
# For classes, use @injector.construct to set instance attributes
# based on the class attributes
class InjectMe:
injected_attribute: MyInjectableClass = None
@injector.construct
def __init__(self):
# self.injected_attribute is set to an instance of MyInjectableClass
pass
# No need to do anything special here:
obj = InjectMe()
# obj.injected_attribute is set by the decorator before __init__() is called.
```
## Specifying injected classes in tests
You can override injected classes in your unit tests using the `@injector.test_case()` decorator. This provides an
independent global context within the test case function and allows you to pass a map of objects to inject. For example,
```python
from autoinject import injector
# Your injectable original class
@injector.injectable_global
class ServiceConnection:
def execute(self) -> int:
# Real connection code here, returns HTTP status code
pass
# The class you want to write a test case for that uses the injectable class.
class UsesServiceConnection:
connection: ServiceConnection = None
@injector.construct
def __init__(self):
pass
def test_me(self) -> bool:
# Super simple, check if response code is under 400
resp_code = self.connection.execute()
return resp_code < 400
# Testing stuff
import unittest
# Stub for testing
class _StubServiceFixture:
def __init__(self, response_code):
self.response_code = response_code
def execute(self) -> int:
return self.response_code
# Test case
class TestUsesServiceConnection(unittest.TestCase):
@injector.test_case({
ServiceConnection: _StubServiceFixture(200)
})
def test_success_200(self):
test_obj = UsesServiceConnection() # this will use the injected objects now
self.assertTrue(test_obj.test_me())
@injector.test_case({
ServiceConnection: _StubServiceFixture(400)
})
def test_failure_400(self):
test_obj = UsesServiceConnection()
self.assertFalse(test_obj.test_me())
```
Read the [full documentation](https://autoinject.readthedocs.io/en/latest/?) for more details.
## Changelog
### v1.3.0
- The new `@injector.test_case()` decorator is available for use with unit testing frameworks. It executes the decorated
function with a different global and non-global context to ensure the independence of test functions. In addition, one
can override the injected classes to provide specific test fixtures. These are passed as a dict of either `type` objects
or fully qualified class names as strings as keys and either the `type` or class name as string (to create the object),
or an object or function to use as the injected object.
- A bug was fixed where exceptions within a context caused issues with the new contextvars integration.
### v1.2.0
- Contextvar-driven contexts are now respected by default
- Several wrappers exist to better support using contextvars. All of them provide for a separate set of injected
CONTEXT_CACHE dependencies. In addition, each is a wrapper around `@injector.inject`, so both are not needed.
- `@injector.with_contextvars`: Creates a new context that is a copy of the current one
- `@injector.with_same_contextvars`: Uses the current context
- `@injector.with_empty_contextvars`: Creates a new empty context
- When using a `with_contextvars` wrapper, you can inject the context object using type-hinting (e.g.
`ctx: contextvars.Context`). Note that this is actually an instance of `ContextVarsManager` which is a context manager
that delegates most functionality to the current `contextvars.Context` object with a few modifications:
- It provides the method `set(context_var, value) -> token` and the complementary `reset(context_var, token)` to
handle variable setting and resetting within the context manager.
- If the "same" context is used, these methods are equivalent to calling the methods directly on the `context_var`
- In all other cases, they are equivalent to calling `ctx.run(context_var.METHOD, *args, **kwargs)`.
- In essence, this makes sure the `set()` and `reset()` operations are performed in the context that the manager is
managing (since the manager doesn't run the inner block in the context).
- If the "same" context is used:
- `run()` will just directly call the function (it is in the current context essentially)
- `copy()` is an alias for `contextvars.copy_context()`
- Other functions besides `set()` and `reset()` make a copy of the current context and return the results of its
method. This copy is transient and remade each time, so modules making extensive use of it can call `copy()` and
check the copy.
- Note that, unlike the context manager, the decorators also RUN the inner code in the given context.
- Thread-handling was improved significantly and now also includes a wrapper function for `threading.Thread.run()` methods to
ensure clean-up (`@injector.as_thread_run()`). This also is a wrapper around `@injector.inject` so you can inject
variables into your `run()` method directly.
### v1.1.0
- Injectable objects may now define a `__cleanup__()` method which will be invoked when the global cache or context
cache is cleared.
- Note that `__cleanup__()` IS NOT INVOKED for one-time use objects at the moment, but this is planned as a feature.
### v1.0.1
- Inherited injectable class members are now supported properly
### v1.0.0
- Official initial release
- Added support for @injector.injectable_global which registers with GLOBAL cache instead of context-specific cache
- Added support for @injector.injectable_nocache which registers with NO_CACHE instead
- Added support for injector.override() as a helper function to replace one constructor with another.
- Added support for any constructor argument (e.g. via override() or register_constructor()) to be specified
by fully-qualified Python name (e.g. package.module.MyInjectableClass) to better support systems where injected
classes are specified by name.
- Fixed a bug whereby the cache wasn't cleared
### v0.2.2
- Fixed a bug for injection when a non-truthy default value needed to be used.
### v0.2.1
- Fixed a bug in Python 3.8 and 3.9 where `entry_points(group=?)` was not supported
### v0.2.0
- Objects with a cache strategy of `CONTEXT_CACHE` will now have separate instances within threads
- Added `injector.get()` as a fast way to get the object that would be injected (useful if operating outside of
a function or method)
- Added `injector.register_constructor()` as a wrapper to register a class in a non-decorated fashion
- Added the entry point `autoinject.injectables` to directly register injectable classes
- Added the entry point `autoinject.registrars`
- Support for overriding injectables and for injecting functions
- Added a `weight` keyword argument to `register()` and `register_construct()` to control overriding order
- There is now a `cleanup()` function in the `ContextManager()` class which triggers informant objects to check for
old items that are no longer needed. This was added mostly to support the thread-based context informant, since it
has no easy way of calling `destroy()` whenever the thread ends (unless one manually calls it). It is the best
practice if you can call `destroy()` directly whenever a context ceases to exist instead of relying on `cleanup()`.
Raw data
{
"_id": null,
"home_page": "https://github.com/turnbullerin/autoinject",
"name": "autoinject",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": "",
"keywords": "",
"author": "Erin Turnbull",
"author_email": "erin.a.turnbull@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/04/5d/8c4e54d14bcbc80ee6069110339af79b65d2748a02c9875644248e17adc1/autoinject-1.3.2.tar.gz",
"platform": null,
"description": "# Autoinject\r\n\r\n[![Documentation Status](https://readthedocs.org/projects/autoinject/badge/?version=latest)](https://autoinject.readthedocs.io/en/latest/?badge=latest)\r\n\r\n[![CircleCI](https://dl.circleci.com/status-badge/img/gh/turnbullerin/autoinject/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/turnbullerin/autoinject/tree/main)\r\n\r\nA clean, simple framework for automatically injecting dependencies into objects and functions\r\nbased around Python's type-hinting system. The framework provides caching of injectable objects,\r\nthough this may be disabled on a class-by-class basis. It also supports managing independent\r\ncaches for different contexts.\r\n\r\n## Define Injectable Classes\r\n\r\n```python\r\n# Easy mode\r\n\r\nfrom autoinject import injector\r\n\r\n@injector.injectable\r\nclass MyInjectableClass:\r\n\r\n # __init__() should have no additional required arguments\r\n def __init__(self):\r\n pass\r\n\r\n\r\n# Hard mode, must specify the fully-qualified name of the class,\r\n# but gain control over the arguments\r\n\r\n@injector.register(\"example.MyInjectableClass\", os.environ(\"MY_CONFIG_FILE\"))\r\nclass MyInjectableClass:\r\n\r\n def __init__(self, config_file):\r\n # we receive os.environ(\"MY_CONFIG_FILE\") as config_file here\r\n # positional and keyword arguments to @injector.register() are supported\r\n pass\r\n```\r\n \r\n## Inject Objects With Decorators\r\n \r\n```python\r\n# Decorate with @injector.inject for functions/methods:\r\n\r\n@injector.inject\r\ndef inject_me(param1, param2, injected_param: MyInjectableClass):\r\n # injected_param is set to an instance of MyInjectableClass\r\n pass\r\n\r\n# Omit the injected parameters when calling it:\r\n\r\ninject_me(\"arg1\", \"arg2\")\r\n\r\n\r\n# For classes, use @injector.construct to set instance attributes \r\n# based on the class attributes \r\nclass InjectMe:\r\n\r\n injected_attribute: MyInjectableClass = None\r\n\r\n @injector.construct\r\n def __init__(self):\r\n # self.injected_attribute is set to an instance of MyInjectableClass\r\n pass\r\n\r\n# No need to do anything special here:\r\nobj = InjectMe()\r\n# obj.injected_attribute is set by the decorator before __init__() is called.\r\n```\r\n\r\n## Specifying injected classes in tests\r\n\r\nYou can override injected classes in your unit tests using the `@injector.test_case()` decorator. This provides an \r\nindependent global context within the test case function and allows you to pass a map of objects to inject. For example,\r\n\r\n```python\r\n\r\nfrom autoinject import injector \r\n\r\n# Your injectable original class\r\n@injector.injectable_global \r\nclass ServiceConnection:\r\n \r\n def execute(self) -> int:\r\n # Real connection code here, returns HTTP status code\r\n pass\r\n \r\n\r\n# The class you want to write a test case for that uses the injectable class.\r\nclass UsesServiceConnection:\r\n \r\n connection: ServiceConnection = None\r\n \r\n @injector.construct \r\n def __init__(self):\r\n pass\r\n \r\n def test_me(self) -> bool:\r\n # Super simple, check if response code is under 400\r\n resp_code = self.connection.execute()\r\n return resp_code < 400\r\n \r\n \r\n# Testing stuff\r\nimport unittest\r\n \r\n# Stub for testing\r\nclass _StubServiceFixture:\r\n \r\n def __init__(self, response_code):\r\n self.response_code = response_code\r\n \r\n def execute(self) -> int:\r\n return self.response_code\r\n\r\n\r\n# Test case\r\nclass TestUsesServiceConnection(unittest.TestCase):\r\n\r\n @injector.test_case({\r\n ServiceConnection: _StubServiceFixture(200)\r\n })\r\n def test_success_200(self):\r\n test_obj = UsesServiceConnection() # this will use the injected objects now\r\n self.assertTrue(test_obj.test_me())\r\n\r\n\r\n @injector.test_case({\r\n ServiceConnection: _StubServiceFixture(400)\r\n })\r\n def test_failure_400(self):\r\n test_obj = UsesServiceConnection() \r\n self.assertFalse(test_obj.test_me())\r\n\r\n\r\n```\r\n\r\nRead the [full documentation](https://autoinject.readthedocs.io/en/latest/?) for more details.\r\n\r\n## Changelog\r\n\r\n### v1.3.0\r\n- The new `@injector.test_case()` decorator is available for use with unit testing frameworks. It executes the decorated\r\n function with a different global and non-global context to ensure the independence of test functions. In addition, one\r\n can override the injected classes to provide specific test fixtures. These are passed as a dict of either `type` objects \r\n or fully qualified class names as strings as keys and either the `type` or class name as string (to create the object), \r\n or an object or function to use as the injected object.\r\n- A bug was fixed where exceptions within a context caused issues with the new contextvars integration.\r\n\r\n### v1.2.0\r\n- Contextvar-driven contexts are now respected by default\r\n- Several wrappers exist to better support using contextvars. All of them provide for a separate set of injected \r\n CONTEXT_CACHE dependencies. In addition, each is a wrapper around `@injector.inject`, so both are not needed.\r\n - `@injector.with_contextvars`: Creates a new context that is a copy of the current one \r\n - `@injector.with_same_contextvars`: Uses the current context\r\n - `@injector.with_empty_contextvars`: Creates a new empty context\r\n- When using a `with_contextvars` wrapper, you can inject the context object using type-hinting (e.g. \r\n `ctx: contextvars.Context`). Note that this is actually an instance of `ContextVarsManager` which is a context manager\r\n that delegates most functionality to the current `contextvars.Context` object with a few modifications:\r\n - It provides the method `set(context_var, value) -> token` and the complementary `reset(context_var, token)` to\r\n handle variable setting and resetting within the context manager.\r\n - If the \"same\" context is used, these methods are equivalent to calling the methods directly on the `context_var`\r\n - In all other cases, they are equivalent to calling `ctx.run(context_var.METHOD, *args, **kwargs)`. \r\n - In essence, this makes sure the `set()` and `reset()` operations are performed in the context that the manager is\r\n managing (since the manager doesn't run the inner block in the context).\r\n - If the \"same\" context is used:\r\n - `run()` will just directly call the function (it is in the current context essentially)\r\n - `copy()` is an alias for `contextvars.copy_context()`\r\n - Other functions besides `set()` and `reset()` make a copy of the current context and return the results of its\r\n method. This copy is transient and remade each time, so modules making extensive use of it can call `copy()` and\r\n check the copy.\r\n- Note that, unlike the context manager, the decorators also RUN the inner code in the given context. \r\n- Thread-handling was improved significantly and now also includes a wrapper function for `threading.Thread.run()` methods to\r\n ensure clean-up (`@injector.as_thread_run()`). This also is a wrapper around `@injector.inject` so you can inject\r\n variables into your `run()` method directly.\r\n\r\n### v1.1.0\r\n- Injectable objects may now define a `__cleanup__()` method which will be invoked when the global cache or context\r\n cache is cleared.\r\n- Note that `__cleanup__()` IS NOT INVOKED for one-time use objects at the moment, but this is planned as a feature.\r\n\r\n### v1.0.1\r\n- Inherited injectable class members are now supported properly\r\n\r\n### v1.0.0\r\n- Official initial release\r\n- Added support for @injector.injectable_global which registers with GLOBAL cache instead of context-specific cache\r\n- Added support for @injector.injectable_nocache which registers with NO_CACHE instead \r\n- Added support for injector.override() as a helper function to replace one constructor with another.\r\n- Added support for any constructor argument (e.g. via override() or register_constructor()) to be specified\r\n by fully-qualified Python name (e.g. package.module.MyInjectableClass) to better support systems where injected\r\n classes are specified by name.\r\n- Fixed a bug whereby the cache wasn't cleared\r\n\r\n### v0.2.2\r\n- Fixed a bug for injection when a non-truthy default value needed to be used.\r\n\r\n### v0.2.1\r\n- Fixed a bug in Python 3.8 and 3.9 where `entry_points(group=?)` was not supported\r\n\r\n### v0.2.0\r\n- Objects with a cache strategy of `CONTEXT_CACHE` will now have separate instances within threads\r\n- Added `injector.get()` as a fast way to get the object that would be injected (useful if operating outside of\r\n a function or method)\r\n- Added `injector.register_constructor()` as a wrapper to register a class in a non-decorated fashion\r\n- Added the entry point `autoinject.injectables` to directly register injectable classes\r\n- Added the entry point `autoinject.registrars`\r\n- Support for overriding injectables and for injecting functions \r\n- Added a `weight` keyword argument to `register()` and `register_construct()` to control overriding order\r\n- There is now a `cleanup()` function in the `ContextManager()` class which triggers informant objects to check for\r\n old items that are no longer needed. This was added mostly to support the thread-based context informant, since it \r\n has no easy way of calling `destroy()` whenever the thread ends (unless one manually calls it). It is the best \r\n practice if you can call `destroy()` directly whenever a context ceases to exist instead of relying on `cleanup()`.\r\n",
"bugtrack_url": null,
"license": "",
"summary": "Automated dependency injection for Python",
"version": "1.3.2",
"project_urls": {
"Bug Tracker": "https://github.com/turnbullerin/autoinject/issues",
"Documentation": "https://autoinject.readthedocs.io/en/latest/",
"Homepage": "https://github.com/turnbullerin/autoinject"
},
"split_keywords": [],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "4a61c0b64e4973ce1840339aa294c6aceaee520efe21aaf88f53e993c2596ced",
"md5": "de861b37b95fc0049749c1991de77699",
"sha256": "fa14895653dc888149ccb57c5989d6664152cb0660eb30fa58595fa573568b4b"
},
"downloads": -1,
"filename": "autoinject-1.3.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "de861b37b95fc0049749c1991de77699",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 19222,
"upload_time": "2023-05-15T21:32:48",
"upload_time_iso_8601": "2023-05-15T21:32:48.794096Z",
"url": "https://files.pythonhosted.org/packages/4a/61/c0b64e4973ce1840339aa294c6aceaee520efe21aaf88f53e993c2596ced/autoinject-1.3.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "045d8c4e54d14bcbc80ee6069110339af79b65d2748a02c9875644248e17adc1",
"md5": "0af50ff7a97202ceff0b3857ba32e1dc",
"sha256": "667f72278718da3b0133d3b84b21ef1a34f453a83085e26ae68c07864ee7a5e2"
},
"downloads": -1,
"filename": "autoinject-1.3.2.tar.gz",
"has_sig": false,
"md5_digest": "0af50ff7a97202ceff0b3857ba32e1dc",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 25626,
"upload_time": "2023-05-15T21:32:50",
"upload_time_iso_8601": "2023-05-15T21:32:50.341006Z",
"url": "https://files.pythonhosted.org/packages/04/5d/8c4e54d14bcbc80ee6069110339af79b65d2748a02c9875644248e17adc1/autoinject-1.3.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-05-15 21:32:50",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "turnbullerin",
"github_project": "autoinject",
"travis_ci": false,
"coveralls": true,
"github_actions": false,
"circle": true,
"lcname": "autoinject"
}