diff --git a/README.md b/README.md index 5659367..a675a8c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # py-dependency-injection -A prototypical dependency injection library for Python. +A dependency injection library for Python. ## Features @@ -83,20 +83,26 @@ You can find the source code for `py-dependency-injection` on [GitHub](https://g ## Release Notes +### [1.0.0-alpha.9](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.9) (2024-08-08) + +- **Breaking Change**: Removed constructor injection when resolving dataclasses. +- **Enhancement**: Added dependency container getter and setter for registrations. Also added new `RegistrationSerializer` class for for serializing and deserializing them. These additions provide a more flexible way to interact with the container's registrations. + ### [1.0.0-alpha.8](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.8) (2024-06-07) -- Bug Fix: Fixed an issue in the dependency resolution logic where registered constructor arguments were not properly merged with automatically injected dependencies. This ensures that constructor arguments specified during registration are correctly combined with dependencies resolved by the container. +- **Bug Fix**: Fixed an issue in the dependency resolution logic where registered constructor arguments were not properly merged with automatically injected dependencies. This ensures that constructor arguments specified during registration are correctly combined with dependencies resolved by the container. +- **Documentation Update**: The documentation structure has been updated for better organization and ease of understanding. ### [1.0.0-alpha.7](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.7) (2024-03-24) -- Documentation Update: Updated the documentation to provide clearer instructions and more comprehensive examples. -- Preparing for Beta Release: Made necessary adjustments and refinements in preparation for the upcoming first beta release. +- **Documentation Update**: Updated the documentation to provide clearer instructions and more comprehensive examples. +- **Preparing for Beta Release**: Made necessary adjustments and refinements in preparation for the upcoming first beta release. ### [1.0.0-alpha.6](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.6) (2024-03-23) -- Factory Registration: Added support for registering dependencies using factory functions for dynamic instantiation. -- Instance Registration: Enabled registering existing instances as dependencies. -- Tag-based Registration and Resolution: Introduced the ability to register and resolve dependencies using tags for flexible dependency management. +- **Factory Registration**: Added support for registering dependencies using factory functions for dynamic instantiation. +- **Instance Registration**: Enabled registering existing instances as dependencies. +- **Tag-based Registration and Resolution**: Introduced the ability to register and resolve dependencies using tags for flexible dependency management. ### [1.0.0-alpha.5](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.5) (2024-03-03) @@ -104,25 +110,25 @@ You can find the source code for `py-dependency-injection` on [GitHub](https://g ### [1.0.0-alpha.4](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.4) (2024-03-02) -- Constructor Arguments: Support for constructor arguments added to dependency registration. +- **Constructor Arguments**: Support for constructor arguments added to dependency registration. ### [1.0.0-alpha.3](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.3) (2024-03-02) - **Breaking Change**: Starting from this version, the `@inject` decorator can only be used on static class methods and class methods. It can't be used on instance methods anymore. -- Documentation Update: The documentation has been updated to reflect the new restriction on the usage of the decorator. +- **Documentation Update**: The documentation has been updated to reflect the new restriction on the usage of the decorator. ### [1.0.0-alpha.2](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.2) (2024-02-27) -- Python Version Support: Added support for Python versions 3.7, 3.9, 3.10, 3.11, and 3.12. -- New Feature: Method Injection with Decorator: Introduced a new feature allowing method injection using the @inject decorator. Dependencies can now be injected into an instance method, providing more flexibility in managing dependencies within class instance methods. -- New Feature: Multiple Containers: Enhanced the library to support multiple containers. Users can now create and manage multiple dependency containers, enabling better organization and separation of dependencies for different components or modules. -- Documentation Update: Expanded and improved the documentation to include details about the newly added method injection feature and additional usage examples. Users can refer to the latest documentation at readthedocs for comprehensive guidance. +- **Python Version Support**: Added support for Python versions 3.7, 3.9, 3.10, 3.11, and 3.12. +- **New Feature**: Method Injection with Decorator: Introduced a new feature allowing method injection using the @inject decorator. Dependencies can now be injected into an instance method, providing more flexibility in managing dependencies within class instance methods. +- **New Feature**: Multiple Containers: Enhanced the library to support multiple containers. Users can now create and manage multiple dependency containers, enabling better organization and separation of dependencies for different components or modules. +- **Documentation Update**: Expanded and improved the documentation to include details about the newly added method injection feature and additional usage examples. Users can refer to the latest documentation at readthedocs for comprehensive guidance. ### [1.0.0-alpha.1](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.1) (2024-02-25) -- Initial alpha release. -- Added Dependency Container: The library includes a dependency container for managing object dependencies. -- Added Constructor Injection: Users can leverage constructor injection for cleaner and more modular code. -- Added Dependency Scopes: Define and manage the lifecycle of dependencies with support for different scopes. -- Basic Documentation: An initial set of documentation is provided, giving users an introduction to the library. -- License: Released under the GPL 3 license. +- **Initial alpha release**. +- **Added Dependency Container**: The library includes a dependency container for managing object dependencies. +- **Added Constructor Injection**: Users can leverage constructor injection for cleaner and more modular code. +- **Added Dependency Scopes**: Define and manage the lifecycle of dependencies with support for different scopes. +- **Basic Documentation**: An initial set of documentation is provided, giving users an introduction to the library. +- **License**: Released under the GPL 3 license. diff --git a/docs/conf.py b/docs/conf.py index e8e7018..9f3163c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,7 +35,7 @@ version = "1.0" # The full version, including alpha/beta/rc tags -release = "1.0.0-alpha.8" +release = "1.0.0-alpha.9" # -- General configuration --------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index f7a75ec..23cf62d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -5,7 +5,7 @@ py-dependency-injection ======================= -A prototypical dependency injection library for Python. +A dependency injection library for Python. Purpose ------- diff --git a/docs/releases.rst b/docs/releases.rst index b52e84f..1e22003 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -6,24 +6,32 @@ Version History ############### +**1.0.0-alpha.9 (2024-08-08)** + +- **Breaking Change**: Removed constructor injection when resolving dataclasses. +- **Enhancement**: Added dependency container getter and setter for registrations. Also added new `RegistrationSerializer` class for for serializing and deserializing them. These additions provide a more flexible way to interact with the container's registrations. + +`View release on GitHub `_ + **1.0.0-alpha.8 (2024-06-07)** - **Bug Fix**: Fixed an issue in the dependency resolution logic where registered constructor arguments were not properly merged with automatically injected dependencies. This ensures that constructor arguments specified during registration are correctly combined with dependencies resolved by the container. +- **Documentation Update**: The documentation structure has been updated for better organization and ease of understanding. `View release on GitHub `_ **1.0.0-alpha.7 (2024-03-24)** -- Documentation Update: Updated the documentation to provide clearer instructions and more comprehensive examples. -- Preparing for Beta Release: Made necessary adjustments and refinements in preparation for the upcoming first beta release. +- **Documentation Update**: Updated the documentation to provide clearer instructions and more comprehensive examples. +- **Preparing for Beta Release**: Made necessary adjustments and refinements in preparation for the upcoming first beta release. `View release on GitHub `_ **1.0.0-alpha.6 (2024-03-23)** -- Factory Registration: Added support for registering dependencies using factory functions for dynamic instantiation. -- Instance Registration: Enabled registering existing instances as dependencies. -- Tag-based Registration and Resolution: Introduced the ability to register and resolve dependencies using tags for flexible dependency management. +- **Factory Registration**: Added support for registering dependencies using factory functions for dynamic instantiation. +- **Instance Registration**: Enabled registering existing instances as dependencies. +- **Tag-based Registration and Resolution**: Introduced the ability to register and resolve dependencies using tags for flexible dependency management. `View release on GitHub `_ @@ -35,33 +43,33 @@ Version History **1.0.0-alpha.4 (2024-03-02)** -- Constructor Arguments: Support for constructor arguments added to dependency registration. +- **Constructor Arguments**: Support for constructor arguments added to dependency registration. `View release on GitHub `_ **1.0.0-alpha.3 (2024-03-02)** - **Breaking Change**: Starting from this version, the `@inject` decorator can only be used on static class methods and class methods. It can't be used on instance methods anymore. -- Documentation Update: The documentation has been updated to reflect the new restriction on the usage of the decorator. +- **Documentation Update**: The documentation has been updated to reflect the new restriction on the usage of the decorator. `View release on GitHub `_ **1.0.0-alpha.2 (2024-02-27)** -- Python Version Support: Added support for Python versions 3.7, 3.9, 3.10, 3.11, and 3.12. -- New Feature: Method Injection with Decorator: Introduced a new feature allowing method injection using the @inject decorator. Dependencies can now be injected into an instance method, providing more flexibility in managing dependencies within class instance methods. -- New Feature: Multiple Containers: Enhanced the library to support multiple containers. Users can now create and manage multiple dependency containers, enabling better organization and separation of dependencies for different components or modules. -- Documentation Update: Expanded and improved the documentation to include details about the newly added method injection feature and additional usage examples. Users can refer to the latest documentation at readthedocs for comprehensive guidance. +- **Python Version Support**: Added support for Python versions 3.7, 3.9, 3.10, 3.11, and 3.12. +- **New Feature**: Method Injection with Decorator: Introduced a new feature allowing method injection using the @inject decorator. Dependencies can now be injected into an instance method, providing more flexibility in managing dependencies within class instance methods. +- **New Feature**: Multiple Containers: Enhanced the library to support multiple containers. Users can now create and manage multiple dependency containers, enabling better organization and separation of dependencies for different components or modules. +- **Documentation Update**: Expanded and improved the documentation to include details about the newly added method injection feature and additional usage examples. Users can refer to the latest documentation at readthedocs for comprehensive guidance. `View release on GitHub `_ **1.0.0-alpha.1 (2024-02-25)** -- Initial alpha release. -- Added Dependency Container: The library includes a dependency container for managing object dependencies. -- Added Constructor Injection: Users can leverage constructor injection for cleaner and more modular code. -- Added Dependency Scopes: Define and manage the lifecycle of dependencies with support for different scopes. -- Basic Documentation: An initial set of documentation is provided, giving users an introduction to the library. -- License: Released under the GPL 3 license. +- **Initial alpha release**. +- **Added Dependency Container**: The library includes a dependency container for managing object dependencies. +- **Added Constructor Injection**: Users can leverage constructor injection for cleaner and more modular code. +- **Added Dependency Scopes**: Define and manage the lifecycle of dependencies with support for different scopes. +- **Basic Documentation**: An initial set of documentation is provided, giving users an introduction to the library. +- **License**: Released under the GPL 3 license. `View release on GitHub `_ diff --git a/setup.py b/setup.py index 119345d..2796357 100644 --- a/setup.py +++ b/setup.py @@ -6,10 +6,10 @@ setup( name="py-dependency-injection", - version="1.0.0-alpha.8", + version="1.0.0-alpha.9", author="David Runemalm, 2024", author_email="david.runemalm@gmail.com", - description="A prototypical dependency injection library for Python.", + description="A dependency injection library for Python.", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/runemalm/py-dependency-injection", diff --git a/src/dependency_injection/container.py b/src/dependency_injection/container.py index 92abb02..ee7defa 100644 --- a/src/dependency_injection/container.py +++ b/src/dependency_injection/container.py @@ -1,4 +1,5 @@ import inspect +from dataclasses import is_dataclass from typing import Any, Callable, Dict, List, Optional, TypeVar, Type @@ -18,6 +19,7 @@ def __init__(self, name: str = None): self._registrations = {} self._singleton_instances = {} self._scoped_instances = {} + self._has_resolved = False @classmethod def get_instance(cls, name: str = None) -> Self: @@ -29,6 +31,16 @@ def get_instance(cls, name: str = None) -> Self: return cls._instances[(cls, name)] + def get_registrations(self) -> Dict[Type, Registration]: + return self._registrations + + def set_registrations(self, registrations) -> None: + if self._has_resolved: + raise Exception( + "You can't set registrations after a dependency has been resolved." + ) + self._registrations = registrations + def register_transient( self, dependency: Type, @@ -98,6 +110,8 @@ def register_instance( self._singleton_instances[dependency] = instance def resolve(self, dependency: Type, scope_name: str = DEFAULT_SCOPE_NAME) -> Type: + self._has_resolved = True + if scope_name not in self._scoped_instances: self._scoped_instances[scope_name] = {} @@ -176,6 +190,9 @@ def _inject_dependencies( scope_name: str = None, constructor_args: Optional[Dict[str, Any]] = None, ) -> Type: + if is_dataclass(implementation): + return implementation() # Do not inject into dataclasses + constructor = inspect.signature(implementation.__init__) params = constructor.parameters diff --git a/src/dependency_injection/serialization.py b/src/dependency_injection/serialization.py new file mode 100644 index 0000000..2bfbad0 --- /dev/null +++ b/src/dependency_injection/serialization.py @@ -0,0 +1,15 @@ +import pickle + +from typing import Dict, Type + +from dependency_injection.registration import Registration + + +class RegistrationSerializer: + @staticmethod + def serialize(registrations) -> bytes: + return pickle.dumps(registrations) + + @staticmethod + def deserialize(serialized_state) -> Dict[Type, Registration]: + return pickle.loads(serialized_state) diff --git a/tests/unit_test/container/register/test_get_registrations.py b/tests/unit_test/container/register/test_get_registrations.py new file mode 100644 index 0000000..5d9d50c --- /dev/null +++ b/tests/unit_test/container/register/test_get_registrations.py @@ -0,0 +1,32 @@ +from dependency_injection.container import DependencyContainer +from dependency_injection.scope import Scope +from unit_test.unit_test_case import UnitTestCase + + +class TestGetRegistrations(UnitTestCase): + def test_get_registrations_returns_empty_dict_initially(self): + # arrange + dependency_container = DependencyContainer.get_instance() + + # act + registrations = dependency_container.get_registrations() + + # assert + self.assertEqual(registrations, {}) + + def test_get_registrations_returns_correct_registrations(self): + # arrange + class Vehicle: + pass + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Vehicle) + + # act + registrations = dependency_container.get_registrations() + + # assert + self.assertIn(Vehicle, registrations) + self.assertEqual(registrations[Vehicle].dependency, Vehicle) + self.assertEqual(registrations[Vehicle].implementation, Vehicle) + self.assertEqual(registrations[Vehicle].scope, Scope.TRANSIENT) diff --git a/tests/unit_test/container/register/test_set_registrations.py b/tests/unit_test/container/register/test_set_registrations.py new file mode 100644 index 0000000..04d6b46 --- /dev/null +++ b/tests/unit_test/container/register/test_set_registrations.py @@ -0,0 +1,43 @@ +from dependency_injection.container import DependencyContainer +from unit_test.unit_test_case import UnitTestCase + + +class TestSetRegistrations(UnitTestCase): + def test_set_registrations_before_first_resolution(self): + # arrange + class Vehicle: + pass + + dummy_container = DependencyContainer.get_instance("dummy_container") + dummy_container.register_transient(Vehicle) + new_registrations = dummy_container.get_registrations() + + container = DependencyContainer.get_instance() + + # act + container.set_registrations(new_registrations) # no exception + + def test_not_allowed_to_set_registrations_after_first_resolution(self): + # arrange + class Vehicle: + pass + + class Fruit: + pass + + dummy_container = DependencyContainer.get_instance("dummy_container") + dummy_container.register_transient(Vehicle) + new_registrations = dummy_container.get_registrations() + + container = DependencyContainer.get_instance() + container.register_transient(Fruit) + container.resolve(Fruit) + + # act & assert + with self.assertRaises(Exception) as context: + container.set_registrations(new_registrations) + + self.assertIn( + "You can't set registrations after a dependency has been resolved.", + str(context.exception), + ) diff --git a/tests/unit_test/container/resolve/vehicle.py b/tests/unit_test/container/resolve/test_data/vehicle.py similarity index 100% rename from tests/unit_test/container/resolve/vehicle.py rename to tests/unit_test/container/resolve/test_data/vehicle.py diff --git a/tests/unit_test/container/resolve/test_resolve_with_alias.py b/tests/unit_test/container/resolve/test_resolve_with_alias.py index b1cbbbf..a323868 100644 --- a/tests/unit_test/container/resolve/test_resolve_with_alias.py +++ b/tests/unit_test/container/resolve/test_resolve_with_alias.py @@ -2,8 +2,8 @@ from unit_test.unit_test_case import UnitTestCase -from unit_test.container.resolve.vehicle import Vehicle -from unit_test.container.resolve.vehicle import Vehicle as VehicleAlias +from unit_test.container.resolve.test_data.vehicle import Vehicle +from unit_test.container.resolve.test_data.vehicle import Vehicle as VehicleAlias class TestResolveWithAlias(UnitTestCase): diff --git a/tests/unit_test/container/resolve/test_resolve_with_injection.py b/tests/unit_test/container/resolve/test_resolve_with_injection.py index ba5f08e..f8a10f3 100644 --- a/tests/unit_test/container/resolve/test_resolve_with_injection.py +++ b/tests/unit_test/container/resolve/test_resolve_with_injection.py @@ -1,3 +1,5 @@ +from dataclasses import dataclass + from dependency_injection.container import DependencyContainer from unit_test.unit_test_case import UnitTestCase @@ -25,3 +27,25 @@ def __init__(self, engine: Engine): self.assertIsInstance(resolved_dependency, Car) self.assertIsNotNone(resolved_dependency.engine) self.assertIsInstance(resolved_dependency.engine, Engine) + + def test_resolve_skips_constructor_injection_for_dataclass(self): + # arrange + class Engine: + pass + + @dataclass + class Car: + engine: Engine = None + + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(Engine) + dependency_container.register_transient(Car) + + # act + resolved_dependency = dependency_container.resolve(Car) + + # assert + self.assertIsInstance(resolved_dependency, Car) + self.assertIsNone( + resolved_dependency.engine + ) # Should be None since injection is skipped diff --git a/tests/unit_test/serialization/__init__.py b/tests/unit_test/serialization/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_test/serialization/test_registration_serializer.py b/tests/unit_test/serialization/test_registration_serializer.py new file mode 100644 index 0000000..9be563d --- /dev/null +++ b/tests/unit_test/serialization/test_registration_serializer.py @@ -0,0 +1,63 @@ +import pickle + +from unit_test.unit_test_case import UnitTestCase + +from dependency_injection.serialization import RegistrationSerializer +from dependency_injection.registration import Registration +from dependency_injection.scope import Scope + + +class Vehicle: + pass + + +class Car: + def __init__(self, vehicle: Vehicle): + self.vehicle = vehicle + + +class TestRegistrationSerializer(UnitTestCase): + def test_serialize_and_deserialize(self): + # arrange + registrations = { + Vehicle: Registration( + dependency=Vehicle, + implementation=Car, + scope=Scope.TRANSIENT, + tags={"example_tag"}, + constructor_args={"vehicle": Vehicle()}, + factory=None, + factory_args={}, + ) + } + + # act + serialized = RegistrationSerializer.serialize(registrations) + deserialized = RegistrationSerializer.deserialize(serialized) + + # assert + self.assertEqual(deserialized.keys(), registrations.keys()) + self.assertEqual( + deserialized[Vehicle].dependency, registrations[Vehicle].dependency + ) + self.assertEqual( + deserialized[Vehicle].implementation, registrations[Vehicle].implementation + ) + self.assertEqual(deserialized[Vehicle].scope, registrations[Vehicle].scope) + self.assertEqual(deserialized[Vehicle].tags, registrations[Vehicle].tags) + self.assertEqual( + deserialized[Vehicle].constructor_args.keys(), + registrations[Vehicle].constructor_args.keys(), + ) + self.assertEqual(deserialized[Vehicle].factory, registrations[Vehicle].factory) + self.assertEqual( + deserialized[Vehicle].factory_args, registrations[Vehicle].factory_args + ) + + def test_deserialize_invalid_data(self): + # arrange + invalid_data = b"not a valid pickle" + + # act & assert + with self.assertRaises(pickle.UnpicklingError): + RegistrationSerializer.deserialize(invalid_data)