diff --git a/Makefile b/Makefile index 664b00d..a7b533d 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,7 @@ HOME := $(shell echo ~) PWD := $(shell pwd) SRC := $(PWD)/src TESTS := $(PWD)/tests +DOCS := $(PWD)/docs # Load env file include env.make @@ -29,7 +30,7 @@ help: .PHONY: test test: ## run test suite - PYTHONPATH=./src:./tests pipenv run pytest -n 1 $(TESTS) + PYTHONPATH=$(SRC):$(TESTS) pipenv run pytest $(TESTS) ################################################################################ # RELEASE @@ -42,8 +43,10 @@ build: ## build the python package .PHONY: clean clean: ## clean the build python setup.py clean - rm -rf build dist py_dependency_injection.egg-info - find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete + rm -rf build dist + find . -type f -name '*.py[co]' -delete + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type d -name '*.egg-info' -exec rm -rf {} + .PHONY: bump_version bump_version: ## Bump the version @@ -67,23 +70,25 @@ sphinx-html: ## build the sphinx html .PHONY: sphinx-rebuild sphinx-rebuild: ## re-build the sphinx docs - make -C docs clean && make -C docs html + cd $(DOCS) && \ + pipenv run make clean && pipenv run make html .PHONY: sphinx-autobuild sphinx-autobuild: ## activate autobuild of docs - pipenv run sphinx-autobuild docs docs/_build/html --watch $(SRC) + cd $(DOCS) && \ + pipenv run sphinx-autobuild . _build/html --watch $(SRC) ################################################################################ -# FORMAT & LINT +# PRE-COMMIT HOOKS ################################################################################ .PHONY: black black: ## run black auto-formatting - pipenv run black $(SRC) $(TESTS) --line-length=88 + pipenv run black $(SRC) $(TESTS) .PHONY: black-check black-check: ## check code don't violate black formatting rules - pipenv run black --check $(SRC) --line-length=88 + pipenv run black --check $(SRC) .PHONY: flake flake: ## lint code with flake @@ -105,12 +110,12 @@ pre-commit-run: ## run the pre-commit hooks pipenv-install: ## setup the virtual environment pipenv install --dev -.PHONY: pipenv-packages-install -pipenv-packages-install: ## install a package (uses PACKAGE) +.PHONY: pipenv-install-package +pipenv-install-package: ## install a package (uses PACKAGE) pipenv install $(PACKAGE) -.PHONY: pipenv-packages-install-dev -pipenv-packages-install-dev: ## install a dev package (uses PACKAGE) +.PHONY: pipenv-install-package-dev +pipenv-install-package-dev: ## install a dev package (uses PACKAGE) pipenv install --dev $(PACKAGE) .PHONY: pipenv-packages-graph @@ -121,12 +126,12 @@ pipenv-packages-graph: ## Check installed packages pipenv-requirements-generate: ## Check a requirements.txt pipenv lock -r > requirements.txt -.PHONY: pipenv-venv-activate -pipenv-venv-activate: ## Activate the virtual environment +.PHONY: pipenv-shell +pipenv-shell: ## Activate the virtual environment pipenv shell -.PHONY: pipenv-venv-path -pipenv-venv-path: ## Show the path to the venv +.PHONY: pipenv-venv +pipenv-venv: ## Show the path to the venv pipenv --venv .PHONY: pipenv-lock-and-install diff --git a/README.md b/README.md index 37b19e1..5659367 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,21 @@ # py-dependency-injection -A dependency injection library for Python. +A prototypical dependency injection library for Python. ## Features -- **Dependency Container:** Manage and resolve object dependencies with a flexible and easy-to-use container. -- **Dependency Scopes:** Define different scopes for dependencies, allowing for fine-grained control over their lifecycle. -- **Constructor Injection:** Inject dependencies into constructors, promoting cleaner and more modular code. -- **Method Injection:** Inject dependencies into methods, enabling more flexible dependency management within class instances. -- **Tags:** Register and resolve dependencies using tags, facilitating flexible and dynamic dependency management. -- **Factory Registration:** Register dependencies using factory functions for dynamic instantiation. -- **Instance Registration:** Register existing instances as dependencies, providing more control over object creation. -- **Python Compatibility:** Compatible with Python versions 3.7 to 3.12, ensuring broad compatibility with existing and future Python projects. +- **Scoped Registrations:** Define the lifetime of your dependencies as transient, scoped, or singleton. +- **Constructor Injection:** Automatically resolve and inject dependencies when creating instances. +- **Method Injection:** Inject dependencies into methods using a simple decorator. +- **Factory Functions:** Register factory functions, classes, or lambdas to create dependencies. +- **Instance Registration:** Register existing instances as dependencies. +- **Tag-Based Registration and Resolution:** Organize and resolve dependencies based on tags. +- **Multiple Containers:** Support for using multiple dependency containers. ## Compatibility -This library is compatible with the following Python versions: +The library is compatible with the following Python versions: - 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 @@ -29,138 +28,70 @@ This library is compatible with the following Python versions: $ pip install py-dependency-injection ``` -## Basic Usage +## Quick Start -The following examples demonstrates how to use the library. - -### Creating a Dependency Container +Here's a quick example to get you started: ```python -# Get the default dependency container -dependency_container = DependencyContainer.get_instance() - -# Create additional named containers if needed -another_container = DependencyContainer.get_instance(name="another_container") -``` +from dependency_injection.container import DependencyContainer -### Registering Dependencies with Scopes - -```python -# Register a transient dependency (a new instance every time) -dependency_container.register_transient(Connection, PostgresConnection) +# Define an abstract Connection +class Connection: + pass -# Register a scoped dependency (a new instance per scope) -dependency_container.register_scoped(Connection, PostgresConnection, scope_name="http_request") +# Define a specific implementation of the Connection +class PostgresConnection(Connection): + def connect(self): + print("Connecting to PostgreSQL database...") -# Register a singleton dependency (a single instance for the container's lifetime) -dependency_container.register_singleton(Connection, PostgresConnection) -``` - -### Using Constructor Arguments - -```python -# Register a dependency with constructor arguments -dependency_container.register_transient( - Connection, - PostgresConnection, - constructor_args={"host": "localhost", "port": 5432} -) -``` - -### Using Factory Functions - -```python -# Define a factory function -def create_connection(host: str, port: int) -> Connection: - return PostgresConnection(host=host, port=port) +# Define a repository that depends on some type of Connection +class UserRepository: + def __init__(self, connection: Connection): + self._connection = connection -# Register the factory function -dependency_container.register_factory(Connection, create_connection, factory_args={"host": "localhost", "port": 5432}) -``` + def fetch_users(self): + self._connection.connect() + print("Fetching users from the database...") -Besides functions, you can also use lambdas and class functions. Read more in the [documentation](https://py-dependency-injection.readthedocs.io/en/latest/userguide.html#using-factory-functions). +# Get an instance of the (default) DependencyContainer +container = DependencyContainer.get_instance() -### Registering and Using Instances +# Register the specific connection type as a singleton instance +container.register_singleton(Connection, PostgresConnection) -```python -# Create an instance -my_connection = PostgresConnection(host="localhost", port=5432) +# Register UserRepository as a transient (new instance every time) +container.register_transient(UserRepository) -# Register the instance -dependency_container.register_instance(Connection, my_connection) +# Resolve an instance of UserRepository, automatically injecting the required Connection +user_repository = container.resolve(UserRepository) -# Resolve the instance -resolved_connection = dependency_container.resolve(Connection) -print(resolved_connection.host) # Output: localhost +# Use the resolved user_repository to perform an operation +user_repository.find_all() ``` -### Registering and Resolving with Tags - -```python -# Register dependencies with tags -dependency_container.register_transient(Connection, PostgresConnection, tags={"Querying", "Startable"}) -dependency_container.register_scoped(BusConnection, KafkaBusConnection, tags={"Publishing", "Startable"}) - -# Resolve dependencies by tags -startable_dependencies = dependency_container.resolve_all(tags={"Startable"}) -for dependency in startable_dependencies: - dependency.start() -``` - -### Using Constructor Injection - -```python -class OrderRepository: - def __init__(self, connection: Connection): - self.connection = connection - -# Register dependencies -dependency_container.register_transient(OrderRepository) -dependency_container.register_singleton(Connection, PostgresConnection) - -# Resolve the OrderRepository with injected dependencies -repository = dependency_container.resolve(OrderRepository) -print(repository.connection.__class__.__name__) # Output: PostgresConnection -``` +## Documentation -### Using Method Injection +For more advanced usage and examples, please visit our [readthedocs](https://py-dependency-injection.readthedocs.io/en/latest/) page. -```python -class OrderController: - @staticmethod - @inject() - def place_order(order: Order, repository: OrderRepository): - order.status = "placed" - repository.save(order) - -# Register the dependency -dependency_container.register_transient(OrderRepository) -dependency_container.register_singleton(Connection, PostgresConnection) - -# Use method injection to inject the dependency -my_order = Order.create() -OrderController.place_order(order=my_order) # The repository instance will be automatically injected -``` +## License -You can also specify container and scope using the decorator arguments `container_name` and `scope_name`. +`py-dependency-injection` is released under the GPL 3 license. See [LICENSE](LICENSE) for more details. -## Documentation +## Source Code -For the latest documentation, visit [readthedocs](https://py-dependency-injection.readthedocs.io/en/latest/). +You can find the source code for `py-dependency-injection` on [GitHub](https://github.com/runemalm/py-dependency-injection). -## Contribution +## Release Notes -To contribute, create a pull request on the develop branch following the [git flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. +### [1.0.0-alpha.8](https://github.com/runemalm/py-dependency-injection/releases/tag/v1.0.0-alpha.8) (2024-06-07) -## Release Notes +- 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. ### [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. -This release focuses on enhancing the documentation and making final preparations for the transition to the beta phase. If you have any more updates or need further assistance, feel free to reach out! - ### [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. diff --git a/docs/concepts.rst b/docs/concepts.rst deleted file mode 100644 index 130c536..0000000 --- a/docs/concepts.rst +++ /dev/null @@ -1,67 +0,0 @@ -############## -Basic Concepts -############## - - -Dependency Injection --------------------- - -Dependency Injection (DI) is a design pattern that enables the inversion of control in software applications by allowing the injection of dependencies from external sources. In the context of `py-dependency-injection`, it simplifies the management of object dependencies and promotes modular and testable code. - - -Dependency Container --------------------- - -The Dependency Container is a central component that manages the registration and resolution of dependencies. It acts as a repository for holding instances of classes and their dependencies, facilitating the inversion of control provided by dependency injection. - - -Constructor Injection ---------------------- - -Constructor Injection is a form of dependency injection where dependencies are provided through a class's constructor. This pattern enhances code readability, maintainability, and testability by explicitly declaring and injecting dependencies when creating an object. - - -Method Injection ----------------- - -Method Injection is another form of dependency injection where dependencies are injected into an object's method rather than its constructor. This allows for more flexible and dynamic dependency management, as dependencies can be provided at the time of method invocation. - - -Factory Registration --------------------- - -Factory Registration is a technique where a factory function or class is used to create instances of a dependency. This allows for more complex instantiation logic, such as conditional creation based on runtime parameters or integration with external resources. - - -Instance Registration ---------------------- - -Instance Registration involves registering an already created instance of an object as a dependency. This is useful when you want to use a specific instance with a predefined state or when integrating with third-party libraries that provide instances of their classes. - - -Tags ----- - -Tags are used to categorize and identify dependencies within the container. By registering and resolving dependencies with tags, you can group related dependencies and retrieve them collectively. This is particularly useful in scenarios where you need to apply the same operation to multiple dependencies or when you want to resolve dependencies based on certain criteria. - - -Scoped Dependencies -------------------- - -Scoped Dependencies refer to instances of objects that have a limited scope during their lifecycle. In `py-dependency-injection`, you can register dependencies with three different scopes, which are transient, scoped, or singleton, allowing control over how instances are created and managed. - - -Dependency Scopes ------------------ - -Dependency Scopes define the lifecycle and visibility of a dependency within the application. The `py-dependency-injection` library supports three scopes: - -- **Transient**: A new instance is created each time the dependency is resolved. -- **Scoped**: A single instance is created within a specific scope (e.g., a request in a web application) and reused across that scope. -- **Singleton**: A single instance is created and shared throughout the application's lifetime. - - -Dependency Resolution ---------------------- - -Dependency Resolution is the process of retrieving an instance of a required dependency from the container. The `py-dependency-injection` library provides various methods for resolving dependencies, including direct resolution by type, resolution by tag, and resolution with constructor or method injection. diff --git a/docs/conf.py b/docs/conf.py index bd61a2d..e8e7018 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.7" +release = "1.0.0-alpha.8" # -- General configuration --------------------------------------------------- @@ -84,6 +84,6 @@ html_static_path = ["_static"] intersphinx_mapping = { - "python": ("https://docs.python.org/", None), + "python": ("https://docs.python.org/3", None), "sqlalchemy": ("http://docs.sqlalchemy.org/en/latest/", None), } diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..1346e04 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,262 @@ +############################## +Creating dependency containers +############################## + +In this example, we demonstrate how to create and retrieve dependency containers using the `DependencyContainer` class. This is useful when you want to manage dependencies in different contexts or areas of your application. + +.. code-block:: python + + from dependency_injection.container import DependencyContainer + + # Get the default dependency container + dependency_container = DependencyContainer.get_instance() + + # Get an additional container if necessary + another_container = DependencyContainer.get_instance( + name="another_container" + ) + + +#################################### +Registering dependencies with scopes +#################################### + +This example shows how to register dependencies with different scopes (transient, scoped, and singleton). This is important for controlling the lifecycle and reuse of your dependencies. + +.. code-block:: python + + # Register a transient dependency + dependency_container.register_transient( + Connection, + PostgresConnection + ) + + # Register a scoped dependency + dependency_container.register_scoped( + Connection, + PostgresConnection, + scope_name="http_request" + ) + + # Register a singleton dependency + dependency_container.register_singleton( + Connection, + PostgresConnection + ) + + +###################################### +Registering with constructor arguments +###################################### + +Here, we illustrate how to register a dependency with constructor arguments. This allows you to provide specific values or configurations to your dependencies when they are instantiated. + +.. code-block:: python + + # Register with constructor arguments + dependency_container.register_transient( + Connection, + PostgresConnection, + constructor_args={ + "host": "localhost", + "port": 5432 + } + ) + + +################################ +Resolving with factory functions +################################ + +In this section, we demonstrate how to register and resolve dependencies using the factory pattern. This provides flexibility in how your dependencies are created and configured. You can use factory functions, factory classes and factory lambdas. + +.. note:: + Any `callable `_ can be used as factory. + +.. code-block:: python + + # Define factory function + def factory_function(host: str, port: int) -> Connection: + return PostgresConnection( + host=host, + port=port + ) + + # Register with factory function + dependency_container.register_factory( + Connection, + factory_function, + factory_args={ + "host": "localhost", + "port": 5432 + } + ) + +.. code-block:: python + + # Define factory class + class FactoryClass: + @staticmethod + def create(host: str, port: int) -> Connection: + return PostgresConnection( + host=host, + port=port + ) + + # Register with factory class + dependency_container.register_factory( + Connection, + FactoryClass.create, + factory_args={ + "host": "localhost", + "port": 5432 + } + ) + +.. code-block:: python + + # Register with lambda factory function + dependency_container.register_factory( + Connection, + lambda host, port: PostgresConnection( + host=host, + port=port + ), + factory_args={ + "host": "localhost", + "port": 5432 + } + ) + + +############################### +Registering and using instances +############################### + +This example demonstrates how to register and use instances of your dependencies. This is useful when you want to provide a specific instance of a dependency for use throughout your application. + +.. code-block:: python + + # Create instance + instance = PostgresConnection( + host="localhost", + port=5432 + ) + + # Register instance + dependency_container.register_instance( + Connection, + instance + ) + + # Resolve instance + resolved_instance = dependency_container.resolve(Connection) + print(resolved_instance.host) # Output: localhost + + +################################### +Registering and resolving with tags +################################### + +In this example, we show how to register and resolve dependencies using tags. This allows you to categorize and retrieve specific groups of dependencies based on their tags. + +.. code-block:: python + + # Register with tags + dependency_container.register_scoped( + Connection, + PostgresConnection, + tags={ + Querying, + Startable + } + ) + + # Register another dependency with tags + dependency_container.register_scoped( + BusConnection, + KafkaBusConnection, + tags={ + Publishing, + Startable + } + ) + + # Resolve all dependencies with the 'Startable' tag + resolved_dependencies = dependency_container.resolve_all( + tags={ + Startable + } + ) + + # Use resolved dependencies + for dependency in resolved_dependencies: + dependency.start() + + +########################### +Using constructor injection +########################### + +This example illustrates how to use constructor injection to automatically inject dependencies into your classes. This is a common pattern for managing dependencies in object-oriented programming. This is probably how you'll want to resolve 99% of the dependencies in your software application. + +.. code-block:: python + + class OrderRepository: + def __init__(self, connection: Connection): + self.connection = connection + + # Register dependencies + dependency_container.register_transient( + OrderRepository + ) + + dependency_container.register_singleton( + Connection, + PostgresConnection + ) + + # Resolve with injected dependencies + repository = dependency_container.resolve( + OrderRepository + ) + + # Use injected dependency + print(repository.connection.__class__.__name__) # Output: PostgresConnection + + +###################### +Using method injection +###################### + +This example demonstrates how to use method injection to inject dependencies into methods at runtime. This is useful for dynamically providing dependencies to class- or static methods, without affecting the entire class. + +.. note:: + You can pass the arguments ``container_name`` and ``scope_name`` to ``@inject``. + +.. note:: + The ``@inject`` has to be applied to the function after the ``@classmethod`` or ``@staticmethod``. + +.. code-block:: python + + class OrderController: + @staticmethod + @inject() + def place_order(order: Order, repository: OrderRepository): + order.set_status("placed") + repository.save(order) + + # Register dependencies + dependency_container.register_transient( + OrderRepository + ) + + dependency_container.register_singleton( + Connection, + PostgresConnection + ) + + # Call decorated method (missing argument will be injected) + OrderController.place_order( + order=Order.create() + ) diff --git a/docs/index.rst b/docs/index.rst index 403b345..f7a75ec 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,52 +1,46 @@ +.. warning:: + + This library is currently in the alpha stage of development. Expect changes and improvements as we work towards a stable release. + py-dependency-injection ======================= -Welcome to the documentation for the `py-dependency-injection` library. +A prototypical dependency injection library for Python. -Introduction -============ +Purpose +------- -The `py-dependency-injection` library simplifies and enhances the management of dependencies in your Python projects. Whether you're new to the concept of dependency injection or an experienced developer seeking an efficient solution, this guide is designed to help you grasp the fundamentals and leverage the features offered by the library. +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. -Features -============ +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. -- **Dependency Container:** Manage and resolve object dependencies with a flexible and easy-to-use container. -- **Dependency Scopes:** Define different scopes for dependencies, allowing for fine-grained control over their lifecycle. -- **Constructor Injection:** Inject dependencies into constructors, promoting cleaner and more modular code. -- **Method Injection:** Inject dependencies into methods, enabling more flexible dependency management within class instances. -- **Tags:** Register and resolve dependencies using tags, facilitating flexible and dynamic dependency management. -- **Factory Registration:** Register dependencies using factory functions for dynamic instantiation. -- **Instance Registration:** Register existing instances as dependencies, providing more control over object creation. -- **Python Compatibility:** Compatible with Python versions 3.7 to 3.12, ensuring broad compatibility with existing and future Python projects. +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. .. userguide-docs: .. toctree:: :maxdepth: 1 - :caption: User guide + :caption: User Guide userguide -.. concepts-docs: +.. examples-docs: .. toctree:: :maxdepth: 1 - :caption: Basic Concepts + :caption: Examples - concepts + examples -.. versionhistory-docs: +.. releases-docs: .. toctree:: :maxdepth: 1 :caption: Releases - versionhistory - -.. community-docs: -.. toctree:: - :maxdepth: 1 - :caption: Community - - community + releases .. apireference-docs: .. toctree:: @@ -54,3 +48,5 @@ Features :caption: API Reference py-modindex + +You can find the source code for `py-dependency-injection` in our `GitHub repository `_. diff --git a/docs/py-modindex.rst b/docs/py-modindex.rst index 3cdb8ca..bcb43b4 100644 --- a/docs/py-modindex.rst +++ b/docs/py-modindex.rst @@ -1,2 +1,2 @@ -API reference +API Reference ============= diff --git a/docs/versionhistory.rst b/docs/releases.rst similarity index 86% rename from docs/versionhistory.rst rename to docs/releases.rst index ef33413..b52e84f 100644 --- a/docs/versionhistory.rst +++ b/docs/releases.rst @@ -1,14 +1,22 @@ +.. warning:: + + This library is currently in the alpha stage of development. Expect changes and improvements as we work towards a stable release. + ############### -Version history +Version History ############### +**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. + +`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. -This release focuses on enhancing the documentation and making final preparations for the transition to the beta phase. If you have any more updates or need further assistance, feel free to reach out! - `View release on GitHub `_ **1.0.0-alpha.6 (2024-03-23)** diff --git a/docs/userguide.rst b/docs/userguide.rst index 3407d8a..0463dd5 100644 --- a/docs/userguide.rst +++ b/docs/userguide.rst @@ -2,159 +2,9 @@ Getting Started ############### - Installation ------------- +--------------- Install using `pip `_:: $ pip install py-dependency-injection - - -Creating a Dependency Container -------------------------------- - -.. code-block:: python - - # Get the default dependency container - dependency_container = DependencyContainer.get_instance() - - # Create additional named containers if needed - another_container = DependencyContainer.get_instance(name="another_container") - - -Registering Dependencies with Scopes ------------------------------------- - -.. code-block:: python - - # Register a transient dependency (a new instance every time) - dependency_container.register_transient(Connection, PostgresConnection) - - # Register a scoped dependency (a new instance per scope) - dependency_container.register_scoped(Connection, PostgresConnection, scope_name="http_request") - - # Register a singleton dependency (a single instance for the container's lifetime) - dependency_container.register_singleton(Connection, PostgresConnection) - - -Using Constructor Arguments ---------------------------- - -.. code-block:: python - - # Register a dependency with constructor arguments - dependency_container.register_transient( - Connection, - PostgresConnection, - constructor_args={"host": "localhost", "port": 5432} - ) - - -Using Factory Functions ------------------------ - -.. code-block:: python - - # Define a factory function - def create_connection(host: str, port: int) -> Connection: - return PostgresConnection(host=host, port=port) - - # Register dependency with the factory function - dependency_container.register_factory(Connection, create_connection, factory_args={"host": "localhost", "port": 5432}) - -.. code-block:: python - - # Register dependency with a lambda factory function - dependency_container.register_factory( - Connection, - lambda host, port: PostgresConnection(host=host, port=port), - factory_args={"host": "localhost", "port": 5432} - ) - -.. code-block:: python - - # Register dependency with a factory class function - class ConnectionFactory: - @staticmethod - def create_connection(host: str, port: int) -> Connection: - return PostgresConnection(host=host, port=port) - - # Register the factory method - dependency_container.register_factory( - Connection, - connection_factory.create_connection, - factory_args={"host": "localhost", "port": 5432} - ) - - -Registering and Using Instances -------------------------------- - -.. code-block:: python - - # Create an instance - my_connection = PostgresConnection(host="localhost", port=5432) - - # Register the instance - dependency_container.register_instance(Connection, my_connection) - - # Resolve the instance - resolved_connection = dependency_container.resolve(Connection) - print(resolved_connection.host) # Output: localhost - - -Registering and Resolving with Tags ------------------------------------ - -.. code-block:: python - - # Register dependencies with tags - dependency_container.register_transient(Connection, PostgresConnection, tags={"Querying", "Startable"}) - dependency_container.register_scoped(BusConnection, KafkaBusConnection, tags={"Publishing", "Startable"}) - - # Resolve dependencies by tags - startable_dependencies = dependency_container.resolve_all(tags={"Startable"}) - for dependency in startable_dependencies: - dependency.start() - - -Using Constructor Injection ---------------------------- - -.. code-block:: python - - class OrderRepository: - def __init__(self, connection: Connection): - self.connection = connection - - # Register dependencies - dependency_container.register_transient(OrderRepository) - dependency_container.register_singleton(Connection, PostgresConnection) - - # Resolve the OrderRepository with injected dependencies - repository = dependency_container.resolve(OrderRepository) - print(repository.connection.__class__.__name__) # Output: PostgresConnection - - -Using Method Injection ----------------------- - -.. code-block:: python - - class OrderController: - @staticmethod - @inject() - def place_order(order: Order, repository: OrderRepository): - order.status = "placed" - repository.save(order) - - # Register the dependency - dependency_container.register_transient(OrderRepository) - dependency_container.register_singleton(Connection, PostgresConnection) - - # Use method injection to inject the dependency - my_order = Order.create() - OrderController.place_order(order=my_order) # The repository instance will be automatically injected - -You can also specify container and scope using the decorator arguments ``container_name`` and ``scope_name``. diff --git a/setup.py b/setup.py index 1ea2320..119345d 100644 --- a/setup.py +++ b/setup.py @@ -6,10 +6,10 @@ setup( name="py-dependency-injection", - version="1.0.0-alpha.7", + version="1.0.0-alpha.8", author="David Runemalm, 2024", author_email="david.runemalm@gmail.com", - description="A dependency injection library for Python.", + description="A prototypical 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 589954b..92abb02 100644 --- a/src/dependency_injection/container.py +++ b/src/dependency_injection/container.py @@ -153,19 +153,6 @@ def _validate_constructor_args( ) -> None: constructor = inspect.signature(implementation.__init__).parameters - # Check if any required parameter is missing - missing_params = [ - param - for param in 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 '{implementation.__name__}'." - ) - for arg_name, arg_value in constructor_args.items(): if arg_name not in constructor: raise ValueError( diff --git a/tests/unit_test/container/resolve/test_resolve_with_alias.py b/tests/unit_test/container/resolve/test_resolve_with_alias.py new file mode 100644 index 0000000..b1cbbbf --- /dev/null +++ b/tests/unit_test/container/resolve/test_resolve_with_alias.py @@ -0,0 +1,22 @@ +from dependency_injection.container import DependencyContainer + +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 + + +class TestResolveWithAlias(UnitTestCase): + def test_register_with_alias_and_resolve_with_original_name( + self, + ): + # arrange + dependency_container = DependencyContainer.get_instance() + dependency_container.register_transient(VehicleAlias) + + # act + resolved_dependency = dependency_container.resolve(Vehicle) + + # assert + self.assertIsNotNone(resolved_dependency) + self.assertIsInstance(resolved_dependency, Vehicle) diff --git a/tests/unit_test/container/resolve/test_resolve_with_args.py b/tests/unit_test/container/resolve/test_resolve_with_args.py index 277d7e7..8894cb5 100644 --- a/tests/unit_test/container/resolve/test_resolve_with_args.py +++ b/tests/unit_test/container/resolve/test_resolve_with_args.py @@ -62,7 +62,7 @@ def __init__(self, color: str, make: str): ): dependency_container.resolve(dependency) - def test_resolve_with_missing_constructor_arg_raises( + def test_resolve_with_wrong_constructor_arg_type_raises( self, ): # arrange @@ -80,17 +80,18 @@ def __init__(self, color: str, make: str): dependency_container.register_transient( dependency=dependency, implementation=implementation, - constructor_args={"color": "red"}, + constructor_args={"color": "red", "make": -1}, ) # act with pytest.raises( - ValueError, - match="Missing required constructor arguments: make for class 'Car'.", + TypeError, + match="Constructor argument 'make' has an incompatible type. " + "Expected type: , provided type: .", ): dependency_container.resolve(dependency) - def test_resolve_with_wrong_constructor_arg_type_raises( + def test_resolve_when_no_constructor_arg_type_is_ok( self, ): # arrange @@ -98,7 +99,7 @@ class Vehicle: pass class Car(Vehicle): - def __init__(self, color: str, make: str): + def __init__(self, color: str, make): self.color = color self.make = make @@ -111,34 +112,41 @@ def __init__(self, color: str, make: str): constructor_args={"color": "red", "make": -1}, ) - # act - with pytest.raises( - TypeError, - match="Constructor argument 'make' has an incompatible type. " - "Expected type: , provided type: .", - ): - dependency_container.resolve(dependency) + # act + assert (no exception) + dependency_container.resolve(dependency) - def test_resolve_when_no_constructor_arg_type_is_ok( + def test_resolve_merges_registered_constructor_args_with_auto_injected_dependencies( self, ): # arrange + class Engine: + pass + class Vehicle: pass class Car(Vehicle): - def __init__(self, color: str, make): + def __init__(self, color: str, engine: Engine): self.color = color - self.make = make + self.engine = engine + + class CarFactory: + @classmethod + def create(cls, color: str, engine: Engine) -> Car: + return Car(color=color, engine=engine) dependency_container = DependencyContainer.get_instance() - dependency = Vehicle - implementation = Car + dependency_container.register_transient(Engine) dependency_container.register_transient( - dependency=dependency, - implementation=implementation, - constructor_args={"color": "red", "make": -1}, + Vehicle, + Car, + constructor_args={"color": "red"}, ) - # act + assert (no exception) - dependency_container.resolve(dependency) + # act + resolved_dependency = dependency_container.resolve(Vehicle) + + # assert + self.assertIsInstance(resolved_dependency, Car) + self.assertEqual("red", resolved_dependency.color) + self.assertIsInstance(resolved_dependency.engine, Engine) diff --git a/tests/unit_test/container/resolve/test_resolve_with_injection.py b/tests/unit_test/container/resolve/test_resolve_with_injection.py new file mode 100644 index 0000000..ba5f08e --- /dev/null +++ b/tests/unit_test/container/resolve/test_resolve_with_injection.py @@ -0,0 +1,27 @@ +from dependency_injection.container import DependencyContainer +from unit_test.unit_test_case import UnitTestCase + + +class TestResolveWithInjection(UnitTestCase): + def test_resolve_injects_dependencies_in_constructor( + self, + ): + # arrange + class Engine: + pass + + class Car: + def __init__(self, engine: Engine): + self.engine = engine + + 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.assertIsNotNone(resolved_dependency.engine) + self.assertIsInstance(resolved_dependency.engine, Engine) diff --git a/tests/unit_test/container/resolve/vehicle.py b/tests/unit_test/container/resolve/vehicle.py new file mode 100644 index 0000000..9a0bd64 --- /dev/null +++ b/tests/unit_test/container/resolve/vehicle.py @@ -0,0 +1,2 @@ +class Vehicle: + pass