A dependency injection container for Python
Project description
ophiDIan
Description
ophiDIan is a Dependency Injection (DI) container for Python. Unlike other DI containers that utilize decorators or configuration files to accomplish their goal, ophiDIan uses type hints to identify and resolve dependencies. In other words, by using type hinting to resolve dependencies, ophiDIan avoids making your components dependent upon the DI framework.
Ophidian follows the Register, Resolve, Release (RRR) pattern.
Installation
Install with pip install ophidian-di
Tutorial
Dependency definitions
For this tutorial, we'll use the following class definitions:
from abc import ABC, abstractmethod
class AbstractA(ABC):
@abstractmethod
def do_something(self):
pass
class AbstractB(ABC):
@abstractmethod
def do_something_else(self):
pass
class DependencyA(AbstractA):
def do_something(self):
print(f"{id(self)}: I am dependency A")
class DependencyB(AbstractB):
def __init__(self, name: str):
self._name = name
def do_something_else(self):
print(f"{id(self)} My name is {self._name}")
class TestClass1:
def __init__(self, a: AbstractA):
self.a = a
class TestClass2:
__test__ = False
def __init__(self, b: AbstractB):
self.b = b
class TestClass3:
def __init__(self, a: AbstractA, b: AbstractB):
self.a = a
self.b = b
RRR
The first step in the RRR pattern is to Register dependencies. This can be
performed using either the register()
or register_instance()
methods. Next,
objects can be built by Resolving. Finally, when the dependency is no
longer needed, it can be Released.
from ophidian import DIContainer
dependency_b_instance = DependencyB("Mike")
di_container = DIContainer()
di_container.register(AbstractA, DependencyA)
di_container.register_instance(AbstractB, dependency_b_instance)
test_class_1 = di_container.resolve(TestClass1)
test_class_2 = di_container.resolve(TestClass2)
test_class_3 = di_container.resolve(TestClass3)
assert id(test_class_1.a) != id(test_class_3.a)
assert id(test_class_2.b) == id(dependency_b_instance)
assert id(test_class_2.b) == id(test_class_3.b)
assert isinstance(test_class_1.a, AbstractA)
assert isinstance(test_class_1.a, DependencyA)
assert isinstance(test_class_2.b, AbstractB)
assert isinstance(test_class_2.b, DependencyB)
# Note: All dependencies will automatically be released when the `di_container`
# instance is cleaned up by the garbage collector.
di_container.release(AbstractA)
di_container.release(AbstractB)
In the above example, the DIContainer.register()
method is used tell the DI
container that components that depend upon AbstractA
should be supplied a new
instance of the concrete class DependencyA
. The
DiContainer.register_instance()
method is used to tell the DI container that
all components that depend upon AbstractB
should be supplied with the same
instance of DependencyB
.
Conventions
Conventions are mainly used to resolve primitive dependencies. For example, if a component depends upon a file path for a configuration file passed as a string, a convention can be registered to provide the dependency.
from ophidian import DIContainer
class Configuration:
def __init__(self, a: DependencyA, config_file_path: str):
...
di_container = DIContainer()
di_container.register(AbstractA, DependencyA)
di_container.register_convention(str, "config_file_path", "/tmp/my_config_file")
configuration = di_container.resolve(Configuration)
di_container.release(AbstractA)
di_container.release_convention(str, "config_file_path")
We wouldn't want to use register()
or register_instance()
, as we wouldn't
want all string dependencies to be resolved with the configuration file path.
Instead, the DI container is informed about an established "convention" within
the code: components that depend upon the configuration file path expect a
string parameter named "config_file_path". See Primitive Dependencies by Mark
Seemann for more
information about conventions.
Documentation
DIContainer
class DIContainer()
A dependency injection (DI) container that uses type annotations to resolve and inject dependencies.
__init__
def __init__()
register
@no_type_check
def register(interface: Type[T], concrete_type: Type[T])
Register a concrete type
that satisfies a given interface.
:param interface: An interface or abstract base class that other classes depend
upon
:param concrete_type: A type
(class) that implements interface
:raises TypeError: If concrete_type
is not a class, or not a subclass of
interface
register_instance
@no_type_check
def register_instance(interface: Type[T], instance: T)
Register a concrete instance that satisfies a given interface.
:param interface: An interface or abstract base class that other classes depend
upon
:param instance: An instance (object) of a type
that implements
interface
:raises TypeError: If instance
is not an instance of interface
register_convention
@no_type_check
def register_convention(type_: Type[T], name: str, instance: T)
Register an instance as a convention
At times — particularly when dealing with primitive types — it can be useful to
define a convention for how dependencies should be resolved. For example, you
might want any class that specifies hostname: str
in its constructor to
receive the hostname of the system it's running on. Registering a convention
allows you to assign an object instance to a type, name pair.
Example:
class TestClass:
def __init__(self, hostname: str):
self.hostname = hostname
di_container = DIContainer()
di_container.register_convention(str, "hostname", "my_hostname.domain")
test = di_container.resolve(TestClass)
assert test.hostname == "my_hostname.domain"
:param type_: The type
(class) of the dependency
:param name: The name of the dependency parameter
:param instance: An instance (object) of type_
that will be injected into constructors
that specify [name]: [type_]
as parameters
Errors
UnresolvableDependencyError
class UnresolvableDependencyError(ValueError):
Raised when one or more dependencies cannot be successfully resolved.
Running the tests
Running the tests is as simple as poetry install && poetry run pytest
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file ophidian_di-0.1.0.post2.tar.gz
.
File metadata
- Download URL: ophidian_di-0.1.0.post2.tar.gz
- Upload date:
- Size: 18.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.5.1 CPython/3.10.12 Linux/5.15.0-83-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 002a9c5c3985be4ec5d994fee7ca5663dc8b7d98e9360ca86171753fe59a30db |
|
MD5 | 1f3db9153023c3ef1d9fdd03edc0e4cc |
|
BLAKE2b-256 | bfd28f041ae8477c4901610b21175307e32f84752af5a7c874524a4608aaa911 |
File details
Details for the file ophidian_di-0.1.0.post2-py3-none-any.whl
.
File metadata
- Download URL: ophidian_di-0.1.0.post2-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/1.5.1 CPython/3.10.12 Linux/5.15.0-83-generic
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 991b4b00a6c8de7220e160974b36c805beaf55b2e1ec63551e7b9b8ef3b9c66e |
|
MD5 | 707e71ee98d51131da252273fec01cb9 |
|
BLAKE2b-256 | 15314f392f668403e7299cb21e503b67049893f86453c8868f800b1855c37eff |