From 41b792cf31cdfafb55c27bc190992ce5ec8f80f1 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Sat, 14 Jun 2025 13:34:25 +0200 Subject: [PATCH 1/4] Add configuration of default container name. --- src/dependency_injection/container.py | 24 +++++++- .../unit_test/container/configure/__init__.py | 0 .../test_configure_default_container.py | 56 +++++++++++++++++++ .../unit_test/container/lifecycle/__init__.py | 0 .../lifecycle/test_clear_instances.py | 22 ++++++++ .../container/obtain/test_obtain_container.py | 2 +- .../resolve/test_resolve_instance.py | 2 +- .../resolve/test_resolve_singleton.py | 2 +- .../resolve/test_resolve_transient.py | 2 +- 9 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 tests/unit_test/container/configure/__init__.py create mode 100644 tests/unit_test/container/configure/test_configure_default_container.py create mode 100644 tests/unit_test/container/lifecycle/__init__.py create mode 100644 tests/unit_test/container/lifecycle/test_clear_instances.py diff --git a/src/dependency_injection/container.py b/src/dependency_injection/container.py index 5b7e78c..8cb11c4 100644 --- a/src/dependency_injection/container.py +++ b/src/dependency_injection/container.py @@ -30,14 +30,22 @@ def get_args(tp): class DependencyContainer(metaclass=SingletonMeta): _default_scope_name: Union[str, Callable[[], str]] = DEFAULT_SCOPE_NAME + _default_container_name: Union[str, Callable[[], str]] = DEFAULT_CONTAINER_NAME - def __init__(self, name: str = None): - self.name = name if name is not None else DEFAULT_CONTAINER_NAME + def __init__(self, name: str): + self.name = name self._registrations = {} self._singleton_instances = {} self._scoped_instances = {} self._has_resolved = False + @classmethod + def configure_default_container_name( + cls, name_or_callable: Union[str, Callable[[], str]] + ) -> None: + """Override the default container name, which can be string or callable.""" + cls._default_container_name = name_or_callable + @classmethod def configure_default_scope_name( cls, default_scope_name: Union[str, Callable[[], str]] @@ -54,7 +62,12 @@ def get_default_scope_name(cls) -> str: @classmethod def get_instance(cls, name: str = None) -> Self: - name = name or DEFAULT_CONTAINER_NAME + if name is None: + name = ( + cls._default_container_name() + if callable(cls._default_container_name) + else cls._default_container_name + ) if (cls, name) not in cls._instances: cls._instances[(cls, name)] = cls(name) @@ -326,3 +339,8 @@ def _unwrap_optional_type(self, annotation: Any) -> Any: def _should_use_default(self, param_info: inspect.Parameter) -> bool: return param_info.default is not inspect.Parameter.empty + + @classmethod + def clear_instances(cls) -> None: + """Clear all container instances. Useful for test teardown.""" + cls._instances.clear() diff --git a/tests/unit_test/container/configure/__init__.py b/tests/unit_test/container/configure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_test/container/configure/test_configure_default_container.py b/tests/unit_test/container/configure/test_configure_default_container.py new file mode 100644 index 0000000..f4072b5 --- /dev/null +++ b/tests/unit_test/container/configure/test_configure_default_container.py @@ -0,0 +1,56 @@ +import uuid +from dependency_injection.container import DependencyContainer +from unit_test.unit_test_case import UnitTestCase + + +class TestConfigureDefaultContainer(UnitTestCase): + def tearDown(self): + # Reset after each test to avoid side effects + DependencyContainer.clear_instances() + DependencyContainer.configure_default_container_name("default_container") + + def test_configure_default_container_name_with_static_string(self): + # arrange + DependencyContainer.configure_default_container_name("custom_default") + + # act + container = DependencyContainer.get_instance() + + # assert + self.assertEqual(container.name, "custom_default") + + def test_configure_default_container_name_with_callable(self): + # arrange + container_name = f"test_{uuid.uuid4()}" + DependencyContainer.configure_default_container_name(lambda: container_name) + + # act + container = DependencyContainer.get_instance() + + # assert + self.assertEqual(container.name, container_name) + + def test_get_instance_returns_different_container_when_default_is_changed(self): + # arrange + default_container = DependencyContainer.get_instance() + DependencyContainer.configure_default_container_name("isolated") + isolated_container = DependencyContainer.get_instance() + + # assert + self.assertNotEqual(default_container, isolated_container) + self.assertEqual(isolated_container.name, "isolated") + + def test_clear_instances_removes_all_containers(self): + # arrange + c1 = DependencyContainer.get_instance("a") + c2 = DependencyContainer.get_instance("b") + + # act + DependencyContainer.clear_instances() + + # assert + c1_new = DependencyContainer.get_instance("a") + c2_new = DependencyContainer.get_instance("b") + + self.assertNotEqual(c1, c1_new) + self.assertNotEqual(c2, c2_new) diff --git a/tests/unit_test/container/lifecycle/__init__.py b/tests/unit_test/container/lifecycle/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit_test/container/lifecycle/test_clear_instances.py b/tests/unit_test/container/lifecycle/test_clear_instances.py new file mode 100644 index 0000000..b75918a --- /dev/null +++ b/tests/unit_test/container/lifecycle/test_clear_instances.py @@ -0,0 +1,22 @@ +from dependency_injection.container import DependencyContainer +from unit_test.unit_test_case import UnitTestCase + + +class TestClearInstances(UnitTestCase): + def tearDown(self): + DependencyContainer.clear_instances() + + def test_clear_instances_removes_all_containers(self): + # arrange + c1 = DependencyContainer.get_instance("a") + c2 = DependencyContainer.get_instance("b") + + # act + DependencyContainer.clear_instances() + + # assert + c1_new = DependencyContainer.get_instance("a") + c2_new = DependencyContainer.get_instance("b") + + self.assertNotEqual(c1, c1_new) + self.assertNotEqual(c2, c2_new) diff --git a/tests/unit_test/container/obtain/test_obtain_container.py b/tests/unit_test/container/obtain/test_obtain_container.py index 9803e75..6e91dd8 100644 --- a/tests/unit_test/container/obtain/test_obtain_container.py +++ b/tests/unit_test/container/obtain/test_obtain_container.py @@ -2,7 +2,7 @@ from unit_test.unit_test_case import UnitTestCase -class TestObtainInstance(UnitTestCase): +class TestObtainContainer(UnitTestCase): def test_obtain_instance_without_name_returns_default_container( self, ): diff --git a/tests/unit_test/container/resolve/test_resolve_instance.py b/tests/unit_test/container/resolve/test_resolve_instance.py index 105925b..9f2bb13 100644 --- a/tests/unit_test/container/resolve/test_resolve_instance.py +++ b/tests/unit_test/container/resolve/test_resolve_instance.py @@ -33,7 +33,7 @@ class Vehicle: class Car(Vehicle): pass - dependency_container = DependencyContainer() + dependency_container = DependencyContainer.get_instance() instance = Car() dependency_container.register_instance(Vehicle, instance) diff --git a/tests/unit_test/container/resolve/test_resolve_singleton.py b/tests/unit_test/container/resolve/test_resolve_singleton.py index 0c5a10e..d889fb4 100644 --- a/tests/unit_test/container/resolve/test_resolve_singleton.py +++ b/tests/unit_test/container/resolve/test_resolve_singleton.py @@ -34,7 +34,7 @@ class Vehicle: class Car(Vehicle): pass - dependency_container = DependencyContainer() + dependency_container = DependencyContainer.get_instance() dependency = Vehicle implementation = Car dependency_container.register_singleton(dependency, implementation) diff --git a/tests/unit_test/container/resolve/test_resolve_transient.py b/tests/unit_test/container/resolve/test_resolve_transient.py index 41222de..e682db1 100644 --- a/tests/unit_test/container/resolve/test_resolve_transient.py +++ b/tests/unit_test/container/resolve/test_resolve_transient.py @@ -34,7 +34,7 @@ class Vehicle: class Car(Vehicle): pass - dependency_container = DependencyContainer() + dependency_container = DependencyContainer.get_instance() dependency = Vehicle implementation = Car dependency_container.register_transient(dependency, implementation) From f4341c45a0a5da893761d5bad11d65d46647b929 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Sat, 14 Jun 2025 15:08:36 +0200 Subject: [PATCH 2/4] Make README more clean, add 'why' section. --- README.md | 121 +++++++++++++++--------------------------------------- 1 file changed, 34 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 606810b..1060945 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ [![License](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.html) -![First Principles Software](https://img.shields.io/badge/Powered_by-First_Principles_Software-blue) +[![Author: David Runemalm](https://img.shields.io/badge/Author-David%20Runemalm-blue)](https://www.davidrunemalm.com) [![Master workflow](https://github.com/runemalm/py-dependency-injection/actions/workflows/master.yml/badge.svg?branch=master)](https://github.com/runemalm/py-dependency-injection/actions/workflows/master.yml) +[![PyPI version](https://badge.fury.io/py/py-dependency-injection.svg)](https://pypi.org/project/py-dependency-injection/) +![Downloads](https://pepy.tech/badge/py-dependency-injection) # py-dependency-injection A dependency injection library for Python. +## Why py-dependency-injection? + +`py-dependency-injection` is inspired by the built-in dependency injection system in **ASP.NET Core**. It provides a lightweight and extensible way to manage dependencies in Python applications. By promoting constructor injection and supporting scoped lifetimes, it encourages clean architecture and makes testable, maintainable code the default. + ## Features - **Scoped Registrations:** Define the lifetime of your dependencies as transient, scoped, or singleton. @@ -35,38 +41,36 @@ Here's a quick example to get you started: ```python from dependency_injection.container import DependencyContainer -# Define an abstract Connection -class Connection: - pass +# Define an abstract payment gateway interface +class PaymentGateway: + def charge(self, amount: int, currency: str): + raise NotImplementedError() -# Define a specific implementation of the Connection -class PostgresConnection(Connection): - def connect(self): - print("Connecting to PostgreSQL database...") +# A concrete implementation using Stripe +class StripeGateway(PaymentGateway): + def charge(self, amount: int, currency: str): + print(f"Charging {amount} {currency} using Stripe...") -# Define a repository that depends on some type of Connection -class UserRepository: - def __init__(self, connection: Connection): - self._connection = connection +# A service that depends on the payment gateway +class CheckoutService: + def __init__(self, gateway: PaymentGateway): + self._gateway = gateway - def fetch_users(self): - self._connection.connect() - print("Fetching users from the database...") + def checkout(self): + self._gateway.charge(2000, "USD") # e.g. $20.00 -# Get an instance of the (default) DependencyContainer +# Get the default dependency container container = DependencyContainer.get_instance() -# Register the specific connection type as a singleton instance -container.register_singleton(Connection, PostgresConnection) +# Register StripeGateway as a singleton (shared for the app's lifetime) +container.register_singleton(PaymentGateway, StripeGateway) -# Register UserRepository as a transient (new instance every time) -container.register_transient(UserRepository) +# Register CheckoutService as transient (new instance per resolve) +container.register_transient(CheckoutService) -# Resolve an instance of UserRepository, automatically injecting the required Connection -user_repository = container.resolve(UserRepository) - -# Use the resolved user_repository to perform an operation -user_repository.find_all() +# Resolve and use the service +checkout = container.resolve(CheckoutService) +checkout.checkout() ``` ## Documentation @@ -81,69 +85,12 @@ For more advanced usage and examples, please visit our [readthedocs](https://py- You can find the source code for `py-dependency-injection` on [GitHub](https://github.com/runemalm/py-dependency-injection). -## Release Notes - -### [1.0.0-beta.2](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.2) (2025-06-09) - -- **Enhancement**: Constructor parameters with default values or `Optional[...]` are now supported without requiring explicit registration. -- **Python Version Support**: Added support for Python version 3.13. - -### [1.0.0-beta.1](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.1) (2025-01-06) - -- **Transition to Beta**: Transitioned from alpha to beta. Features have been stabilized and are ready for broader testing. -- **Removal**: We have removed the dependency container getter and setter functions, as well as the RegistrationSerializer class, which were first introduced in v1.0.0-alpha.9. This decision reflects our focus on maintaining a streamlined library that emphasizes core functionality. These features, which would not be widely used, added unnecessary complexity without offering significant value. By removing them, we are reinforcing our commitment to our design principles. -- **Enhancement**: Added suppprt for configuring default scope name. Either a static string value, or a callable that returns the name. - -### [1.0.0-alpha.10](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.10) (2024-08-11) - -- **Tagged Constructor Injection**: Introduced support for constructor injection using the `Tagged`, `AnyTagged`, and `AllTagged` classes. This allows for seamless injection of dependencies that have been registered with specific tags, enhancing flexibility and control in managing your application's dependencies. - -### [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. -- **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. - -### [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. - -### [1.0.0-alpha.5](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.5) (2024-03-03) - -- **Critical Package Integrity Fix**: This release addresses a critical issue that affected the packaging of the Python library in all previous alpha releases (1.0.0-alpha.1 to 1.0.0-alpha.4). The problem involved missing source files in the distribution, rendering the library incomplete and non-functional. Users are strongly advised to upgrade to version 1.0.0-alpha.5 to ensure the correct functioning of the library. All previous alpha releases are affected by this issue. - -### [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. - -### [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. - -### [1.0.0-alpha.2](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.2) (2024-02-27) +## Release Notes -- **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. +### Latest: [1.0.0-beta.3](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-beta.3) (2025-06-14) -### [1.0.0-alpha.1](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.1) (2024-02-25) +- **Enhancement**: Added `DependencyContainer.configure_default_container_name(...)` to support container isolation in parallel tests, even when application code uses a single shared container via `DependencyContainer.get_instance()`. +- **Enhancement**: Added `DependencyContainer.clear_instances()` as a clean alternative to manually resetting `_instances` during test teardown. -- **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. +➡️ Full changelog: [GitHub Releases](https://github.com/runemalm/py-dependency-injection/releases) From 3fa537424c00f1253436709a61bf0a5d01c14cbd Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Sat, 14 Jun 2025 15:31:54 +0200 Subject: [PATCH 3/4] Update also docs introduction and overview to align. --- docs/index.rst | 20 ++++++++++++-------- docs/userguide.rst | 14 +++++++------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 3608fce..83b794e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,21 +6,25 @@ py-dependency-injection ======================= -A dependency injection library for Python. +A dependency injection library for Python — inspired by the built-in DI system in ASP.NET Core. -Purpose -------- +Overview +-------- -Dependency injection is a powerful design pattern that promotes loose coupling and enhances testability in software applications. `py-dependency-injection` is a prototypical implementation of this pattern, designed to provide the essential features needed for effective dependency management in both small scripts and larger software projects. +`py-dependency-injection` provides a lightweight and extensible way to manage dependencies in Python applications. It promotes constructor injection, supports scoped lifetimes, and encourages clean architecture through explicit configuration and testable design. -This library is particularly suited for beginners exploring the concept of dependency injection, as it offers a straightforward and easy-to-understand implementation. It serves as an excellent starting point for learning the pattern and can also be used as a foundational base for frameworks requiring a more specialized interface for dependency injection. +This library is well-suited for both standalone use in Python applications and as a foundation for frameworks or tools that require structured dependency management. Key Advantages -------------- -- **Suitable for Learning:** Ideal for beginners exploring the concept of dependency injection. -- **Base Implementation for Frameworks:** Can be used as a foundational base for frameworks requiring a more specialized interface for dependency injection. -- **Standalone Solution:** Can also be used on its own, as a fully-featured dependency injection solution in any software project. +- **Familiar model** – Inspired by ASP.NET Core’s DI system +- **Scoped lifetimes** – Support for `singleton`, `scoped`, and `transient` registrations +- **Explicit injection** – Promotes clarity over magic +- **Test-friendly** – Designed for container isolation and overrides +- **Minimalistic** – Easy to use, extend, and integrate + +You can find the source code for `py-dependency-injection` in our `GitHub repository `_. .. userguide-docs: .. toctree:: diff --git a/docs/userguide.rst b/docs/userguide.rst index 9be004e..2863cff 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -9,9 +9,9 @@ Getting Started Introduction ############ -`py-dependency-injection` is a lightweight and flexible dependency injection library for Python. It simplifies managing dependencies in your applications, promoting cleaner and more testable code. +`py-dependency-injection` is a lightweight and extensible dependency injection library for Python — inspired by the built-in DI system in **ASP.NET Core**. It promotes constructor injection, supports scoped lifetimes, and encourages clean, testable application architecture. -This guide will help you understand the key concepts and how to start using the library. For detailed examples, see the `Examples` section. +This guide provides an overview of the key concepts and demonstrates how to start using the library effectively. For detailed examples, see the `Examples` section. ############ Installation @@ -75,11 +75,11 @@ Basic workflow: Best Practices ############## -- **Use Constructor Injection**: Preferred for most cases as it promotes clear and testable designs. -- **Leverage Tags for Organization**: Group dependencies logically using tags. -- **Choose the Right Scope**: Use scoped or singleton lifetimes to optimize performance and resource usage. -- **Keep Dependencies Decoupled**: Avoid tightly coupling your components to the container. -- **Isolate Contexts with Containers**: Use multiple containers to manage dependencies for separate modules or contexts. +- **Prefer Constructor Injection**: It promotes clear interfaces and testable components. +- **Use the Right Lifetime**: Choose between transient, scoped, and singleton based on your component's role. +- **Organize with Tags**: Use tag-based registration and resolution to group related services. +- **Avoid Container Coupling**: Inject dependencies via constructors rather than accessing the container directly. +- **Use Multiple Containers When Needed**: For modular apps or test isolation, create dedicated containers. ################# Where to Go Next? From 33c7dcf892d43c31416c5112eb592699d03e6736 Mon Sep 17 00:00:00 2001 From: David Runemalm Date: Sat, 14 Jun 2025 15:32:15 +0200 Subject: [PATCH 4/4] Bump version to v1.0.0-beta.3 --- docs/conf.py | 2 +- docs/releases.rst | 7 +++++++ setup.py | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index d92f95b..57effdf 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-beta.2" +release = "1.0.0-beta.3" # -- General configuration --------------------------------------------------- diff --git a/docs/releases.rst b/docs/releases.rst index 3cd8f93..281ace3 100644 --- a/docs/releases.rst +++ b/docs/releases.rst @@ -7,6 +7,13 @@ Version History ############### +**1.0.0-beta.3 (2025-06-14)** + +- **Enhancement**: Added `DependencyContainer.configure_default_container_name(...)` to support container isolation in parallel tests, even when application code uses a single shared container via `DependencyContainer.get_instance()`. +- **Enhancement**: Added `DependencyContainer.clear_instances()` as a clean alternative to manually resetting `_instances` during test teardown. + +`View release on GitHub `_ + **1.0.0-beta.2 (2025-06-09)** - **Enhancement**: Constructor parameters with default values or `Optional[...]` are now supported without requiring explicit registration. diff --git a/setup.py b/setup.py index 7dda779..7f80b3b 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="py-dependency-injection", - version="1.0.0-beta.2", + version="1.0.0-beta.3", author="David Runemalm, 2025", author_email="david.runemalm@gmail.com", description="A dependency injection library for Python.",