Merge branch 'release/4.40.0' into master

This commit is contained in:
Roman Mogylatov 2022-08-03 21:20:52 -04:00
commit 3858cef657
43 changed files with 19312 additions and 14984 deletions

View File

@ -1,4 +1,4 @@
Copyright (c) 2021, Roman Mogylatov Copyright (c) 2022, Roman Mogylatov
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

View File

@ -48,26 +48,26 @@ What is ``Dependency Injector``?
``Dependency Injector`` is a dependency injection framework for Python. ``Dependency Injector`` is a dependency injection framework for Python.
It helps implementing the dependency injection principle. It helps implement the dependency injection principle.
Key features of the ``Dependency Injector``: Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``, - **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency`` and ``Selector`` providers ``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers
that help assembling your objects. that help assemble your objects.
See `Providers <https://python-dependency-injector.ets-labs.org/providers/index.html>`_. See `Providers <https://python-dependency-injector.ets-labs.org/providers/index.html>`_.
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing - **Overriding**. Can override any provider by another provider on the fly. This helps in testing
and configuring dev / stage environment to replace API clients with stubs etc. See and configuring dev/stage environment to replace API clients with stubs etc. See
`Provider overriding <https://python-dependency-injector.ets-labs.org/providers/overriding.html>`_. `Provider overriding <https://python-dependency-injector.ets-labs.org/providers/overriding.html>`_.
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings, - **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings,
environment variables, and dictionaries. environment variables, and dictionaries.
See `Configuration provider <https://python-dependency-injector.ets-labs.org/providers/configuration.html>`_. See `Configuration provider <https://python-dependency-injector.ets-labs.org/providers/configuration.html>`_.
- **Containers**. Provides declarative and dynamic containers.
See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread - **Resources**. Helps with initialization and configuring of logging, event loop, thread
or process pool, etc. Can be used for per-function execution scope in tandem with wiring. or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
See `Resource provider <https://python-dependency-injector.ets-labs.org/providers/resource.html>`_. See `Resource provider <https://python-dependency-injector.ets-labs.org/providers/resource.html>`_.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with - **Containers**. Provides declarative and dynamic containers.
See `Containers <https://python-dependency-injector.ets-labs.org/containers/index.html>`_.
- **Wiring**. Injects dependencies into functions and methods. Helps integrate with
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc.
See `Wiring <https://python-dependency-injector.ets-labs.org/wiring.html>`_. See `Wiring <https://python-dependency-injector.ets-labs.org/wiring.html>`_.
- **Asynchronous**. Supports asynchronous injections. - **Asynchronous**. Supports asynchronous injections.
@ -75,7 +75,7 @@ Key features of the ``Dependency Injector``:
- **Typing**. Provides typing stubs, ``mypy``-friendly. - **Typing**. Provides typing stubs, ``mypy``-friendly.
See `Typing and mypy <https://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_. See `Typing and mypy <https://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
- **Performance**. Fast. Written in ``Cython``. - **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported. - **Maturity**. Mature and production-ready. Well-tested, documented, and supported.
.. code-block:: python .. code-block:: python
@ -100,7 +100,7 @@ Key features of the ``Dependency Injector``:
@inject @inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]) -> None:
... ...
@ -115,19 +115,18 @@ Key features of the ``Dependency Injector``:
with container.api_client.override(mock.Mock()): with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically main() # <-- overridden dependency is injected automatically
When you call ``main()`` function the ``Service`` dependency is assembled and injected automatically. When you call the ``main()`` function the ``Service`` dependency is assembled and injected automatically.
When doing a testing you call the ``container.api_client.override()`` to replace the real API When you do testing, you call the ``container.api_client.override()`` method to replace the real API
client with a mock. When you call ``main()`` the mock is injected. client with a mock. When you call ``main()``, the mock is injected.
You can override any provider with another provider. You can override any provider with another provider.
It also helps you in configuring project for the different environments: replace an API client It also helps you in a re-configuring project for different environments: replace an API client
with a stub on the dev or stage. with a stub on the dev or stage.
With the ``Dependency Injector`` objects assembling is consolidated in the container. With the ``Dependency Injector``, object assembling is consolidated in a container. Dependency injections are defined explicitly.
Dependency injections are defined explicitly. This makes it easier to understand and change how an application works.
This makes easier to understand and change how application works.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg .. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
:target: https://github.com/ets-labs/python-dependency-injector :target: https://github.com/ets-labs/python-dependency-injector
@ -185,27 +184,27 @@ The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/d
You need to specify how to assemble and where to inject the dependencies explicitly. You need to specify how to assemble and where to inject the dependencies explicitly.
The power of the framework is in a simplicity. The power of the framework is in its simplicity.
``Dependency Injector`` is a simple tool for the powerful concept. ``Dependency Injector`` is a simple tool for the powerful concept.
Frequently asked questions Frequently asked questions
-------------------------- --------------------------
What is the dependency injection? What is dependency injection?
- dependency injection is a principle that decreases coupling and increases cohesion - dependency injection is a principle that decreases coupling and increases cohesion
Why should I do the dependency injection? Why should I do the dependency injection?
- your code becomes more flexible, testable, and clear 😎 - your code becomes more flexible, testable, and clear 😎
How do I start doing the dependency injection? How do I start applying the dependency injection?
- you start writing the code following the dependency injection principle - you start writing the code following the dependency injection principle
- you register all of your application components and their dependencies in the container - you register all of your application components and their dependencies in the container
- when you need a component, you specify where to inject it or get it from the container - when you need a component, you specify where to inject it or get it from the container
What price do I pay and what do I get? What price do I pay and what do I get?
- you need to explicitly specify the dependencies - you need to explicitly specify the dependencies
- it will be an extra work in the beginning - it will be extra work in the beginning
- it will payoff as the project grows - it will payoff as project grows
Have a question? Have a question?
- Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_ - Open a `Github Issue <https://github.com/ets-labs/python-dependency-injector/issues>`_

View File

@ -52,7 +52,7 @@ master_doc = "index"
# General information about the project. # General information about the project.
project = "Dependency Injector" project = "Dependency Injector"
copyright = "2021, Roman Mogylatov" copyright = "2022, Roman Mogylatov"
author = "Roman Mogylatov" author = "Roman Mogylatov"
# The version info for the project you"re documenting, acts as replacement for # The version info for the project you"re documenting, acts as replacement for

View File

@ -65,23 +65,23 @@ It helps implementing the dependency injection principle.
Key features of the ``Dependency Injector``: Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``, - **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency`` and ``Selector`` providers ``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers
that help assembling your objects. See :ref:`providers`. that help assemble your objects. See :ref:`providers`.
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing - **Overriding**. Can override any provider by another provider on the fly. This helps in testing
and configuring dev / stage environment to replace API clients with stubs etc. See and configuring dev/stage environment to replace API clients with stubs etc. See
:ref:`provider-overriding`. :ref:`provider-overriding`.
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings, - **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings,
environment variables, and dictionaries. See :ref:`configuration-provider`. environment variables, and dictionaries. See :ref:`configuration-provider`.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread - **Resources**. Helps with initialization and configuring of logging, event loop, thread
or process pool, etc. Can be used for per-function execution scope in tandem with wiring. or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
See :ref:`resource-provider`. See :ref:`resource-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with - **Wiring**. Injects dependencies into functions and methods. Helps integrate with
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`. other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
- **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`. - **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`. - **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``. - **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported. - **Maturity**. Mature and production-ready. Well-tested, documented, and supported.
.. code-block:: python .. code-block:: python
@ -106,7 +106,7 @@ Key features of the ``Dependency Injector``:
@inject @inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]) -> None:
... ...
@ -121,9 +121,9 @@ Key features of the ``Dependency Injector``:
with container.api_client.override(mock.Mock()): with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically main() # <-- overridden dependency is injected automatically
With the ``Dependency Injector`` objects assembling is consolidated in the container. With the ``Dependency Injector``, object assembling is consolidated in the container.
Dependency injections are defined explicitly. Dependency injections are defined explicitly.
This makes easier to understand and change how application works. This makes it easier to understand and change how the application works.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg .. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
:target: https://github.com/ets-labs/python-dependency-injector :target: https://github.com/ets-labs/python-dependency-injector

View File

@ -11,24 +11,24 @@ Dependency injection and inversion of control in Python
feature for testing or configuring project in different environments and explains feature for testing or configuring project in different environments and explains
why it's better than monkey-patching. why it's better than monkey-patching.
Originally dependency injection pattern got popular in the languages with a static typing, Originally dependency injection pattern got popular in languages with static typing like Java.
like Java. Dependency injection is a principle that helps to achieve an inversion of control. Dependency injection is a principle that helps to achieve an inversion of control. A
Dependency injection framework can significantly improve a flexibility of the language dependency injection framework can significantly improve the flexibility of a language
with a static typing. Implementation of a dependency injection framework for a language with static typing. Implementation of a dependency injection framework for a language
with a static typing is not something that one can do quickly. It will be a quite complex thing with static typing is not something that one can do quickly. It will be a quite complex thing
to be done well. And will take time. to be done well. And will take time.
Python is an interpreted language with a dynamic typing. There is an opinion that dependency Python is an interpreted language with dynamic typing. There is an opinion that dependency
injection doesn't work for it as well as it does for Java. A lot of the flexibility is already injection doesn't work for it as well as it does for Java. A lot of the flexibility is already
built in. Also there is an opinion that a dependency injection framework is something that built-in. Also, there is an opinion that a dependency injection framework is something that
Python developer rarely needs. Python developers say that dependency injection can be implemented Python developer rarely needs. Python developers say that dependency injection can be implemented
easily using language fundamentals. easily using language fundamentals.
This page describes the advantages of the dependency injection usage in Python. It This page describes the advantages of applying dependency injection in Python. It
contains Python examples that show how to implement dependency injection. It demonstrates a usage contains Python examples that show how to implement dependency injection. It demonstrates the usage
of the dependency injection framework ``Dependency Injector``, its container, ``Factory``, of the ``Dependency Injector`` framework, its container, ``Factory``, ``Singleton``,
``Singleton`` and ``Configuration`` providers. The example shows how to use ``Dependency Injector`` and ``Configuration`` providers. The example shows how to use providers' overriding feature
providers overriding feature for testing or configuring project in different environments and of ``Dependency Injector`` for testing or re-configuring a project in different environments and
explains why it's better than monkey-patching. explains why it's better than monkey-patching.
What is dependency injection? What is dependency injection?
@ -44,14 +44,14 @@ What is coupling and cohesion?
Coupling and cohesion are about how tough the components are tied. Coupling and cohesion are about how tough the components are tied.
- **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way - **High coupling**. If the coupling is high it's like using superglue or welding. No easy way
to disassemble. to disassemble.
- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and - **High cohesion**. High cohesion is like using screws. Quite easy to disassemble and
assemble back or assemble a different way. It is an opposite to high coupling. re-assemble in a different way. It is an opposite to high coupling.
When the cohesion is high the coupling is low. Cohesion often correlates with coupling. Higher cohesion usually leads to lower coupling and vice versa.
Low coupling brings a flexibility. Your code becomes easier to change and test. Low coupling brings flexibility. Your code becomes easier to change and test.
How to implement the dependency injection? How to implement the dependency injection?
@ -66,14 +66,14 @@ Before:
class ApiClient: class ApiClient:
def __init__(self): def __init__(self) -> None:
self.api_key = os.getenv("API_KEY") # <-- dependency self.api_key = os.getenv("API_KEY") # <-- dependency
self.timeout = os.getenv("TIMEOUT") # <-- dependency self.timeout = int(os.getenv("TIMEOUT")) # <-- dependency
class Service: class Service:
def __init__(self): def __init__(self) -> None:
self.api_client = ApiClient() # <-- dependency self.api_client = ApiClient() # <-- dependency
@ -94,18 +94,18 @@ After:
class ApiClient: class ApiClient:
def __init__(self, api_key: str, timeout: int): def __init__(self, api_key: str, timeout: int) -> None:
self.api_key = api_key # <-- dependency is injected self.api_key = api_key # <-- dependency is injected
self.timeout = timeout # <-- dependency is injected self.timeout = timeout # <-- dependency is injected
class Service: class Service:
def __init__(self, api_client: ApiClient): def __init__(self, api_client: ApiClient) -> None:
self.api_client = api_client # <-- dependency is injected self.api_client = api_client # <-- dependency is injected
def main(service: Service): # <-- dependency is injected def main(service: Service) -> None: # <-- dependency is injected
... ...
@ -114,7 +114,7 @@ After:
service=Service( service=Service(
api_client=ApiClient( api_client=ApiClient(
api_key=os.getenv("API_KEY"), api_key=os.getenv("API_KEY"),
timeout=os.getenv("TIMEOUT"), timeout=int(os.getenv("TIMEOUT")),
), ),
), ),
) )
@ -137,7 +137,7 @@ Now you need to assemble and inject the objects like this:
service=Service( service=Service(
api_client=ApiClient( api_client=ApiClient(
api_key=os.getenv("API_KEY"), api_key=os.getenv("API_KEY"),
timeout=os.getenv("TIMEOUT"), timeout=int(os.getenv("TIMEOUT")),
), ),
), ),
) )
@ -149,14 +149,14 @@ Here comes the ``Dependency Injector``.
What does the Dependency Injector do? What does the Dependency Injector do?
------------------------------------- -------------------------------------
With the dependency injection pattern objects loose the responsibility of assembling With the dependency injection pattern, objects lose the responsibility of assembling
the dependencies. The ``Dependency Injector`` absorbs that responsibility. the dependencies. The ``Dependency Injector`` absorbs that responsibility.
``Dependency Injector`` helps to assemble and inject the dependencies. ``Dependency Injector`` helps to assemble and inject the dependencies.
It provides a container and providers that help you with the objects assembly. It provides a container and providers that help you with the objects assembly.
When you need an object you place a ``Provide`` marker as a default value of a When you need an object you place a ``Provide`` marker as a default value of a
function argument. When you call this function framework assembles and injects function argument. When you call this function, framework assembles and injects
the dependency. the dependency.
.. code-block:: python .. code-block:: python
@ -182,7 +182,7 @@ the dependency.
@inject @inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]) -> None:
... ...
@ -197,79 +197,79 @@ the dependency.
with container.api_client.override(mock.Mock()): with container.api_client.override(mock.Mock()):
main() # <-- overridden dependency is injected automatically main() # <-- overridden dependency is injected automatically
When you call ``main()`` function the ``Service`` dependency is assembled and injected automatically. When you call the ``main()`` function the ``Service`` dependency is assembled and injected automatically.
When doing a testing you call the ``container.api_client.override()`` to replace the real API When you do testing, you call the ``container.api_client.override()`` method to replace the real API
client with a mock. When you call ``main()`` the mock is injected. client with a mock. When you call ``main()``, the mock is injected.
You can override any provider with another provider. You can override any provider with another provider.
It also helps you in configuring project for the different environments: replace an API client It also helps you in a re-configuring project for different environments: replace an API client
with a stub on the dev or stage. with a stub on the dev or stage.
Objects assembling is consolidated in the container. Dependency injections are defined explicitly. Objects assembling is consolidated in a container. Dependency injections are defined explicitly.
This makes easier to understand and change how application works. This makes it easier to understand and change how an application works.
Testing, Monkey-patching and dependency injection Testing, Monkey-patching and dependency injection
------------------------------------------------- -------------------------------------------------
The testability benefit is opposed to a monkey-patching. The testability benefit is opposed to monkey-patching.
In Python you can monkey-patch In Python, you can monkey-patch anything, anytime. The problem with monkey-patching is
anything, anytime. The problem with a monkey-patching is that it's too fragile. The reason is that that it's too fragile. The cause of it is that when you monkey-patch you do something that
when you monkey-patch you do something that wasn't intended to be done. You monkey-patch the wasn't intended to be done. You monkey-patch the implementation details. When implementation
implementation details. When implementation changes the monkey-patching is broken. changes the monkey-patching is broken.
With a dependency injection you patch the interface, not an implementation. This is a way more With dependency injection, you patch the interface, not an implementation. This is a way more
stable approach. stable approach.
Also monkey-patching is a way too dirty to be used outside of the testing code for Also, monkey-patching is way too dirty to be used outside of the testing code for
reconfiguring the project for the different environments. re-configuring the project for the different environments.
Conclusion Conclusion
---------- ----------
Dependency injection brings you 3 advantages: Dependency injection provides you with three advantages:
- **Flexibility**. The components are loosely coupled. You can easily extend or change a - **Flexibility**. The components are loosely coupled. You can easily extend or change the
functionality of the system by combining the components different way. You even can do it on functionality of a system by combining the components in a different way. You even can do it on
the fly. the fly.
- **Testability**. Testing is easy because you can easily inject mocks instead of real objects - **Testability**. Testing is easier because you can easily inject mocks instead of real objects
that use API or database, etc. that use API or database, etc.
- **Clearness and maintainability**. Dependency injection helps you reveal the dependencies. - **Clearness and maintainability**. Dependency injection helps you reveal the dependencies.
Implicit becomes explicit. And "Explicit is better than implicit" (PEP 20 - The Zen of Python). Implicit becomes explicit. And "Explicit is better than implicit" (PEP 20 - The Zen of Python).
You have all the components and dependencies defined explicitly in the container. This You have all the components and dependencies defined explicitly in a container. This
provides an overview and control on the application structure. It is easy to understand and provides an overview and control of the application structure. It is easier to understand and
change it. change it.
Is it worth to use a dependency injection in Python? Is it worth applying dependency injection in Python?
It depends on what you build. The advantages above are not too important if you use Python as a It depends on what you build. The advantages above are not too important if you use Python as a
scripting language. The picture is different when you use Python to create an application. The scripting language. The picture is different when you use Python to create an application. The
larger the application the more significant is the benefit. larger the application the more significant the benefits.
Is it worth to use a framework for the dependency injection? Is it worth using a framework for applying dependency injection?
The complexity of the dependency injection pattern implementation in Python is The complexity of the dependency injection pattern implementation in Python is
lower than in the other languages but it's still in place. It doesn't mean you have to use a lower than in other languages but it's still in place. It doesn't mean you have to use a
framework but using a framework is beneficial because the framework is: framework but using a framework is beneficial because the framework is:
- Already implemented - Already implemented
- Tested on all platforms and versions of Python - Tested on all platforms and versions of Python
- Documented - Documented
- Supported - Supported
- Known to the other engineers - Other engineers are familiar with it
Few advices at last: An advice at last:
- **Give it a try**. Dependency injection is counter-intuitive. Our nature is that - **Give it a try**. Dependency injection is counter-intuitive. Our nature is that
when we need something the first thought that comes to our mind is to go and get it. Dependency when we need something the first thought that comes to our mind is to go and get it. Dependency
injection is just like "Wait, I need to state a need instead of getting something right now". injection is just like "Wait, I need to state a need instead of getting something right away".
It's like a little investment that will pay-off later. The advice is to just give it a try for It's like a little investment that will pay-off later. The advice is to just give it a try for
two weeks. This time will be enough for getting your own impression. If you don't like it you two weeks. This time will be enough for getting your own impression. If you don't like it you
won't lose too much. won't lose too much.
- **Common sense first**. Use a common sense when apply dependency injection. It is a good - **Common sense first**. Use common sense when applying dependency injection. It is a good
principle, but not a silver bullet. If you do it too much you will reveal too much of the principle, but not a silver bullet. If you do it too much you will reveal too many of the
implementation details. Experience comes with practice and time. implementation details. Experience comes with practice and time.
What's next? What's next?
@ -303,8 +303,7 @@ Choose one of the following as a next step:
Useful links Useful links
------------ ------------
There are some useful links related to dependency injection design pattern A few useful links related to a dependency injection design pattern for further reading:
that could be used for further reading:
+ https://en.wikipedia.org/wiki/Dependency_injection + https://en.wikipedia.org/wiki/Dependency_injection
+ https://martinfowler.com/articles/injection.html + https://martinfowler.com/articles/injection.html

View File

@ -7,8 +7,8 @@ Introduction
overview of the dependency injection, inversion of overview of the dependency injection, inversion of
control and Dependency Injector framework. control and Dependency Injector framework.
Current section of the documentation provides an overview of the The current section of the documentation provides an overview of the
dependency injection, inversion of control and the ``Dependency Injector`` framework. dependency injection, inversion of control, and the ``Dependency Injector`` framework.
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2

View File

@ -2,7 +2,7 @@ Installation
============ ============
``Dependency Injector`` is available on `PyPI <https://pypi.org/project/dependency-injector/>`_. ``Dependency Injector`` is available on `PyPI <https://pypi.org/project/dependency-injector/>`_.
To install latest version you can use ``pip``: To install the latest version you can use ``pip``:
.. code-block:: bash .. code-block:: bash
@ -10,7 +10,7 @@ To install latest version you can use ``pip``:
Some modules of the ``Dependency Injector`` are implemented as C extensions. Some modules of the ``Dependency Injector`` are implemented as C extensions.
``Dependency Injector`` is distributed as a pre-compiled wheels. Wheels are ``Dependency Injector`` is distributed as a pre-compiled wheels. Wheels are
available for all supported Python versions on Linux, Windows and MacOS. available for all supported Python versions on Linux, Windows, and MacOS.
Linux distribution uses `manylinux <https://github.com/pypa/manylinux>`_. Linux distribution uses `manylinux <https://github.com/pypa/manylinux>`_.
If there is no appropriate wheel for your environment (Python version and OS) If there is no appropriate wheel for your environment (Python version and OS)
@ -23,20 +23,20 @@ To verify the installed version:
>>> import dependency_injector >>> import dependency_injector
>>> dependency_injector.__version__ >>> dependency_injector.__version__
'4.37.0' '4.39.0'
.. note:: .. note::
When add ``Dependency Injector`` to the ``requirements.txt`` don't forget to pin version When adding ``Dependency Injector`` to ``pyproject.toml`` or ``requirements.txt``
to the current major: don't forget to pin the version to the current major:
.. code-block:: bash .. code-block:: bash
dependency-injector>=4.0,<5.0 dependency-injector>=4.0,<5.0
*Next major version can be incompatible.* *The next major version can be incompatible.*
All releases are available on `PyPI release history page <https://pypi.org/project/dependency-injector/#history>`_. All releases are available on the `PyPI release history page <https://pypi.org/project/dependency-injector/#history>`_.
Each release has appropriate tag. The tags are available on Each release has an appropriate tag. The tags are available on the
`GitHub releases page <https://github.com/ets-labs/python-dependency-injector/releases>`_. `GitHub releases page <https://github.com/ets-labs/python-dependency-injector/releases>`_.
.. disqus:: .. disqus::

View File

@ -11,23 +11,23 @@ Key features
Key features of the ``Dependency Injector``: Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``, - **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency`` and ``Selector`` providers ``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers
that help assembling your objects. See :ref:`providers`. that help assemble your objects. See :ref:`providers`.
- **Overriding**. Can override any provider by another provider on the fly. This helps in testing - **Overriding**. Can override any provider by another provider on the fly. This helps in testing
and configuring dev / stage environment to replace API clients with stubs etc. See and configuring dev/stage environment to replace API clients with stubs etc. See
:ref:`provider-overriding`. :ref:`provider-overriding`.
- **Configuration**. Reads configuration from ``yaml`` & ``ini`` files, ``pydantic`` settings, - **Configuration**. Reads configuration from ``yaml``, ``ini``, and ``json`` files, ``pydantic`` settings,
environment variables, and dictionaries. See :ref:`configuration-provider`. environment variables, and dictionaries. See :ref:`configuration-provider`.
- **Resources**. Helps with initialization and configuring of logging, event loop, thread - **Resources**. Helps with initialization and configuring of logging, event loop, thread
or process pool, etc. Can be used for per-function execution scope in tandem with wiring. or process pool, etc. Can be used for per-function execution scope in tandem with wiring.
See :ref:`resource-provider`. See :ref:`resource-provider`.
- **Containers**. Provides declarative and dynamic containers. See :ref:`containers`. - **Containers**. Provides declarative and dynamic containers. See :ref:`containers`.
- **Wiring**. Injects dependencies into functions and methods. Helps integrating with - **Wiring**. Injects dependencies into functions and methods. Helps integrate with
other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`. other frameworks: Django, Flask, Aiohttp, Sanic, FastAPI, etc. See :ref:`wiring`.
- **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`. - **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`. - **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **Performance**. Fast. Written in ``Cython``. - **Performance**. Fast. Written in ``Cython``.
- **Maturity**. Mature and production-ready. Well-tested, documented and supported. - **Maturity**. Mature and production-ready. Well-tested, documented, and supported.
The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle: The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/dev/peps/pep-0020/>`_ principle:
@ -37,7 +37,7 @@ The framework stands on the `PEP20 (The Zen of Python) <https://www.python.org/d
You need to specify how to assemble and where to inject the dependencies explicitly. You need to specify how to assemble and where to inject the dependencies explicitly.
The power of the framework is in a simplicity. The power of the framework is in its simplicity.
``Dependency Injector`` is a simple tool for the powerful concept. ``Dependency Injector`` is a simple tool for the powerful concept.
.. disqus:: .. disqus::

View File

@ -7,6 +7,25 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
4.40.0
------
- Add ``Configuration.from_json()`` method to load configuration from a json file.
- Fix bug with wiring not working properly with functions double wrapped by ``@functools.wraps`` decorator.
See issue: `#454 <https://github.com/ets-labs/python-dependency-injector/issues/454>`_.
Many thanks to: `@platipo <https://github.com/platipo>`_, `@MatthieuMoreau0 <https://github.com/MatthieuMoreau0>`_,
`@fabiocerqueira <https://github.com/fabiocerqueira>`_, `@Jitesh-Khuttan <https://github.com/Jitesh-Khuttan>`_.
- Refactor wiring module to store all patched callable data in the ``PatchedRegistry``.
- Improve wording on the "Dependency injection and inversion of control in Python" docs page.
- Add documentation on the ``@inject`` decorator.
- Update typing in the main example and cohesion/coupling correlation definition in
"Dependency injection and inversion of control in Python".
Thanks to `@illia-v (Illia Volochii) <https://github.com/illia-v>`_ for the
PR (`#580 <https://github.com/ets-labs/python-dependency-injector/pull/580>`_).
- Update copyright year.
- Enable skipped test ``test_schema_with_boto3_session()``.
- Update pytest configuration.
- Regenerate C sources using Cython 0.29.30.
4.39.1 4.39.1
------ ------
- Fix bug `#574 <https://github.com/ets-labs/python-dependency-injector/issues/574>`_: - Fix bug `#574 <https://github.com/ets-labs/python-dependency-injector/issues/574>`_:

View File

@ -136,6 +136,50 @@ To use another loader use ``loader`` argument:
*Don't forget to mirror the changes in the requirements file.* *Don't forget to mirror the changes in the requirements file.*
Loading from a JSON file
------------------------
``Configuration`` provider can load configuration from a ``json`` file using the
:py:meth:`Configuration.from_json` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_json.py
:language: python
:lines: 3-
:emphasize-lines: 12
where ``examples/providers/configuration/config.json`` is:
.. literalinclude:: ../../examples/providers/configuration/config.json
:language: json
Alternatively, you can provide a path to a json file over the configuration provider argument. In that case,
the container will call ``config.from_json()`` automatically:
.. code-block:: python
:emphasize-lines: 3
class Container(containers.DeclarativeContainer):
config = providers.Configuration(json_files=["./config.json"])
if __name__ == "__main__":
container = Container() # Config is loaded from ./config.json
:py:meth:`Configuration.from_json` method supports environment variables interpolation.
.. code-block:: json
{
"section": {
"option1": "${ENV_VAR}",
"option2": "${ENV_VAR}/path",
"option3": "${ENV_VAR:default}"
}
}
See also: :ref:`configuration-envs-interpolation`.
Loading from a Pydantic settings Loading from a Pydantic settings
-------------------------------- --------------------------------

View File

@ -22,6 +22,82 @@ To use wiring you need:
:local: :local:
:backlinks: none :backlinks: none
Decorator @inject
-----------------
Decorator ``@inject`` injects the dependencies. Use it to decorate all functions and methods
with the injections.
.. code-block:: python
from dependency_injector.wiring import inject, Provide
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
Decorator ``@inject`` must be specified as a very first decorator of a function to ensure that
the wiring works appropriately. This will also contribute to the performance of the wiring process.
.. code-block:: python
from dependency_injector.wiring import inject, Provide
@decorator_etc
@decorator_2
@decorator_1
@inject
def foo(bar: Bar = Provide[Container.bar]):
...
Specifying the ``@inject`` as a first decorator is also crucial for FastAPI, other frameworks
using decorators similarly, for closures, and for any types of custom decorators with the injections.
FastAPI example:
.. code-block:: python
app = FastAPI()
@app.api_route("/")
@inject
async def index(service: Service = Depends(Provide[Container.service])):
value = await service.process()
return {"result": value}
Decorators example:
.. code-block:: python
def decorator1(func):
@functools.wraps(func)
@inject
def wrapper(value1: int = Provide[Container.config.value1]):
result = func()
return result + value1
return wrapper
def decorator2(func):
@functools.wraps(func)
@inject
def wrapper(value2: int = Provide[Container.config.value2]):
result = func()
return result + value2
return wrapper
@decorator1
@decorator2
def sample():
...
.. seealso::
`Issue #404 <https://github.com/ets-labs/python-dependency-injector/issues/404#issuecomment-785216978>`_
explains ``@inject`` decorator in a few more details.
Markers Markers
------- -------

View File

@ -3,18 +3,18 @@ import os
class ApiClient: class ApiClient:
def __init__(self, api_key: str, timeout: int): def __init__(self, api_key: str, timeout: int) -> None:
self.api_key = api_key # <-- dependency is injected self.api_key = api_key # <-- dependency is injected
self.timeout = timeout # <-- dependency is injected self.timeout = timeout # <-- dependency is injected
class Service: class Service:
def __init__(self, api_client: ApiClient): def __init__(self, api_client: ApiClient) -> None:
self.api_client = api_client # <-- dependency is injected self.api_client = api_client # <-- dependency is injected
def main(service: Service): # <-- dependency is injected def main(service: Service) -> None: # <-- dependency is injected
... ...
@ -23,7 +23,7 @@ if __name__ == "__main__":
service=Service( service=Service(
api_client=ApiClient( api_client=ApiClient(
api_key=os.getenv("API_KEY"), api_key=os.getenv("API_KEY"),
timeout=os.getenv("TIMEOUT"), timeout=int(os.getenv("TIMEOUT")),
), ),
), ),
) )

View File

@ -23,7 +23,7 @@ class Container(containers.DeclarativeContainer):
@inject @inject
def main(service: Service = Provide[Container.service]): def main(service: Service = Provide[Container.service]) -> None:
... ...

View File

@ -24,31 +24,31 @@ The output should be something like:
.. code-block:: .. code-block::
redis_1 | 1:C 04 Jan 2021 02:42:14.115 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo redis_1 | 1:C 04 Jan 2022 02:42:14.115 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1 | 1:C 04 Jan 2021 02:42:14.115 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started redis_1 | 1:C 04 Jan 2022 02:42:14.115 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1 | 1:C 04 Jan 2021 02:42:14.115 # Configuration loaded redis_1 | 1:C 04 Jan 2022 02:42:14.115 # Configuration loaded
redis_1 | 1:M 04 Jan 2021 02:42:14.116 * Running mode=standalone, port=6379. redis_1 | 1:M 04 Jan 2022 02:42:14.116 * Running mode=standalone, port=6379.
redis_1 | 1:M 04 Jan 2021 02:42:14.116 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. redis_1 | 1:M 04 Jan 2022 02:42:14.116 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1 | 1:M 04 Jan 2021 02:42:14.116 # Server initialized redis_1 | 1:M 04 Jan 2022 02:42:14.116 # Server initialized
redis_1 | 1:M 04 Jan 2021 02:42:14.117 * Loading RDB produced by version 6.0.9 redis_1 | 1:M 04 Jan 2022 02:42:14.117 * Loading RDB produced by version 6.0.9
redis_1 | 1:M 04 Jan 2021 02:42:14.117 * RDB age 1 seconds redis_1 | 1:M 04 Jan 2022 02:42:14.117 * RDB age 1 seconds
redis_1 | 1:M 04 Jan 2021 02:42:14.117 * RDB memory usage when created 0.77 Mb redis_1 | 1:M 04 Jan 2022 02:42:14.117 * RDB memory usage when created 0.77 Mb
redis_1 | 1:M 04 Jan 2021 02:42:14.117 * DB loaded from disk: 0.000 seconds redis_1 | 1:M 04 Jan 2022 02:42:14.117 * DB loaded from disk: 0.000 seconds
redis_1 | 1:M 04 Jan 2021 02:42:14.117 * Ready to accept connections redis_1 | 1:M 04 Jan 2022 02:42:14.117 * Ready to accept connections
redis_1 | 1:signal-handler (1609728137) Received SIGTERM scheduling shutdown... redis_1 | 1:signal-handler (1609728137) Received SIGTERM scheduling shutdown...
redis_1 | 1:M 04 Jan 2021 02:42:17.984 # User requested shutdown... redis_1 | 1:M 04 Jan 2022 02:42:17.984 # User requested shutdown...
redis_1 | 1:M 04 Jan 2021 02:42:17.984 # Redis is now ready to exit, bye bye... redis_1 | 1:M 04 Jan 2022 02:42:17.984 # Redis is now ready to exit, bye bye...
redis_1 | 1:C 04 Jan 2021 02:42:22.035 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo redis_1 | 1:C 04 Jan 2022 02:42:22.035 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis_1 | 1:C 04 Jan 2021 02:42:22.035 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started redis_1 | 1:C 04 Jan 2022 02:42:22.035 # Redis version=6.0.9, bits=64, commit=00000000, modified=0, pid=1, just started
redis_1 | 1:C 04 Jan 2021 02:42:22.035 # Configuration loaded redis_1 | 1:C 04 Jan 2022 02:42:22.035 # Configuration loaded
redis_1 | 1:M 04 Jan 2021 02:42:22.037 * Running mode=standalone, port=6379. redis_1 | 1:M 04 Jan 2022 02:42:22.037 * Running mode=standalone, port=6379.
redis_1 | 1:M 04 Jan 2021 02:42:22.037 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. redis_1 | 1:M 04 Jan 2022 02:42:22.037 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
redis_1 | 1:M 04 Jan 2021 02:42:22.037 # Server initialized redis_1 | 1:M 04 Jan 2022 02:42:22.037 # Server initialized
redis_1 | 1:M 04 Jan 2021 02:42:22.037 * Loading RDB produced by version 6.0.9 redis_1 | 1:M 04 Jan 2022 02:42:22.037 * Loading RDB produced by version 6.0.9
redis_1 | 1:M 04 Jan 2021 02:42:22.037 * RDB age 9 seconds redis_1 | 1:M 04 Jan 2022 02:42:22.037 * RDB age 9 seconds
redis_1 | 1:M 04 Jan 2021 02:42:22.037 * RDB memory usage when created 0.77 Mb redis_1 | 1:M 04 Jan 2022 02:42:22.037 * RDB memory usage when created 0.77 Mb
redis_1 | 1:M 04 Jan 2021 02:42:22.037 * DB loaded from disk: 0.000 seconds redis_1 | 1:M 04 Jan 2022 02:42:22.037 * DB loaded from disk: 0.000 seconds
redis_1 | 1:M 04 Jan 2021 02:42:22.037 * Ready to accept connections redis_1 | 1:M 04 Jan 2022 02:42:22.037 * Ready to accept connections
example_1 | INFO: Started server process [1] example_1 | INFO: Started server process [1]
example_1 | INFO: Waiting for application startup. example_1 | INFO: Waiting for application startup.
example_1 | INFO: Application startup complete. example_1 | INFO: Application startup complete.

View File

@ -29,15 +29,15 @@ The output should be something like:
Starting fastapi-sqlalchemy_webapp_1 ... done Starting fastapi-sqlalchemy_webapp_1 ... done
Attaching to fastapi-sqlalchemy_webapp_1 Attaching to fastapi-sqlalchemy_webapp_1
webapp_1 | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1 webapp_1 | 2022-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
webapp_1 | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine () webapp_1 | 2022-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine ()
webapp_1 | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1 webapp_1 | 2022-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
webapp_1 | 2021-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine () webapp_1 | 2022-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine ()
webapp_1 | 2021-02-04 22:07:19,805 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("users") webapp_1 | 2022-02-04 22:07:19,805 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("users")
webapp_1 | 2021-02-04 22:07:19,805 INFO sqlalchemy.engine.base.Engine () webapp_1 | 2022-02-04 22:07:19,805 INFO sqlalchemy.engine.base.Engine ()
webapp_1 | 2021-02-04 22:07:19,808 INFO sqlalchemy.engine.base.Engine PRAGMA temp.table_info("users") webapp_1 | 2022-02-04 22:07:19,808 INFO sqlalchemy.engine.base.Engine PRAGMA temp.table_info("users")
webapp_1 | 2021-02-04 22:07:19,808 INFO sqlalchemy.engine.base.Engine () webapp_1 | 2022-02-04 22:07:19,808 INFO sqlalchemy.engine.base.Engine ()
webapp_1 | 2021-02-04 22:07:19,809 INFO sqlalchemy.engine.base.Engine webapp_1 | 2022-02-04 22:07:19,809 INFO sqlalchemy.engine.base.Engine
webapp_1 | CREATE TABLE users ( webapp_1 | CREATE TABLE users (
webapp_1 | id INTEGER NOT NULL, webapp_1 | id INTEGER NOT NULL,
webapp_1 | email VARCHAR, webapp_1 | email VARCHAR,
@ -49,8 +49,8 @@ The output should be something like:
webapp_1 | ) webapp_1 | )
webapp_1 | webapp_1 |
webapp_1 | webapp_1 |
webapp_1 | 2021-02-04 22:07:19,810 INFO sqlalchemy.engine.base.Engine () webapp_1 | 2022-02-04 22:07:19,810 INFO sqlalchemy.engine.base.Engine ()
webapp_1 | 2021-02-04 22:07:19,821 INFO sqlalchemy.engine.base.Engine COMMIT webapp_1 | 2022-02-04 22:07:19,821 INFO sqlalchemy.engine.base.Engine COMMIT
webapp_1 | INFO: Started server process [8] webapp_1 | INFO: Started server process [8]
webapp_1 | INFO: Waiting for application startup. webapp_1 | INFO: Waiting for application startup.
webapp_1 | INFO: Application startup complete. webapp_1 | INFO: Application startup complete.

View File

@ -0,0 +1,6 @@
{
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET"
}
}

View File

@ -0,0 +1,27 @@
"""`Configuration` provider values loading example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
if __name__ == "__main__":
container = Container()
container.config.from_json("./config.json")
assert container.config() == {
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
}
assert container.config.aws() == {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
}
assert container.config.aws.access_key_id() == "KEY"
assert container.config.aws.secret_access_key() == "SECRET"

View File

@ -0,0 +1,25 @@
"""`Configuration` provider values loading example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration(json_files=["./config.json"])
if __name__ == "__main__":
container = Container()
assert container.config() == {
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
}
assert container.config.aws() == {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
}
assert container.config.aws.access_key_id() == "KEY"
assert container.config.aws.secret_access_key() == "SECRET"

View File

@ -13,12 +13,12 @@ class Container(containers.DeclarativeContainer):
if __name__ == "__main__": if __name__ == "__main__":
container = Container() container = Container()
container.config.option1.from_value(date(2021, 6, 13)) container.config.option1.from_value(date(2022, 3, 13))
container.config.option2.from_value(date(2021, 6, 14)) container.config.option2.from_value(date(2022, 3, 14))
assert container.config() == { assert container.config() == {
"option1": date(2021, 6, 13), "option1": date(2022, 3, 13),
"option2": date(2021, 6, 14), "option2": date(2022, 3, 14),
} }
assert container.config.option1() == date(2021, 6, 13) assert container.config.option1() == date(2022, 3, 13)
assert container.config.option2() == date(2021, 6, 14) assert container.config.option2() == date(2022, 3, 14)

View File

@ -1,4 +1,4 @@
cython==0.29.24 cython==0.29.30
pytest pytest
pytest-asyncio pytest-asyncio
tox tox

View File

@ -1,5 +1,9 @@
# TODO: unpin 3.5.0 when this bug is fixed: https://github.com/sphinx-doc/sphinx/issues/8885 # TODO: unpin 3.5.0 when this bug is fixed: https://github.com/sphinx-doc/sphinx/issues/8885
sphinx<3.5.0 sphinx<3.5.0
# TODO: unpin jinja2 after sphinx update to 4+
jinja2<3.1
-e git+https://github.com/rmk135/sphinxcontrib-disqus.git#egg=sphinxcontrib-disqus -e git+https://github.com/rmk135/sphinxcontrib-disqus.git#egg=sphinxcontrib-disqus
-r requirements-ext.txt -r requirements-ext.txt

View File

@ -1,6 +1,6 @@
"""Top-level package.""" """Top-level package."""
__version__ = "4.39.1" __version__ = "4.40.0"
"""Version number. """Version number.
:type: str :type: str

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,12 @@ import inspect
import types import types
from . import providers from . import providers
from .wiring import _Marker from .wiring import _Marker, PatchedCallable
from .providers cimport Provider from .providers cimport Provider
def _get_sync_patched(fn): def _get_sync_patched(fn, patched: PatchedCallable):
@functools.wraps(fn) @functools.wraps(fn)
def _patched(*args, **kwargs): def _patched(*args, **kwargs):
cdef object result cdef object result
@ -21,14 +21,14 @@ def _get_sync_patched(fn):
cdef Provider provider cdef Provider provider
to_inject = kwargs.copy() to_inject = kwargs.copy()
for arg_key, provider in _patched.__injections__.items(): for arg_key, provider in patched.injections.items():
if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker):
to_inject[arg_key] = provider() to_inject[arg_key] = provider()
result = fn(*args, **to_inject) result = fn(*args, **to_inject)
if _patched.__closing__: if patched.closing:
for arg_key, provider in _patched.__closing__.items(): for arg_key, provider in patched.closing.items():
if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker): if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker):
continue continue
if not isinstance(provider, providers.Resource): if not isinstance(provider, providers.Resource):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -132,8 +132,9 @@ cdef class Configuration(Object):
cdef str __name cdef str __name
cdef bint __strict cdef bint __strict
cdef dict __children cdef dict __children
cdef list __yaml_files
cdef list __ini_files cdef list __ini_files
cdef list __yaml_files
cdef list __json_files
cdef list __pydantic_settings cdef list __pydantic_settings
cdef object __weakref__ cdef object __weakref__

View File

@ -218,6 +218,7 @@ class ConfigurationOption(Provider[Any]):
def update(self, value: Any) -> None: ... def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ... def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ... def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
def from_json(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ... def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ... def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ...
@ -237,8 +238,9 @@ class Configuration(Object[Any]):
default: Optional[Any] = None, default: Optional[Any] = None,
*, *,
strict: bool = False, strict: bool = False,
yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
ini_files: Optional[_Iterable[Union[Path, str]]] = None, ini_files: Optional[_Iterable[Union[Path, str]]] = None,
yaml_files: Optional[_Iterable[Union[Path, str]]] = None,
json_files: Optional[_Iterable[Union[Path, str]]] = None,
pydantic_settings: Optional[_Iterable[PydanticSettings]] = None, pydantic_settings: Optional[_Iterable[PydanticSettings]] = None,
) -> None: ... ) -> None: ...
def __enter__(self) -> Configuration : ... def __enter__(self) -> Configuration : ...
@ -258,11 +260,14 @@ class Configuration(Object[Any]):
def get_children(self) -> _Dict[str, ConfigurationOption]: ... def get_children(self) -> _Dict[str, ConfigurationOption]: ...
def set_children(self, children: _Dict[str, ConfigurationOption]) -> Configuration: ... def set_children(self, children: _Dict[str, ConfigurationOption]) -> Configuration: ...
def get_ini_files(self) -> _List[Union[Path, str]]: ...
def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_yaml_files(self) -> _List[Union[Path, str]]: ... def get_yaml_files(self) -> _List[Union[Path, str]]: ...
def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_ini_files(self) -> _List[Union[Path, str]]: ... def get_json_files(self) -> _List[Union[Path, str]]: ...
def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... def set_json_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_pydantic_settings(self) -> _List[PydanticSettings]: ... def get_pydantic_settings(self) -> _List[PydanticSettings]: ...
def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> Configuration: ... def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> Configuration: ...
@ -275,6 +280,7 @@ class Configuration(Object[Any]):
def update(self, value: Any) -> None: ... def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ... def from_ini(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ... def from_yaml(self, filepath: Union[Path, str], required: bool = False, loader: Optional[Any] = None, envs_required: bool = False) -> None: ...
def from_json(self, filepath: Union[Path, str], required: bool = False, envs_required: bool = False) -> None: ...
def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ... def from_pydantic(self, settings: PydanticSettings, required: bool = False, **kwargs: Any) -> None: ...
def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ...
def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ... def from_env(self, name: str, default: Optional[Any] = None, required: bool = False, as_: Optional[_Callable[..., Any]] = None) -> None: ...

View File

@ -5,13 +5,14 @@ from __future__ import absolute_import
import copy import copy
import errno import errno
import functools import functools
import inspect
import importlib import importlib
import inspect
import json
import os import os
import re import re
import sys import sys
import types
import threading import threading
import types
import warnings import warnings
try: try:
@ -1741,6 +1742,44 @@ cdef class ConfigurationOption(Provider):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_json(self, filepath, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration from a json file.
Loaded configuration is merged recursively over the existing configuration.
:param filepath: Path to a configuration file.
:type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param envs_required: When True, raises an exception on undefined environment variable.
:type envs_required: bool
:rtype: None
"""
try:
with open(filepath) as opened_file:
config_content = opened_file.read()
except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = "Unable to load configuration file {0}".format(exception.strerror)
raise
return
config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content)
current_config = self.__call__()
if not current_config:
current_config = {}
self.override(merge_dicts(current_config, config))
def from_pydantic(self, settings, required=UNDEFINED, **kwargs): def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
"""Load configuration from pydantic settings. """Load configuration from pydantic settings.
@ -1886,24 +1925,29 @@ cdef class Configuration(Object):
DEFAULT_NAME = "config" DEFAULT_NAME = "config"
def __init__(self, name=DEFAULT_NAME, default=None, strict=False, yaml_files=None, ini_files=None, pydantic_settings=None): def __init__(self, name=DEFAULT_NAME, default=None, strict=False, ini_files=None, yaml_files=None, json_files=None, pydantic_settings=None):
self.__name = name self.__name = name
self.__strict = strict self.__strict = strict
self.__children = {} self.__children = {}
self.__yaml_files = []
self.__ini_files = [] self.__ini_files = []
self.__yaml_files = []
self.__json_files = []
self.__pydantic_settings = [] self.__pydantic_settings = []
super().__init__(provides={}) super().__init__(provides={})
self.set_default(default) self.set_default(default)
if ini_files is None:
ini_files = []
self.set_ini_files(ini_files)
if yaml_files is None: if yaml_files is None:
yaml_files = [] yaml_files = []
self.set_yaml_files(yaml_files) self.set_yaml_files(yaml_files)
if ini_files is None: if json_files is None:
ini_files = [] json_files = []
self.set_ini_files(ini_files) self.set_json_files(json_files)
if pydantic_settings is None: if pydantic_settings is None:
pydantic_settings = [] pydantic_settings = []
@ -1919,8 +1963,9 @@ cdef class Configuration(Object):
copied.set_default(self.get_default()) copied.set_default(self.get_default())
copied.set_strict(self.get_strict()) copied.set_strict(self.get_strict())
copied.set_children(deepcopy(self.get_children(), memo)) copied.set_children(deepcopy(self.get_children(), memo))
copied.set_yaml_files(self.get_yaml_files())
copied.set_ini_files(self.get_ini_files()) copied.set_ini_files(self.get_ini_files())
copied.set_yaml_files(self.get_yaml_files())
copied.set_json_files(self.get_json_files())
copied.set_pydantic_settings(self.get_pydantic_settings()) copied.set_pydantic_settings(self.get_pydantic_settings())
self._copy_overridings(copied, memo) self._copy_overridings(copied, memo)
@ -1995,6 +2040,15 @@ cdef class Configuration(Object):
self.__children = children self.__children = children
return self return self
def get_ini_files(self):
"""Return list of INI files."""
return list(self.__ini_files)
def set_ini_files(self, files):
"""Set list of INI files."""
self.__ini_files = list(files)
return self
def get_yaml_files(self): def get_yaml_files(self):
"""Return list of YAML files.""" """Return list of YAML files."""
return list(self.__yaml_files) return list(self.__yaml_files)
@ -2004,13 +2058,13 @@ cdef class Configuration(Object):
self.__yaml_files = list(files) self.__yaml_files = list(files)
return self return self
def get_ini_files(self): def get_json_files(self):
"""Return list of INI files.""" """Return list of JSON files."""
return list(self.__ini_files) return list(self.__json_files)
def set_ini_files(self, files): def set_json_files(self, files):
"""Set list of INI files.""" """Set list of JSON files."""
self.__ini_files = list(files) self.__json_files = list(files)
return self return self
def get_pydantic_settings(self): def get_pydantic_settings(self):
@ -2039,11 +2093,14 @@ cdef class Configuration(Object):
:param envs_required: When True, raises an error on undefined environment variable. :param envs_required: When True, raises an error on undefined environment variable.
:type envs_required: bool :type envs_required: bool
""" """
for file in self.get_ini_files():
self.from_ini(file, required=required, envs_required=envs_required)
for file in self.get_yaml_files(): for file in self.get_yaml_files():
self.from_yaml(file, required=required, envs_required=envs_required) self.from_yaml(file, required=required, envs_required=envs_required)
for file in self.get_ini_files(): for file in self.get_json_files():
self.from_ini(file, required=required, envs_required=envs_required) self.from_json(file, required=required, envs_required=envs_required)
for settings in self.get_pydantic_settings(): for settings in self.get_pydantic_settings():
self.from_pydantic(settings, required=required) self.from_pydantic(settings, required=required)
@ -2254,6 +2311,44 @@ cdef class Configuration(Object):
current_config = {} current_config = {}
self.override(merge_dicts(current_config, config)) self.override(merge_dicts(current_config, config))
def from_json(self, filepath, required=UNDEFINED, envs_required=UNDEFINED):
"""Load configuration from a json file.
Loaded configuration is merged recursively over the existing configuration.
:param filepath: Path to a configuration file.
:type filepath: str
:param required: When required is True, raise an exception if file does not exist.
:type required: bool
:param envs_required: When True, raises an exception on undefined environment variable.
:type envs_required: bool
:rtype: None
"""
try:
with open(filepath) as opened_file:
config_content = opened_file.read()
except IOError as exception:
if required is not False \
and (self._is_strict_mode_enabled() or required is True) \
and exception.errno in (errno.ENOENT, errno.EISDIR):
exception.strerror = "Unable to load configuration file {0}".format(exception.strerror)
raise
return
config_content = _resolve_config_env_markers(
config_content,
envs_required=envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(),
)
config = json.loads(config_content)
current_config = self.__call__()
if not current_config:
current_config = {}
self.override(merge_dicts(current_config, config))
def from_pydantic(self, settings, required=UNDEFINED, **kwargs): def from_pydantic(self, settings, required=UNDEFINED, **kwargs):
"""Load configuration from pydantic settings. """Load configuration from pydantic settings.

View File

@ -1,4 +1,5 @@
"""Wiring module.""" """Wiring module."""
import functools import functools
import inspect import inspect
import importlib import importlib
@ -91,20 +92,26 @@ Container = Any
class PatchedRegistry: class PatchedRegistry:
def __init__(self): def __init__(self) -> None:
self._callables: Set[Callable[..., Any]] = set() self._callables: Dict[Callable[..., Any], "PatchedCallable"] = {}
self._attributes: Set[PatchedAttribute] = set() self._attributes: Set[PatchedAttribute] = set()
def add_callable(self, patched: Callable[..., Any]) -> None: def register_callable(self, patched: "PatchedCallable") -> None:
self._callables.add(patched) self._callables[patched.patched] = patched
def get_callables_from_module(self, module: ModuleType) -> Iterator[Callable[..., Any]]: def get_callables_from_module(self, module: ModuleType) -> Iterator[Callable[..., Any]]:
for patched in self._callables: for patched_callable in self._callables.values():
if patched.__module__ != module.__name__: if not patched_callable.is_in_module(module):
continue continue
yield patched yield patched_callable.patched
def add_attribute(self, patched: "PatchedAttribute"): def get_callable(self, fn: Callable[..., Any]) -> "PatchedCallable":
return self._callables.get(fn)
def has_callable(self, fn: Callable[..., Any]) -> bool:
return fn in self._callables
def register_attribute(self, patched: "PatchedAttribute") -> None:
self._attributes.add(patched) self._attributes.add(patched)
def get_attributes_from_module(self, module: ModuleType) -> Iterator["PatchedAttribute"]: def get_attributes_from_module(self, module: ModuleType) -> Iterator["PatchedAttribute"]:
@ -113,16 +120,69 @@ class PatchedRegistry:
continue continue
yield attribute yield attribute
def clear_module_attributes(self, module: ModuleType): def clear_module_attributes(self, module: ModuleType) -> None:
for attribute in self._attributes.copy(): for attribute in self._attributes.copy():
if not attribute.is_in_module(module): if not attribute.is_in_module(module):
continue continue
self._attributes.remove(attribute) self._attributes.remove(attribute)
class PatchedCallable:
__slots__ = (
"patched",
"original",
"reference_injections",
"injections",
"reference_closing",
"closing",
)
def __init__(
self,
patched: Optional[Callable[..., Any]] = None,
original: Optional[Callable[..., Any]] = None,
reference_injections: Optional[Dict[Any, Any]] = None,
reference_closing: Optional[Dict[Any, Any]] = None,
) -> None:
self.patched = patched
self.original = original
if reference_injections is None:
reference_injections = {}
self.reference_injections: Dict[Any, Any] = reference_injections.copy()
self.injections: Dict[Any, Any] = {}
if reference_closing is None:
reference_closing = {}
self.reference_closing: Dict[Any, Any] = reference_closing.copy()
self.closing: Dict[Any, Any] = {}
def is_in_module(self, module: ModuleType) -> bool:
if self.patched is None:
return False
return self.patched.__module__ == module.__name__
def add_injection(self, kwarg: Any, injection: Any) -> None:
self.injections[kwarg] = injection
def add_closing(self, kwarg: Any, injection: Any) -> None:
self.closing[kwarg] = injection
def unwind_injections(self) -> None:
self.injections = {}
self.closing = {}
class PatchedAttribute: class PatchedAttribute:
def __init__(self, member: Any, name: str, marker: "_Marker"): __slots__ = (
"member",
"name",
"marker",
)
def __init__(self, member: Any, name: str, marker: "_Marker") -> None:
self.member = member self.member = member
self.name = name self.name = name
self.marker = marker self.marker = marker
@ -142,7 +202,7 @@ class ProvidersMap:
CONTAINER_STRING_ID = "<container>" CONTAINER_STRING_ID = "<container>"
def __init__(self, container): def __init__(self, container) -> None:
self._container = container self._container = container
self._map = self._create_providers_map( self._map = self._create_providers_map(
current_container=container, current_container=container,
@ -398,7 +458,6 @@ def inject(fn: F) -> F:
"""Decorate callable with injecting decorator.""" """Decorate callable with injecting decorator."""
reference_injections, reference_closing = _fetch_reference_injections(fn) reference_injections, reference_closing = _fetch_reference_injections(fn)
patched = _get_patched(fn, reference_injections, reference_closing) patched = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add_callable(patched)
return cast(F, patched) return cast(F, patched)
@ -413,7 +472,6 @@ def _patch_fn(
if not reference_injections: if not reference_injections:
return return
fn = _get_patched(fn, reference_injections, reference_closing) fn = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add_callable(fn)
_bind_injections(fn, providers_map) _bind_injections(fn, providers_map)
@ -439,7 +497,6 @@ def _patch_method(
if not reference_injections: if not reference_injections:
return return
fn = _get_patched(fn, reference_injections, reference_closing) fn = _get_patched(fn, reference_injections, reference_closing)
_patched_registry.add_callable(fn)
_bind_injections(fn, providers_map) _bind_injections(fn, providers_map)
@ -476,7 +533,7 @@ def _patch_attribute(
if provider is None: if provider is None:
return return
_patched_registry.add_attribute(PatchedAttribute(member, name, marker)) _patched_registry.register_attribute(PatchedAttribute(member, name, marker))
if isinstance(marker, Provide): if isinstance(marker, Provide):
instance = provider() instance = provider()
@ -537,27 +594,33 @@ def _fetch_reference_injections( # noqa: C901
def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None: def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None:
for injection, marker in fn.__reference_injections__.items(): patched_callable = _patched_registry.get_callable(fn)
if patched_callable is None:
return
for injection, marker in patched_callable.reference_injections.items():
provider = providers_map.resolve_provider(marker.provider, marker.modifier) provider = providers_map.resolve_provider(marker.provider, marker.modifier)
if provider is None: if provider is None:
continue continue
if isinstance(marker, Provide): if isinstance(marker, Provide):
fn.__injections__[injection] = provider patched_callable.add_injection(injection, provider)
elif isinstance(marker, Provider): elif isinstance(marker, Provider):
if isinstance(provider, providers.Delegate): if isinstance(provider, providers.Delegate):
fn.__injections__[injection] = provider patched_callable.add_injection(injection, provider)
else: else:
fn.__injections__[injection] = provider.provider patched_callable.add_injection(injection, provider.provider)
if injection in fn.__reference_closing__: if injection in patched_callable.reference_closing:
fn.__closing__[injection] = provider patched_callable.add_closing(injection, provider)
def _unbind_injections(fn: Callable[..., Any]) -> None: def _unbind_injections(fn: Callable[..., Any]) -> None:
fn.__injections__ = {} patched_callable = _patched_registry.get_callable(fn)
fn.__closing__ = {} if patched_callable is None:
return
patched_callable.unwind_injections()
def _fetch_modules(package): def _fetch_modules(package):
@ -573,26 +636,32 @@ def _fetch_modules(package):
return modules return modules
def _is_method(member): def _is_method(member) -> bool:
return inspect.ismethod(member) or inspect.isfunction(member) return inspect.ismethod(member) or inspect.isfunction(member)
def _is_marker(member): def _is_marker(member) -> bool:
return isinstance(member, _Marker) return isinstance(member, _Marker)
def _get_patched(fn, reference_injections, reference_closing): def _get_patched(
if inspect.iscoroutinefunction(fn): fn: F,
patched = _get_async_patched(fn) reference_injections: Dict[Any, Any],
else: reference_closing: Dict[Any, Any],
patched = _get_sync_patched(fn) ) -> F:
patched_object = PatchedCallable(
original=fn,
reference_injections=reference_injections,
reference_closing=reference_closing,
)
patched.__wired__ = True if inspect.iscoroutinefunction(fn):
patched.__original__ = fn patched = _get_async_patched(fn, patched_object)
patched.__injections__ = {} else:
patched.__reference_injections__ = reference_injections patched = _get_sync_patched(fn, patched_object)
patched.__closing__ = {}
patched.__reference_closing__ = reference_closing patched_object.patched = patched
_patched_registry.register_callable(patched_object)
return patched return patched
@ -601,8 +670,8 @@ def _is_fastapi_depends(param: Any) -> bool:
return fastapi and isinstance(param, fastapi.params.Depends) return fastapi and isinstance(param, fastapi.params.Depends)
def _is_patched(fn): def _is_patched(fn) -> bool:
return getattr(fn, "__wired__", False) is True return _patched_registry.has_callable(fn)
def _is_declarative_container(instance: Any) -> bool: def _is_declarative_container(instance: Any) -> bool:
@ -630,7 +699,7 @@ class Modifier:
class TypeModifier(Modifier): class TypeModifier(Modifier):
def __init__(self, type_: Type): def __init__(self, type_: Type) -> None:
self.type_ = type_ self.type_ = type_
def modify( def modify(
@ -658,7 +727,7 @@ def as_(type_: Type) -> TypeModifier:
class RequiredModifier(Modifier): class RequiredModifier(Modifier):
def __init__(self): def __init__(self) -> None:
self.type_modifier = None self.type_modifier = None
def as_int(self) -> "RequiredModifier": def as_int(self) -> "RequiredModifier":
@ -714,7 +783,7 @@ class ProvidedInstance(Modifier):
TYPE_ITEM = "item" TYPE_ITEM = "item"
TYPE_CALL = "call" TYPE_CALL = "call"
def __init__(self): def __init__(self) -> None:
self.segments = [] self.segments = []
def __getattr__(self, item): def __getattr__(self, item):
@ -799,32 +868,32 @@ class AutoLoader:
Automatically wire containers when modules are imported. Automatically wire containers when modules are imported.
""" """
def __init__(self): def __init__(self) -> None:
self.containers = [] self.containers = []
self._path_hook = None self._path_hook = None
def register_containers(self, *containers): def register_containers(self, *containers) -> None:
self.containers.extend(containers) self.containers.extend(containers)
if not self.installed: if not self.installed:
self.install() self.install()
def unregister_containers(self, *containers): def unregister_containers(self, *containers) -> None:
for container in containers: for container in containers:
self.containers.remove(container) self.containers.remove(container)
if not self.containers: if not self.containers:
self.uninstall() self.uninstall()
def wire_module(self, module): def wire_module(self, module) -> None:
for container in self.containers: for container in self.containers:
container.wire(modules=[module]) container.wire(modules=[module])
@property @property
def installed(self): def installed(self) -> bool:
return self._path_hook in sys.path_hooks return self._path_hook in sys.path_hooks
def install(self): def install(self) -> None:
if self.installed: if self.installed:
return return
@ -855,7 +924,7 @@ class AutoLoader:
sys.path_importer_cache.clear() sys.path_importer_cache.clear()
importlib.invalidate_caches() importlib.invalidate_caches()
def uninstall(self): def uninstall(self) -> None:
if not self.installed: if not self.installed:
return return
@ -900,14 +969,14 @@ from ._cwiring import _async_inject # noqa
# Wiring uses the following Python wrapper because there is # Wiring uses the following Python wrapper because there is
# no possibility to compile a first-type citizen coroutine in Cython. # no possibility to compile a first-type citizen coroutine in Cython.
def _get_async_patched(fn): def _get_async_patched(fn: F, patched: PatchedCallable) -> F:
@functools.wraps(fn) @functools.wraps(fn)
async def _patched(*args, **kwargs): async def _patched(*args, **kwargs):
return await _async_inject( return await _async_inject(
fn, fn,
args, args,
kwargs, kwargs,
_patched.__injections__, patched.injections,
_patched.__closing__, patched.closing,
) )
return _patched return _patched

View File

@ -1,7 +1,10 @@
[pytest] [pytest]
testpaths = tests/unit/ testpaths = tests/unit/
python_files = test_*_py2_py3.py python_files = test_*_py2_py3.py
asyncio_mode = auto
filterwarnings = filterwarnings =
ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning
ignore:The \`scipy(.*?)\` namespace is deprecated(.*):DeprecationWarning
ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.* ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.*

View File

@ -1,7 +1,10 @@
[pytest] [pytest]
testpaths = tests/unit/ testpaths = tests/unit/
python_files = test_*_py3.py python_files = test_*_py3.py
asyncio_mode = auto
filterwarnings = filterwarnings =
ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning
ignore:The \`scipy(.*?)\` namespace is deprecated(.*):DeprecationWarning
ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.* ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.*

View File

@ -1,7 +1,10 @@
[pytest] [pytest]
testpaths = tests/unit/ testpaths = tests/unit/
python_files = test_*_py3*.py python_files = test_*_py3*.py
asyncio_mode = auto
filterwarnings = filterwarnings =
ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning
ignore:The \`scipy(.*?)\` namespace is deprecated(.*):DeprecationWarning
ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.* ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.*

View File

@ -1,5 +1,7 @@
from pathlib import Path from pathlib import Path
from dependency_injector import providers from dependency_injector import providers
from pydantic import BaseSettings as PydanticSettings
# Test 1: to check the getattr # Test 1: to check the getattr
@ -8,18 +10,26 @@ provider1 = providers.Factory(dict, a=config1.a)
# Test 2: to check the from_*() method # Test 2: to check the from_*() method
config2 = providers.Configuration() config2 = providers.Configuration()
config2.from_dict({}) config2.from_dict({})
config2.from_value({})
config2.from_ini("config.ini") config2.from_ini("config.ini")
config2.from_ini(Path("config.ini")) config2.from_ini(Path("config.ini"))
config2.from_yaml("config.yml") config2.from_yaml("config.yml")
config2.from_yaml(Path("config.yml")) config2.from_yaml(Path("config.yml"))
config2.from_json("config.json")
config2.from_json(Path("config.json"))
config2.from_env("ENV", "default") config2.from_env("ENV", "default")
config2.from_env("ENV", as_=int, default=123) config2.from_env("ENV", as_=int, default=123)
config2.from_env("ENV", as_=float, required=True) config2.from_env("ENV", as_=float, required=True)
config2.from_env("ENV", as_=lambda env: str(env)) config2.from_env("ENV", as_=lambda env: str(env))
config2.from_pydantic(PydanticSettings())
# Test 3: to check as_*() methods # Test 3: to check as_*() methods
config3 = providers.Configuration() config3 = providers.Configuration()
int3: providers.Callable[int] = config3.option.as_int() int3: providers.Callable[int] = config3.option.as_int()
@ -29,3 +39,39 @@ int3_custom: providers.Callable[int] = config3.option.as_(int)
# Test 4: to check required() method # Test 4: to check required() method
config4 = providers.Configuration() config4 = providers.Configuration()
option4: providers.ConfigurationOption = config4.option.required() option4: providers.ConfigurationOption = config4.option.required()
# Test 5: to check get/set config files' methods and init arguments
# Test 5: ini
config5_ini = providers.Configuration(
ini_files=["config.ini", Path("config.ini")],
)
config5_ini.set_ini_files(["config.ini", Path("config.ini")])
config5_ini_files: list[str | Path] = config5_ini.get_ini_files()
# Test 5: yaml
config5_yaml = providers.Configuration(
yaml_files=["config.yml", Path("config.yml")],
)
config5_yaml.set_yaml_files(["config.yml", Path("config.yml")])
config5_yaml_files: list[str | Path] = config5_yaml.get_yaml_files()
# Test 5: json
config5_json = providers.Configuration(
json_files=["config.json", Path("config.json")],
)
config5_json.set_json_files(["config.json", Path("config.json")])
config5_json_files: list[str | Path] = config5_json.get_json_files()
# Test 5: pydantic
config5_pydantic = providers.Configuration(
pydantic_settings=[PydanticSettings()],
)
config5_pydantic.set_pydantic_settings([PydanticSettings()])
config5_pydantic_settings: list[PydanticSettings] = config5_pydantic.get_pydantic_settings()
# Test 6: to check init arguments
config6 = providers.Configuration(
name="config",
strict=True,
default={},
)

View File

@ -1,5 +1,6 @@
"""Fixtures module.""" """Fixtures module."""
import json
import os import os
from dependency_injector import providers from dependency_injector import providers
@ -51,14 +52,14 @@ def ini_config_file_2(tmp_path):
@fixture @fixture
def ini_config_file_3(tmp_path): def ini_config_file_3(tmp_path):
ini_config_file_3 = str(tmp_path / "config_3.ini") config_file = str(tmp_path / "config_3.ini")
with open(ini_config_file_3, "w") as file: with open(config_file, "w") as file:
file.write( file.write(
"[section1]\n" "[section1]\n"
"value1=${CONFIG_TEST_ENV}\n" "value1=${CONFIG_TEST_ENV}\n"
"value2=${CONFIG_TEST_PATH}/path\n" "value2=${CONFIG_TEST_PATH}/path\n"
) )
return ini_config_file_3 return config_file
@fixture @fixture
@ -91,14 +92,70 @@ def yaml_config_file_2(tmp_path):
@fixture @fixture
def yaml_config_file_3(tmp_path): def yaml_config_file_3(tmp_path):
yaml_config_file_3 = str(tmp_path / "config_3.yml") config_file = str(tmp_path / "config_3.yml")
with open(yaml_config_file_3, "w") as file: with open(config_file, "w") as file:
file.write( file.write(
"section1:\n" "section1:\n"
" value1: ${CONFIG_TEST_ENV}\n" " value1: ${CONFIG_TEST_ENV}\n"
" value2: ${CONFIG_TEST_PATH}/path\n" " value2: ${CONFIG_TEST_PATH}/path\n"
) )
return yaml_config_file_3 return config_file
@fixture
def json_config_file_1(tmp_path):
config_file = str(tmp_path / "config_1.json")
with open(config_file, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": 1,
},
"section2": {
"value2": 2,
},
},
),
)
return config_file
@fixture
def json_config_file_2(tmp_path):
config_file = str(tmp_path / "config_2.json")
with open(config_file, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": 11,
"value11": 11,
},
"section3": {
"value3": 3,
},
},
),
)
return config_file
@fixture
def json_config_file_3(tmp_path):
config_file = str(tmp_path / "config_3.json")
with open(config_file, "w") as file:
file.write(
json.dumps(
{
"section1": {
"value1": "${CONFIG_TEST_ENV}",
"value2": "${CONFIG_TEST_PATH}/path",
},
},
),
)
return config_file
@fixture(autouse=True) @fixture(autouse=True)

View File

@ -0,0 +1,84 @@
"""Configuration.from_json() tests."""
from dependency_injector import errors
from pytest import mark, raises
def test(config, json_config_file_1):
config.from_json(json_config_file_1)
assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}}
assert config.section1() == {"value1": 1}
assert config.section1.value1() == 1
assert config.section2() == {"value2": 2}
assert config.section2.value2() == 2
def test_merge(config, json_config_file_1, json_config_file_2):
config.from_json(json_config_file_1)
config.from_json(json_config_file_2)
assert config() == {
"section1": {
"value1": 11,
"value11": 11,
},
"section2": {
"value2": 2,
},
"section3": {
"value3": 3,
},
}
assert config.section1() == {"value1": 11, "value11": 11}
assert config.section1.value1() == 11
assert config.section1.value11() == 11
assert config.section2() == {"value2": 2}
assert config.section2.value2() == 2
assert config.section3() == {"value3": 3}
assert config.section3.value3() == 3
def test_file_does_not_exist(config):
config.from_json("./does_not_exist.json")
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
with raises(IOError):
config.from_json("./does_not_exist.json")
def test_option_file_does_not_exist(config):
config.option.from_json("./does_not_exist.json")
assert config.option() is None
@mark.parametrize("config_type", ["strict"])
def test_option_file_does_not_exist_strict_mode(config):
with raises(IOError):
config.option.from_json("./does_not_exist.json")
def test_required_file_does_not_exist(config):
with raises(IOError):
config.from_json("./does_not_exist.json", required=True)
def test_required_option_file_does_not_exist(config):
with raises(IOError):
config.option.from_json("./does_not_exist.json", required=True)
@mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config):
config.from_json("./does_not_exist.json", required=False)
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_not_required_option_file_does_not_exist_strict_mode(config):
config.option.from_json("./does_not_exist.json", required=False)
with raises(errors.Error):
config.option()

View File

@ -0,0 +1,198 @@
"""Configuration.from_json() with environment variables interpolation tests."""
import json
import os
from pytest import mark, raises
def test_env_variable_interpolation(config, json_config_file_3):
config.from_json(json_config_file_3)
assert config() == {
"section1": {
"value1": "test-value",
"value2": "test-path/path",
},
}
assert config.section1() == {
"value1": "test-value",
"value2": "test-path/path",
}
assert config.section1.value1() == "test-value"
assert config.section1.value2() == "test-path/path"
def test_missing_envs_not_required(config, json_config_file_3):
del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"]
config.from_json(json_config_file_3)
assert config() == {
"section1": {
"value1": "",
"value2": "/path",
},
}
assert config.section1() == {
"value1": "",
"value2": "/path",
}
assert config.section1.value1() == ""
assert config.section1.value2() == "/path"
def test_missing_envs_required(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_json(json_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"])
def test_missing_envs_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.from_json(json_config_file_3)
@mark.parametrize("config_type", ["strict"])
def test_missing_envs_not_required_in_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.from_json(json_config_file_3, envs_required=False)
assert config.section.undefined() == ""
def test_option_missing_envs_not_required(config, json_config_file_3):
del os.environ["CONFIG_TEST_ENV"]
del os.environ["CONFIG_TEST_PATH"]
config.option.from_json(json_config_file_3)
assert config.option() == {
"section1": {
"value1": "",
"value2": "/path",
},
}
assert config.option.section1() == {
"value1": "",
"value2": "/path",
}
assert config.option.section1.value1() == ""
assert config.option.section1.value2() == "/path"
def test_option_missing_envs_required(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_json(json_config_file_3, envs_required=True)
@mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_not_required_in_strict_mode(config, json_config_file_3):
config.override({"option": {}})
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.option.from_json(json_config_file_3, envs_required=False)
assert config.option.section.undefined() == ""
@mark.parametrize("config_type", ["strict"])
def test_option_missing_envs_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.option.from_json(json_config_file_3)
def test_default_values(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"defined_with_default": "${DEFINED:default}",
"undefined_with_default": "${UNDEFINED:default}",
"complex": "${DEFINED}/path/${DEFINED:default}/${UNDEFINED}/${UNDEFINED:default}",
},
},
),
)
config.from_json(json_config_file_3)
assert config.section() == {
"defined_with_default": "defined",
"undefined_with_default": "default",
"complex": "defined/path/defined//default",
}
def test_option_env_variable_interpolation(config, json_config_file_3):
config.option.from_json(json_config_file_3)
assert config.option() == {
"section1": {
"value1": "test-value",
"value2": "test-path/path",
},
}
assert config.option.section1() == {
"value1": "test-value",
"value2": "test-path/path",
}
assert config.option.section1.value1() == "test-value"
assert config.option.section1.value2() == "test-path/path"

View File

@ -47,6 +47,11 @@ def test_set_files(config):
assert config.get_ini_files() == ["file1.ini", "file2.ini"] assert config.get_ini_files() == ["file1.ini", "file2.ini"]
def test_copy(config, ini_config_file_1, ini_config_file_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_ini_files() == [ini_config_file_1, ini_config_file_2]
def test_file_does_not_exist(config): def test_file_does_not_exist(config):
config.set_ini_files(["./does_not_exist.ini"]) config.set_ini_files(["./does_not_exist.ini"])
config.load() config.load()

View File

@ -0,0 +1,114 @@
"""Configuration(json_files=[...]) tests."""
import json
from dependency_injector import providers
from pytest import fixture, mark, raises
@fixture
def config(config_type, json_config_file_1, json_config_file_2):
if config_type == "strict":
return providers.Configuration(strict=True)
elif config_type == "default":
return providers.Configuration(json_files=[json_config_file_1, json_config_file_2])
else:
raise ValueError("Undefined config type \"{0}\"".format(config_type))
def test_load(config):
config.load()
assert config() == {
"section1": {
"value1": 11,
"value11": 11,
},
"section2": {
"value2": 2,
},
"section3": {
"value3": 3,
},
}
assert config.section1() == {"value1": 11, "value11": 11}
assert config.section1.value1() == 11
assert config.section1.value11() == 11
assert config.section2() == {"value2": 2}
assert config.section2.value2() == 2
assert config.section3() == {"value3": 3}
assert config.section3.value3() == 3
def test_get_files(config, json_config_file_1, json_config_file_2):
assert config.get_json_files() == [json_config_file_1, json_config_file_2]
def test_set_files(config):
config.set_json_files(["file1.json", "file2.json"])
assert config.get_json_files() == ["file1.json", "file2.json"]
def test_copy(config, json_config_file_1, json_config_file_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_json_files() == [json_config_file_1, json_config_file_2]
def test_file_does_not_exist(config):
config.set_json_files(["./does_not_exist.json"])
config.load()
assert config() == {}
@mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config):
config.set_json_files(["./does_not_exist.json"])
with raises(IOError):
config.load()
assert config() == {}
def test_required_file_does_not_exist(config):
config.set_json_files(["./does_not_exist.json"])
with raises(IOError):
config.load(required=True)
@mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config):
config.set_json_files(["./does_not_exist.json"])
config.load(required=False)
assert config() == {}
def test_missing_envs_required(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.set_json_files([json_config_file_3])
with raises(ValueError, match="Missing required environment variable \"UNDEFINED\""):
config.load(envs_required=True)
@mark.parametrize("config_type", ["strict"])
def test_missing_envs_not_required_in_strict_mode(config, json_config_file_3):
with open(json_config_file_3, "w") as file:
file.write(
json.dumps(
{
"section": {
"undefined": "${UNDEFINED}",
},
},
),
)
config.set_json_files([json_config_file_3])
config.load(envs_required=False)
assert config.section.undefined() == ""

View File

@ -80,6 +80,11 @@ def test_get_pydantic_settings(config, pydantic_settings_1, pydantic_settings_2)
assert config.get_pydantic_settings() == [pydantic_settings_1, pydantic_settings_2] assert config.get_pydantic_settings() == [pydantic_settings_1, pydantic_settings_2]
def test_copy(config, pydantic_settings_1, pydantic_settings_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_pydantic_settings() == [pydantic_settings_1, pydantic_settings_2]
def test_set_pydantic_settings(config): def test_set_pydantic_settings(config):
class Settings3(pydantic.BaseSettings): class Settings3(pydantic.BaseSettings):
... ...

View File

@ -47,6 +47,11 @@ def test_set_files(config):
assert config.get_yaml_files() == ["file1.yml", "file2.yml"] assert config.get_yaml_files() == ["file1.yml", "file2.yml"]
def test_copy(config, yaml_config_file_1, yaml_config_file_2):
config_copy = providers.deepcopy(config)
assert config_copy.get_yaml_files() == [yaml_config_file_1, yaml_config_file_2]
def test_file_does_not_exist(config): def test_file_does_not_exist(config):
config.set_yaml_files(["./does_not_exist.yml"]) config.set_yaml_files(["./does_not_exist.yml"])
config.load() config.load()

View File

@ -4,7 +4,6 @@ import os
import sqlite3 import sqlite3
from dependency_injector import containers from dependency_injector import containers
from pytest import mark
from samples.schema.services import UserService, AuthService, PhotoService from samples.schema.services import UserService, AuthService, PhotoService
@ -19,18 +18,20 @@ SAMPLES_DIR = os.path.abspath(
def test_single_container_schema(container: containers.DynamicContainer): def test_single_container_schema(container: containers.DynamicContainer):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-single.yml") container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-single.yml")
container.config.from_dict({ container.config.from_dict(
"database": { {
"dsn": ":memory:", "database": {
}, "dsn": ":memory:",
"aws": { },
"access_key_id": "KEY", "aws": {
"secret_access_key": "SECRET", "access_key_id": "KEY",
}, "secret_access_key": "SECRET",
"auth": { },
"token_ttl": 3600, "auth": {
}, "token_ttl": 3600,
}) },
},
)
# User service # User service
user_service1 = container.user_service() user_service1 = container.user_service()
@ -79,18 +80,20 @@ def test_single_container_schema(container: containers.DynamicContainer):
def test_multiple_containers_schema(container: containers.DynamicContainer): def test_multiple_containers_schema(container: containers.DynamicContainer):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple.yml") container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple.yml")
container.core.config.from_dict({ container.core.config.from_dict(
"database": { {
"dsn": ":memory:", "database": {
"dsn": ":memory:",
},
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
}, },
"aws": { )
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
})
# User service # User service
user_service1 = container.services.user() user_service1 = container.services.user()
@ -139,18 +142,20 @@ def test_multiple_containers_schema(container: containers.DynamicContainer):
def test_multiple_reordered_containers_schema(container: containers.DynamicContainer): def test_multiple_reordered_containers_schema(container: containers.DynamicContainer):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple-reordered.yml") container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple-reordered.yml")
container.core.config.from_dict({ container.core.config.from_dict(
"database": { {
"dsn": ":memory:", "database": {
"dsn": ":memory:",
},
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
}, },
"aws": { )
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
})
# User service # User service
user_service1 = container.services.user() user_service1 = container.services.user()
@ -199,18 +204,20 @@ def test_multiple_reordered_containers_schema(container: containers.DynamicConta
def test_multiple_containers_with_inline_providers_schema(container: containers.DynamicContainer): def test_multiple_containers_with_inline_providers_schema(container: containers.DynamicContainer):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple-inline.yml") container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple-inline.yml")
container.core.config.from_dict({ container.core.config.from_dict(
"database": { {
"dsn": ":memory:", "database": {
"dsn": ":memory:",
},
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
}, },
"aws": { )
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
})
# User service # User service
user_service1 = container.services.user() user_service1 = container.services.user()
@ -257,7 +264,6 @@ def test_multiple_containers_with_inline_providers_schema(container: containers.
assert photo_service2.s3 is container.gateways.s3_client() assert photo_service2.s3 is container.gateways.s3_client()
@mark.skip(reason="Boto3 tries to connect to the internet")
def test_schema_with_boto3_session(container: containers.DynamicContainer): def test_schema_with_boto3_session(container: containers.DynamicContainer):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-boto3-session.yml") container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-boto3-session.yml")
container.config.from_dict( container.config.from_dict(

View File

@ -0,0 +1,49 @@
"""Test that wiring works properly with @functools.wraps decorator.
See issue for details: https://github.com/ets-labs/python-dependency-injector/issues/454
"""
import functools
from dependency_injector.wiring import inject, Provide
from pytest import fixture
from samples.wiring.container import Container
@fixture
def container():
container = Container()
yield container
container.unwire()
def decorator1(func):
@functools.wraps(func)
@inject
def wrapper(value1: int = Provide[Container.config.value1]):
result = func()
return result + value1
return wrapper
def decorator2(func):
@functools.wraps(func)
@inject
def wrapper(value2: int = Provide[Container.config.value2]):
result = func()
return result + value2
return wrapper
@decorator1
@decorator2
def sample():
return 2
def test_wraps(container: Container):
container.wire(modules=[__name__])
container.config.from_dict({"value1": 42, "value2": 15})
assert sample() == 2 + 42 + 15