Skip to content

Commit

Permalink
Merge pull request #2 from runemalm/feature/constructor-args
Browse files Browse the repository at this point in the history
Feature/constructor args
  • Loading branch information
runemalm committed Mar 2, 2024
2 parents ef22679 + 0f4a6b2 commit 33ae0b8
Show file tree
Hide file tree
Showing 17 changed files with 370 additions and 38 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ third_container = DependencyContainer.get_instance(name="third_container")
dependency_container.register_transient(SomeInterface, SomeClass)
dependency_container.register_scoped(AnotherInterface, AnotherClass)
dependency_container.register_singleton(ThirdInterface, ThirdClass)

# Registering dependencies with constructor arguments
dependency_container.register_transient(
SomeInterface,
SomeClass,
constructor_args={"arg1": value1, "arg2": value2}
)
```

### Resolving dependencies using the container
Expand Down Expand Up @@ -139,6 +146,21 @@ To contribute, create a pull request on the develop branch following the [git fl

## Release Notes

### [1.0.0-alpha.4](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.4) (2024-03-02)

- **New Feature**: Support for constructor arguments in dependency registration: In this release, we introduce the ability to specify constructor arguments when registering dependencies with the container. This feature provides more flexibility when configuring dependencies, allowing users to customize the instantiation of classes during registration.

**Usage Example:**
```python
# Registering a dependency with constructor arguments
dependency_container.register_transient(
SomeInterface, SomeClass,
constructor_args={"arg1": value1, "arg2": value2}
)
```

Users can now pass specific arguments to be used during the instantiation of the dependency. This is particularly useful when a class requires dynamic or configuration-dependent parameters.

### [1.0.0-alpha.3](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.3) (2024-03-02)

- **Breaking Change**: Restriction on `@inject` Decorator: Starting from this version, the `@inject` decorator can now only be used on static class methods and class methods. This change is introduced due to potential pitfalls associated with resolving and injecting dependencies directly into class instance methods using the dependency container.
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
version = '1.0'

# The full version, including alpha/beta/rc tags
release = '1.0.0-alpha.3'
release = '1.0.0-alpha.4'


# -- General configuration ---------------------------------------------------
Expand Down
16 changes: 16 additions & 0 deletions docs/versionhistory.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@
Version history
###############

**1.0.0-alpha.4 (2024-03-02)**

- **New Feature**: Support for constructor arguments in dependency registration: In this release, we introduce the ability to specify constructor arguments when registering dependencies with the container. This feature provides more flexibility when configuring dependencies, allowing users to customize the instantiation of classes during registration.

**Usage Example:**::
# Registering a dependency with constructor arguments
dependency_container.register_transient(
SomeInterface, SomeClass,
constructor_args={"arg1": value1, "arg2": value2}
)

Users can now pass specific arguments to be used during the instantiation of the dependency. This is particularly useful when a class requires dynamic or configuration-dependent parameters.

`View release on GitHub <https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.4>`_

**1.0.0-alpha.3 (2024-03-02)**

- **Breaking Change**: Restriction on `@inject` Decorator: Starting from this version, the `@inject` decorator can now only be used on static class methods and class methods. This change is introduced due to potential pitfalls associated with resolving and injecting dependencies directly into class instance methods using the dependency container.
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

setup(
name='py-dependency-injection',
version='1.0.0-alpha.3',
version='1.0.0-alpha.4',
author='David Runemalm, 2024',
author_email='david.runemalm@gmail.com',
description=
Expand Down
65 changes: 54 additions & 11 deletions src/dependency_injection/container.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import inspect
from typing import Any, Dict, Type

from dependency_injection.registration import Registration
from dependency_injection.scope import DEFAULT_SCOPE_NAME, Scope
Expand All @@ -25,20 +26,20 @@ def get_instance(cls, name=None):

return cls._instances[(cls, name)]

def register_transient(self, interface, class_):
def register_transient(self, interface, class_, constructor_args=None):
if interface in self._registrations:
raise ValueError(f"Dependency {interface} is already registered.")
self._registrations[interface] = Registration(interface, class_, Scope.TRANSIENT)
self._registrations[interface] = Registration(interface, class_, Scope.TRANSIENT, constructor_args)

def register_scoped(self, interface, class_):
def register_scoped(self, interface, class_, constructor_args=None):
if interface in self._registrations:
raise ValueError(f"Dependency {interface} is already registered.")
self._registrations[interface] = Registration(interface, class_, Scope.SCOPED)
self._registrations[interface] = Registration(interface, class_, Scope.SCOPED, constructor_args)

def register_singleton(self, interface, class_):
def register_singleton(self, interface, class_, constructor_args=None):
if interface in self._registrations:
raise ValueError(f"Dependency {interface} is already registered.")
self._registrations[interface] = Registration(interface, class_, Scope.SINGLETON)
self._registrations[interface] = Registration(interface, class_, Scope.SINGLETON, constructor_args)

def resolve(self, interface, scope_name=DEFAULT_SCOPE_NAME):
if scope_name not in self._scoped_instances:
Expand All @@ -50,24 +51,62 @@ def resolve(self, interface, scope_name=DEFAULT_SCOPE_NAME):
registration = self._registrations[interface]
dependency_scope = registration.scope
dependency_class = registration.class_
constructor_args = registration.constructor_args

self._validate_constructor_args(constructor_args=constructor_args, class_=dependency_class)

if dependency_scope == Scope.TRANSIENT:
return self._inject_dependencies(dependency_class)
return self._inject_dependencies(
class_=dependency_class,
constructor_args=constructor_args
)
elif dependency_scope == Scope.SCOPED:
if interface not in self._scoped_instances[scope_name]:
self._scoped_instances[scope_name][interface] = (
self._inject_dependencies(
class_=dependency_class,
scope_name=scope_name,))
scope_name=scope_name,
constructor_args=constructor_args,
))
return self._scoped_instances[scope_name][interface]
elif dependency_scope == Scope.SINGLETON:
if interface not in self._singleton_instances:
self._singleton_instances[interface] = self._inject_dependencies(dependency_class)
self._singleton_instances[interface] = (
self._inject_dependencies(
class_=dependency_class,
constructor_args=constructor_args
)
)
return self._singleton_instances[interface]

raise ValueError(f"Invalid dependency scope: {dependency_scope}")

def _inject_dependencies(self, class_, scope_name=None):
def _validate_constructor_args(self, constructor_args: Dict[str, Any], class_: Type) -> None:
class_constructor = inspect.signature(class_.__init__).parameters

# Check if any required parameter is missing
missing_params = [param for param in class_constructor.keys() if
param not in ["self", "cls", "args", "kwargs"] and
param not in constructor_args]
if missing_params:
raise ValueError(
f"Missing required constructor arguments: "
f"{', '.join(missing_params)} for class '{class_.__name__}'.")

for arg_name, arg_value in constructor_args.items():
if arg_name not in class_constructor:
raise ValueError(
f"Invalid constructor argument '{arg_name}' for class '{class_.__name__}'. "
f"The class does not have a constructor parameter with this name.")

expected_type = class_constructor[arg_name].annotation
if expected_type != inspect.Parameter.empty:
if not isinstance(arg_value, expected_type):
raise TypeError(
f"Constructor argument '{arg_name}' has an incompatible type. "
f"Expected type: {expected_type}, provided type: {type(arg_value)}.")

def _inject_dependencies(self, class_, scope_name=None, constructor_args=None):
constructor = inspect.signature(class_.__init__)
params = constructor.parameters

Expand All @@ -82,6 +121,10 @@ def _inject_dependencies(self, class_, scope_name=None):
# **kwargs parameter
pass
else:
dependencies[param_name] = self.resolve(param_info.annotation, scope_name=scope_name)
# Check if constructor_args has an argument with the same name
if constructor_args and param_name in constructor_args:
dependencies[param_name] = constructor_args[param_name]
else:
dependencies[param_name] = self.resolve(param_info.annotation, scope_name=scope_name)

return class_(**dependencies)
5 changes: 4 additions & 1 deletion src/dependency_injection/registration.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from typing import Any, Dict

from dependency_injection.scope import Scope


class Registration():

def __init__(self, interface, class_, scope: Scope):
def __init__(self, interface, class_, scope: Scope, constructor_args: Dict[str, Any] = None):
self.interface = interface
self.class_ = class_
self.scope = scope
self.constructor_args = constructor_args or {}
5 changes: 0 additions & 5 deletions tests/unit_test/car.py

This file was deleted.

14 changes: 12 additions & 2 deletions tests/unit_test/container/register/test_register_scoped.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import pytest

from dependency_injection.container import DependencyContainer
from unit_test.car import Car
from unit_test.unit_test_case import UnitTestCase
from unit_test.vehicle import Vehicle


class TestRegisterScoped(UnitTestCase):
Expand All @@ -12,6 +10,12 @@ def test_register_scoped_succeeds_when_not_previously_registered(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand All @@ -26,6 +30,12 @@ def test_register_scoped_fails_when_already_registered(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand Down
14 changes: 12 additions & 2 deletions tests/unit_test/container/register/test_register_singleton.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import pytest

from dependency_injection.container import DependencyContainer
from unit_test.car import Car
from unit_test.unit_test_case import UnitTestCase
from unit_test.vehicle import Vehicle


class TestRegisterSingleton(UnitTestCase):
Expand All @@ -12,6 +10,12 @@ def test_register_singleton_succeeds_when_not_previously_registered(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand All @@ -26,6 +30,12 @@ def test_register_singleton_fails_when_already_registered(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand Down
17 changes: 13 additions & 4 deletions tests/unit_test/container/register/test_register_transient.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import pytest

from dependency_injection.container import DependencyContainer
from unit_test.car import Car
from unit_test.unit_test_case import UnitTestCase
from unit_test.vehicle import Vehicle


class TestRegisterTransient(UnitTestCase):
Expand All @@ -12,20 +10,31 @@ def test_register_transient_succeeds_when_not_previously_registered(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car

# act
dependency_container.register_transient(interface, dependency_class)

# assert
# (no exception thrown)
# assert (no exception thrown)

def test_register_transient_fails_when_already_registered(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand Down
22 changes: 22 additions & 0 deletions tests/unit_test/container/register/test_register_with_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from dependency_injection.container import DependencyContainer
from unit_test.unit_test_case import UnitTestCase


class TestRegisterWithArgs(UnitTestCase):

def test_register_with_constructor_args(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car

# act + assert (no exception)
dependency_container.register_transient(interface, dependency_class, constructor_args={"color": "red", "mileage": 3800})
14 changes: 12 additions & 2 deletions tests/unit_test/container/resolve/test_resolve_scoped.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
from dependency_injection.container import DependencyContainer
from unit_test.car import Car
from unit_test.unit_test_case import UnitTestCase
from unit_test.vehicle import Vehicle


class TestResolveScoped(UnitTestCase):
Expand All @@ -10,6 +8,12 @@ def test_resolve_scoped_in_same_scope_returns_same_instance(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand All @@ -26,6 +30,12 @@ def test_resolve_scoped_in_different_scopes_returns_different_instances(
self,
):
# arrange
class Vehicle:
pass

class Car(Vehicle):
pass

dependency_container = DependencyContainer.get_instance()
interface = Vehicle
dependency_class = Car
Expand Down
Loading

0 comments on commit 33ae0b8

Please sign in to comment.