diff --git a/README.md b/README.md index f3e7b7f..f1c4937 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Injection -![PyPI - Version](https://img.shields.io/pypi/v/deps-injection?label=pypi%20version) +![PyPI - Version](https://img.shields.io/pypi/v/deps-injection?label=pypi%20version&color=012111012) ![GitHub License](https://img.shields.io/github/license/nightblure/injection?color=012111012) ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/deps-injection) diff --git a/docs/containers/declarative-container.md b/docs/containers/declarative-container.md new file mode 100644 index 0000000..e8121b4 --- /dev/null +++ b/docs/containers/declarative-container.md @@ -0,0 +1,2 @@ +# Declarative container +soon... diff --git a/docs/containers/resolving.md b/docs/containers/resolving.md new file mode 100644 index 0000000..e075b44 --- /dev/null +++ b/docs/containers/resolving.md @@ -0,0 +1,2 @@ +# Resolving +soon... diff --git a/docs/index.md b/docs/index.md index 26a1124..a9dbdb3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,9 +7,16 @@ :maxdepth: 1 :caption: Introduction - introduction/concepts-and-key_features + introduction/concepts-and-features introduction/installation +.. toctree:: + :maxdepth: 1 + :caption: Containers + + containers/declarative-container + containers/resolving + .. toctree:: :maxdepth: 1 :caption: Providers @@ -17,11 +24,26 @@ providers/transient providers/callable providers/partial_callable - providers_coroutine + providers/coroutine providers/singleton providers/object providers/provided_instance +.. toctree:: + :maxdepth: 1 + :caption: Integration with web frameworks + + integration-with-web-frameworks/fastapi + integration-with-web-frameworks/flask + integration-with-web-frameworks/litestart + integration-with-web-frameworks/drf + +.. toctree:: + :maxdepth: 1 + :caption: Testing + + testing/provider-overriding + .. toctree:: :maxdepth: 1 :caption: DEV section diff --git a/docs/integration-with-web-frameworks/drf.md b/docs/integration-with-web-frameworks/drf.md new file mode 100644 index 0000000..4b89ab6 --- /dev/null +++ b/docs/integration-with-web-frameworks/drf.md @@ -0,0 +1,2 @@ +# Django REST Framework +soon... diff --git a/docs/integration-with-web-frameworks/fastapi.md b/docs/integration-with-web-frameworks/fastapi.md new file mode 100644 index 0000000..5ecdd97 --- /dev/null +++ b/docs/integration-with-web-frameworks/fastapi.md @@ -0,0 +1,2 @@ +# FastAPI +soon... diff --git a/docs/integration-with-web-frameworks/flask.md b/docs/integration-with-web-frameworks/flask.md new file mode 100644 index 0000000..696a0b1 --- /dev/null +++ b/docs/integration-with-web-frameworks/flask.md @@ -0,0 +1,2 @@ +# Flask +soon.. diff --git a/docs/integration-with-web-frameworks/litestart.md b/docs/integration-with-web-frameworks/litestart.md new file mode 100644 index 0000000..e413b59 --- /dev/null +++ b/docs/integration-with-web-frameworks/litestart.md @@ -0,0 +1,2 @@ +# Litestar +soon.. diff --git a/docs/introduction/concepts-and-key_features.md b/docs/introduction/concepts-and-features.md similarity index 56% rename from docs/introduction/concepts-and-key_features.md rename to docs/introduction/concepts-and-features.md index 2d7934b..5a6578b 100644 --- a/docs/introduction/concepts-and-key_features.md +++ b/docs/introduction/concepts-and-features.md @@ -1,9 +1,20 @@ -# Concepts and key features +# Concepts and features -With `Injection` you can implement the principle of Dependency Injection. This is needed for ... +## Concept +With `Injection` you can implement the principle of **Dependency Injection**. +**Dependency Injection** (**DI**) - providing a process to an external software component. +This is a specific form of “**inversion of control**” (**IoC**) when applied to dependency management. +In accordance with the verification principle, the responsible entity outsources the construction +of the dependencies it requires from outside, specifically designed for this general mechanism +([Wikipedia source](https://en.wikipedia.org/wiki/Dependency_injection)). Using this framework you can reduce [coupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)) and stop monkeypatching your tests. +Instead of monkeypatching you can pass mock objects or any other objects as parameters. + +--- + +## Features The public API of this framework is almost completely identical to [Dependency Injector](https://python-dependency-injector.ets-labs.org/index.html#), @@ -11,8 +22,7 @@ because the author found it successful and understandable. In addition, this will provide an easy migration to the current framework with [Dependency Injector](https://python-dependency-injector.ets-labs.org/index.html#) (see [migration from Dependency Injector](https://injection.readthedocs.io/latest/dev/migration-from-dependency-injector.html)). - -## Features and advantages +Other features and advantages: * support **Python 3.8-3.12**; * works with **FastAPI, Flask, Litestar** and **Django REST Framework**; @@ -21,8 +31,4 @@ In addition, this will provide an easy migration to the current framework with * **multiple containers**; * providers - delegate object creation and lifecycle management to providers; * **overriding** dependencies for tests without wiring; -* **100%** code coverage and very simple code; -* good [documentation](https://injection.readthedocs.io/latest/); -* intuitive and almost identical api with [dependency-injector](https://github.com/ets-labs/python-dependency-injector), -which will allow you to easily migrate to injection -(see [migration from dependency injector](https://injection.readthedocs.io/latest/dev/migration-from-dependency-injector.html)); +* **100%** code coverage and very simple code. diff --git a/docs/testing/provider-overriding.md b/docs/testing/provider-overriding.md new file mode 100644 index 0000000..f20f5c1 --- /dev/null +++ b/docs/testing/provider-overriding.md @@ -0,0 +1,169 @@ +# Provider overriding + +DI container provides, in addition to direct dependency injection, another very important functionality: +**dependencies or providers overriding**. + +Any provider registered with the container can be overridden. +This can help you replace objects with simple stubs, or with other objects. +**Override affects all providers that use the overridden provider (_see example_)**. + +## Example + +```python +from pydantic_settings import BaseSettings +from sqlalchemy import create_engine, Engine, text +from testcontainers.postgres import PostgresContainer +from injection import DeclarativeContainer, providers, Provide, inject + + +class SomeSQLADao: + def __init__(self, *, sqla_engine: Engine): + self.engine = sqla_engine + self._connection = None + + def __enter__(self): + self._connection = self.engine.connect() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self._connection.close() + + def exec_query(self, query: str): + return self._connection.execute(text(query)) + + +class Settings(BaseSettings): + db_url: str = 'some_production_db_url' + + +class DIContainer(DeclarativeContainer): + settings = providers.Singleton(Settings) + sqla_engine = providers.Singleton(create_engine, settings.provided.db_url) + some_sqla_dao = providers.Transient(SomeSQLADao, sqla_engine=sqla_engine) + + +@inject +def exec_query_example(some_sqla_dao=Provide["some_sqla_dao"]): + with some_sqla_dao: + result = some_sqla_dao.exec_query('SELECT 234') + + return next(result) + + +def main(): + pg_container = PostgresContainer(image='postgres:alpine3.19') + pg_container.start() + db_url = pg_container.get_connection_url() + + """ + We override only settings, but this override will also affect the 'sqla_engine' + and 'some_sqla_dao' providers because the 'settings' provider is used by them! + """ + local_testing_settings = Settings(db_url=db_url) + DIContainer.settings.override(local_testing_settings) + + try: + result = exec_query_example() + assert result == (234,) + finally: + DIContainer.settings.reset_override() + pg_container.stop() + + +if __name__ == '__main__': + main() + +``` + +The example above shows how overriding a nested provider ('_settings_') +affects another provider ('_engine_' and '_some_sqla_dao_'). + +## Override multiple providers + +The example above looked at overriding only one settings provider, +but the container also provides the ability to override +multiple providers at once with method ```override_providers```. + +The code above could remain the same except that +the single provider override could be replaced with the following code: + +```python +def main(): + pg_container = PostgresContainer(image='postgres:alpine3.19') + pg_container.start() + db_url = pg_container.get_connection_url() + + local_testing_settings = Settings(db_url=db_url) + providers_for_overriding = { + 'settings': local_testing_settings, + # more values... + } + with DIContainer.override_providers(providers_for_overriding): + try: + result = exec_query_example() + assert result == (234,) + finally: + pg_container.stop() +``` + +## Overriding of singleton provider +If singleton attribute is used in other singleton or resource and this other provider is initialized, +then in case of overriding of the first singleton, second one will be cached with original value. + +Same with resetting overriding. Here is an example. + +```python +from dataclasses import dataclass + +from injection import Provide, DeclarativeContainer, providers, inject + +DEFAULT_REDIS_URL = 'url_1' +MOCK_REDIS_URL = 'url_2' + + +@dataclass +class Settings: + redis_url: str = DEFAULT_REDIS_URL + + +class Redis: + def __init__(self, url: str): + self.url = url + + +class DIContainer(DeclarativeContainer): + settings = providers.Singleton(Settings) + redis = providers.Singleton(Redis, url=settings.provided.redis_url) + + +@inject +def func(redis: Redis = Provide[DIContainer.redis]): + return redis.url + + +def test_case_1(): + DIContainer.settings.override(Settings(redis_url=MOCK_REDIS_URL)) + + assert func() == MOCK_REDIS_URL + + DIContainer.settings.reset_override() + # DIContainer.redis.reset_cache() # FIX OF ASSERTION ERROR + + assert func() == DEFAULT_REDIS_URL # ASSERTION ERROR + + +def test_case_2(): + assert DIContainer.redis().url == DEFAULT_REDIS_URL + + DIContainer.settings.override(Settings(redis_url=MOCK_REDIS_URL)) + # DIContainer.redis.reset_cache() # FIX OF ASSERTION ERROR + + redis_url = func() + assert redis_url == MOCK_REDIS_URL # ASSERTION ERROR + + +if __name__ == "__main__": + test_case_1() + test_case_2() + +```