Reactor DI for Python
A code generator for dependency injection (DI) in Python which is based on the mediator and factory patterns.
Features
- Two powerful decorators:
@moduleand@law_of_demeter, pluslookupandmakeannotation markers - Code generation approach: Generates DI code rather than runtime injection
- Mediator pattern: Central coordination of dependencies
- Factory pattern: Object creation abstraction
- Type-safe: Full type hint support
- Nested modules: Child modules use
lookup[Type]to share dependencies from parent modules - Lazy dependency resolution: Dependencies resolved individually on first access, supporting deferred initialization patterns (e.g., async context managers)
- TYPE_CHECKING compatible: Works with
if TYPE_CHECKING:imports for circular dependency avoidance - Pydantic compatible: Works with Pydantic BaseSettings/BaseModel annotation-only fields
- ABC compatible: Automatic resolution of abstract
@propertyconflicts with DI annotations - Python 3.9+ support: Tested on Python 3.9 through 3.15
Installation
pip install reactor-di
Quick Start
from reactor_di import module, law_of_demeter, CachingStrategy
class DatabaseConfig:
host = "localhost"
port = 5432
timeout = 30
@law_of_demeter("_config")
class DatabaseService:
_config: DatabaseConfig
_host: str # Forwarded from config.host
_port: int # Forwarded from config.port
_timeout: int # Forwarded from config.timeout
def connect(self) -> str:
return f"Connected to {self._host}:{self._port} (timeout: {self._timeout}s)"
# Module: Automatic DI: Implements annotations as @cached_property functions
@module(CachingStrategy.NOT_THREAD_SAFE)
class AppModule:
config: DatabaseConfig # Directly instantiated
database: DatabaseService # Synthesized with dependencies
# Usage
app = AppModule()
db_service = app.database
print(db_service.connect()) # → "Connected to localhost:5432 (timeout: 30s)"
# Properties are cleanly forwarded
print(db_service._host) # → "localhost" (from config.host)
print(db_service._timeout) # → 30 (from config.timeout)
Examples
The examples/ directory contains testable examples that demonstrate all the features shown in this README:
quick_start.py- The complete Quick Start example above, converted to testable formatquick_start_advanced.py- Advanced quick start with inheritance patternscaching_strategy.py- DemonstratesCachingStrategy.DISABLED,CachingStrategy.NOT_THREAD_SAFE, andCachingStrategy.THREAD_SAFEmake_marker.py- Subtype factory generation withmake[BaseType, ImplType]nested_modules.py- Nested modules withlookupfor parent dependency sharingstacked_decorators.py- Shows using multiple@law_of_demeterdecorators on the same classcustom_prefix.py- Demonstrates custom prefix options (prefix='',prefix='cfg_', etc.)side_effects.py- Tests side effect isolation during decorationtesting.py- Testing pattern: replacing module components with mockstyping_decorators_preserve_class_type.py- Demonstrates (and statically asserts viamypy) that@law_of_demeterand@modulepreserve the input class typetyping_lookup_make_erasure.py- Demonstrates (and statically asserts viamypy) thatlookup[T]andmake[B, I]erase toT/Bfor type checkers, so attribute access type-checks withoutcast()
Running Examples
# Run all examples as tests
uv run pytest examples/
# Run a specific example
uv run pytest examples/quick_start.py
All examples are automatically tested as part of the CI pipeline to ensure they stay current with the codebase.
Tests
The tests/ directory contains regression and unit tests (63 tests):
test_module_integration.py- Module + law_of_demeter integration with annotation-only configs, Pydantic compatibility, and abstract property conflict resolutiontest_lazy_resolution.py- Lazy per-attribute resolution with deferred initialization patternstest_forward_ref.py- TYPE_CHECKING forward reference handling in module factorytest_dataclass_law_of_demeter.py- Dataclass + stacked @law_of_demeter regression tests (3 tests)test_pure_hasattr.py- Comprehensive tests for thepure_hasattrutility (15 tests)test_thread_safe.py- Thread-safe caching strategy with concurrent access tests (12 tests)test_type_utils_markers.py- Runtime invariants of thelookup/makemarkers, e.g. that they are callable fortyping._type_check(14 tests)test_law_of_demeter.py- Law of Demeter decorator teststest_side_effects.py- Side effects isolation during decoration
# Run all tests (examples + regression tests)
uv run pytest
# Run only regression tests
uv run pytest tests/
Architecture
Reactor DI uses a code generation approach with clean separation of concerns:
module.py- The@moduledecorator for dependency injection containerslaw_of_demeter.py- The@law_of_demeterdecorator for property forwardingcaching.py- Caching strategies (CachingStrategy.DISABLED,CachingStrategy.NOT_THREAD_SAFE,CachingStrategy.THREAD_SAFE)type_utils.py- Simplified type checking utilities and thelookup/makeannotation markers (Python 3.9+ stable APIs)
The decorators work together through simple hasattr checks - @law_of_demeter creates forwarding properties that @module recognizes as already implemented, enabling clean cooperation without complex validation logic.
Advanced Usage
Caching Strategies
from reactor_di import module, CachingStrategy
# No caching - components created fresh each time
@module(CachingStrategy.DISABLED)
class DevModule:
service: MyService
# Cached components - same instance returned (not thread-safe)
@module(CachingStrategy.NOT_THREAD_SAFE)
class ProdModule:
service: MyService
# Cached components - same instance returned (thread-safe, uses locking)
@module(CachingStrategy.THREAD_SAFE)
class ThreadSafeProdModule:
service: MyService
Multiple Decorator Integration
@law_of_demeter("_config") # Creates forwarding properties
@law_of_demeter("_module") # Auto-setup: self._config = self._module.config
class ResourceController:
def __init__(self, module):
self._module = module
# Decorator automatically sets up: self._config = module.config
# From _config
_timeout: int
_is_dry_run: bool
# From _module
_api: object
_namespace: str
Subtype Factory Generation
Use make[BaseType, ImplType] to program to an interface while the factory instantiates a concrete implementation:
from abc import ABC, abstractmethod
from reactor_di import module, make, CachingStrategy
class Logger(ABC):
@abstractmethod
def log(self, message: str) -> str: ...
class ConsoleLogger(Logger):
def log(self, message: str) -> str:
return f"[console] {message}"
@module(CachingStrategy.NOT_THREAD_SAFE)
class AppModule:
logger: make[Logger, ConsoleLogger] # factory returns ConsoleLogger()
app = AppModule()
assert isinstance(app.logger, ConsoleLogger)
assert isinstance(app.logger, Logger)
Nested Modules
Child modules can share dependencies from their parent using lookup[Type]:
from reactor_di import module, lookup, CachingStrategy
@module(CachingStrategy.NOT_THREAD_SAFE)
class CacheModule:
db: lookup[DatabaseConnection] # resolved from parent module
cache_config: CacheConfig # created locally
@module(CachingStrategy.NOT_THREAD_SAFE)
class AppModule:
db: DatabaseConnection # created here
cache_module: CacheModule # child module — db injected from here
app = AppModule()
assert app.cache_module.db is app.db # same instance
An optional second parameter renames the lookup — the child uses a different local name than the parent's attribute:
@module(CachingStrategy.NOT_THREAD_SAFE)
class AuditModule:
connection: lookup[DatabaseConnection, "db"] # look up "db" on parent, bind as "connection"
On a component (non-module class), @law_of_demeter skips lookup-annotated attributes — the parent module's factory resolves them through the dependency-map mechanism instead of forwarding from the base reference.
lookup[T] and make[B, I] erase to T / B at type-check time (mirroring how Final[T] and ClassVar[T] erase to T), so attribute access works naturally in IDEs and mypy without cast().
Custom Prefixes
# No prefix - direct forwarding
@law_of_demeter('config', prefix='')
class DirectController:
timeout: int # → config.timeout
is_dry_run: bool # → config.is_dry_run
# Custom prefix
@law_of_demeter('config', prefix='cfg_')
class PrefixController:
cfg_timeout: int # → config.timeout
cfg_is_dry_run: bool # → config.is_dry_run
API Reference
Core Decorators
@module(strategy: CachingStrategy = CachingStrategy.DISABLED)
Creates a dependency injection module that automatically instantiates and provides dependencies.
Parameters:
strategy: Caching strategy for component instancesCachingStrategy.DISABLED: Create new instances each time (default)CachingStrategy.NOT_THREAD_SAFE: Cache instances (not thread-safe)CachingStrategy.THREAD_SAFE: Cache instances with per-instance locking (thread-safe)
Usage:
@module(CachingStrategy.NOT_THREAD_SAFE)
class AppModule:
config: Config
service: Service # Automatically injected with dependencies
@law_of_demeter(base_ref: str, prefix: str = "_")
Creates property forwarding from a base reference to avoid Law of Demeter violations.
Parameters:
base_ref: Name of the base object attribute to forward fromprefix: Prefix for forwarded property names (default: "_")
Usage:
@law_of_demeter("_config")
class Service:
_timeout: int # Forwards to _config.timeout
_host: str # Forwards to _config.host
Annotation Markers
lookup[Type]
Marks a module annotation as a dependency that should be resolved from the parent module rather than being created locally. On a module, @module skips factory generation and installs a lightweight property; the actual value is injected lazily by the parent. On a component, lookup is a no-op.
Parameters:
- First (required): The type of the dependency
- Second (optional): Name of the attribute to look up on the parent module. Defaults to the annotation's own name.
Usage:
@module(CachingStrategy.NOT_THREAD_SAFE)
class ChildModule:
shared_db: lookup[DatabaseConnection] # lookup "shared_db" on parent
connection: lookup[DatabaseConnection, "db"] # lookup "db" on parent, bind as "connection"
local_service: MyService # created locally
make[BaseType, ImplType]
Marks a module annotation for subtype factory generation. The factory instantiates ImplType (a subtype of BaseType) instead of BaseType. This enables programming to interfaces while keeping concrete wiring in one place.
Parameters:
- First (required): The base type (used for the property return type)
- Second (required): The implementation type (actually instantiated by the factory)
Usage:
@module(CachingStrategy.NOT_THREAD_SAFE)
class AppModule:
service: make[ServiceBase, HttpService] # factory returns HttpService()
cache: make[Cache, InMemoryCache] # factory returns InMemoryCache()
Type Utilities
The simplified type utilities leverage Python 3.9+ stable type hint APIs:
get_alternative_names(name: str, prefix: str = "_") -> list[str]
Generates alternative names for dependency mapping (e.g., _config → config).
has_constructor_assignment(class_type: type[Any], attr_name: str) -> bool
Detects if a constructor assigns to an attribute using regex source analysis.
is_primitive_type(attr_type: type[Any]) -> bool
Identifies primitive types (int, str, bool, etc.) that shouldn't be auto-instantiated.
resolve_abstract_property_conflicts(cls: type[Any]) -> None
Replaces inherited abstract @property methods that collide with DI annotations, installing concrete dict-backed properties so lazy DI resolution via __getattr__ works correctly. Safe to call multiple times.
pure_hasattr(obj: Any, attr_name: str) -> bool
Checks if an attribute exists without side effects like triggering descriptors or properties. Used internally to avoid premature evaluation during dependency resolution.
Enums
CachingStrategy
Component caching strategies for the @module decorator.
DISABLED = "disabled": No caching, create new instances each timeNOT_THREAD_SAFE = "not_thread_safe": Cache instances (not thread-safe)THREAD_SAFE = "thread_safe": Cache instances with per-instance locking (thread-safe)
Development
This project uses modern Python tooling and best practices:
Setup
- Clone the repository
- Install dependencies:
uv sync
Running Tests
# Run all tests (116 tests: 53 examples + 63 regression/unit tests)
uv run pytest
# Run tests with coverage and HTML/terminal reports
uv run pytest --cov
# Run example tests only
uv run pytest examples/ # Run all examples as tests (53 tests)
uv run pytest examples/caching_strategy.py # Caching strategy examples (5 tests)
uv run pytest examples/custom_prefix.py # Custom prefix examples (6 tests)
uv run pytest examples/quick_start.py # Quick start examples (4 tests)
# Run regression/unit tests only
uv run pytest tests/ # Run all regression tests (63 tests)
Debugging in PyCharm
The project is optimized for fast development and debugging:
- Default: Tests run without coverage for fast feedback and debugging
- Coverage analysis: Use
--covflag when you need coverage reports - Coverage threshold: Set to 90% to maintain high code quality (when coverage is enabled)
This configuration ensures breakpoints work reliably and tests run quickly during development.
Code Quality
# Run linting
uv run ruff check src tests examples
uv run black --check src tests examples
# Run type checking
uv run mypy src examples
# Fix formatting
uv run black src tests examples
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality with meaningful assertions
- Ensure all tests pass and 90% coverage is maintained
- Verify realistic code scenarios rather than mocking impossible edge cases
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Metadata
Release files for reactor-di 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| reactor_di-0.6.0.tar.gz | 101.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| reactor_di-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 122.7 kB
Release files / reactor_di-0.6.0.tar.gz
| Download URL | reactor_di-0.6.0.tar.gz |
|---|---|
| Size | 101.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
14cda4ab8a25415e96a513d11db205c22ec4cec1af9dd247930b84c68a5549c2
|
|
BLAKE2b-256 checksum How to use checksums |
747fc4c09df2764166699b919d52d3bb879e506e6972da985984ad3ac317025d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / reactor_di-0.6.0-py3-none-any.whl
| Download URL | reactor_di-0.6.0-py3-none-any.whl |
|---|---|
| Size | 20.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
350a9dc1d6dce67229bac3a952fae068df8a17022c502cafde376b2b85036ca5
|
|
BLAKE2b-256 checksum How to use checksums |
728e1434711a05d36bfb2c5b6b792479faeb10fcffe3b0fd360c0b04bcf00b4a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|