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.
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.
It helps implementing the dependency injection principle.
It helps implement the dependency injection principle.
Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency`` and ``Selector`` providers
that help assembling your objects.
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers
that help assemble your objects.
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
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>`_.
- **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 `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
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>`_.
- **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.
See `Wiring <https://python-dependency-injector.ets-labs.org/wiring.html>`_.
- **Asynchronous**. Supports asynchronous injections.
@ -75,7 +75,7 @@ Key features of the ``Dependency Injector``:
- **Typing**. Provides typing stubs, ``mypy``-friendly.
See `Typing and mypy <https://python-dependency-injector.ets-labs.org/providers/typing_mypy.html>`_.
- **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
@ -100,7 +100,7 @@ Key features of the ``Dependency Injector``:
@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()):
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
client with a mock. When you call ``main()`` the mock is injected.
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.
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 the ``Dependency Injector`` objects assembling is consolidated in the container.
Dependency injections are defined explicitly.
This makes easier to understand and change how application works.
With the ``Dependency Injector``, object assembling is consolidated in a container. Dependency injections are defined explicitly.
This makes it easier to understand and change how an application works.
.. figure:: https://raw.githubusercontent.com/wiki/ets-labs/python-dependency-injector/img/di-readme.svg
: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.
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.
Frequently asked questions
--------------------------
What is the dependency injection?
What is dependency injection?
- dependency injection is a principle that decreases coupling and increases cohesion
Why should I do the dependency injection?
- 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 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
What price do I pay and what do I get?
- you need to explicitly specify the dependencies
- it will be an extra work in the beginning
- it will payoff as the project grows
- it will be extra work in the beginning
- it will payoff as project grows
Have a question?
- 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.
project = "Dependency Injector"
copyright = "2021, Roman Mogylatov"
copyright = "2022, Roman Mogylatov"
author = "Roman Mogylatov"
# 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``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency`` and ``Selector`` providers
that help assembling your objects. See :ref:`providers`.
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers
that help assemble your objects. See :ref:`providers`.
- **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`.
- **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`.
- **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.
See :ref:`resource-provider`.
- **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`.
- **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **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
@ -106,7 +106,7 @@ Key features of the ``Dependency Injector``:
@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()):
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.
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
: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
why it's better than monkey-patching.
Originally dependency injection pattern got popular in the languages with a static typing,
like Java. Dependency injection is a principle that helps to achieve an inversion of control.
Dependency injection framework can significantly improve a flexibility of the language
with a 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
Originally dependency injection pattern got popular in languages with static typing like Java.
Dependency injection is a principle that helps to achieve an inversion of control. A
dependency injection framework can significantly improve the flexibility of a language
with static typing. Implementation of a dependency injection framework for a language
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.
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
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
easily using language fundamentals.
This page describes the advantages of the dependency injection usage in Python. It
contains Python examples that show how to implement dependency injection. It demonstrates a usage
of the dependency injection framework ``Dependency Injector``, its container, ``Factory``,
``Singleton`` and ``Configuration`` providers. The example shows how to use ``Dependency Injector``
providers overriding feature for testing or configuring project in different environments and
This page describes the advantages of applying dependency injection in Python. It
contains Python examples that show how to implement dependency injection. It demonstrates the usage
of the ``Dependency Injector`` framework, its container, ``Factory``, ``Singleton``,
and ``Configuration`` providers. The example shows how to use providers' overriding feature
of ``Dependency Injector`` for testing or re-configuring a project in different environments and
explains why it's better than monkey-patching.
What is dependency injection?
@ -44,14 +44,14 @@ What is coupling and cohesion?
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.
- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and
assemble back or assemble a different way. It is an opposite to high coupling.
- **High cohesion**. High cohesion is like using screws. Quite easy to disassemble and
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?
@ -66,14 +66,14 @@ Before:
class ApiClient:
def __init__(self):
def __init__(self) -> None:
self.api_key = os.getenv("API_KEY") # <-- dependency
self.timeout = os.getenv("TIMEOUT") # <-- dependency
self.timeout = int(os.getenv("TIMEOUT")) # <-- dependency
class Service:
def __init__(self):
def __init__(self) -> None:
self.api_client = ApiClient() # <-- dependency
@ -94,18 +94,18 @@ After:
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.timeout = timeout # <-- dependency is injected
class Service:
def __init__(self, api_client: ApiClient):
def __init__(self, api_client: ApiClient) -> None:
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(
api_client=ApiClient(
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(
api_client=ApiClient(
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?
-------------------------------------
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.
``Dependency Injector`` helps to assemble and inject the dependencies.
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
function argument. When you call this function framework assembles and injects
function argument. When you call this function, framework assembles and injects
the dependency.
.. code-block:: python
@ -182,7 +182,7 @@ the dependency.
@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()):
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
client with a mock. When you call ``main()`` the mock is injected.
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.
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.
Objects assembling is consolidated in the container. Dependency injections are defined explicitly.
This makes easier to understand and change how application works.
Objects assembling is consolidated in a container. Dependency injections are defined explicitly.
This makes it easier to understand and change how an application works.
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
anything, anytime. The problem with a monkey-patching is that it's too fragile. The reason is that
when you monkey-patch you do something that wasn't intended to be done. You monkey-patch the
implementation details. When implementation changes the monkey-patching is broken.
In Python, you can monkey-patch anything, anytime. The problem with monkey-patching is
that it's too fragile. The cause of it is that when you monkey-patch you do something that
wasn't intended to be done. You monkey-patch the implementation details. When implementation
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.
Also monkey-patching is a way too dirty to be used outside of the testing code for
reconfiguring the project for the different environments.
Also, monkey-patching is way too dirty to be used outside of the testing code for
re-configuring the project for the different environments.
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
functionality of the system by combining the components different way. You even can do it on
- **Flexibility**. The components are loosely coupled. You can easily extend or change the
functionality of a system by combining the components in a different way. You even can do it on
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.
- **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).
You have all the components and dependencies defined explicitly in the container. This
provides an overview and control on the application structure. It is easy to understand and
You have all the components and dependencies defined explicitly in a container. This
provides an overview and control of the application structure. It is easier to understand and
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
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
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:
- Already implemented
- Tested on all platforms and versions of Python
- Documented
- 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
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
two weeks. This time will be enough for getting your own impression. If you don't like it you
won't lose too much.
- **Common sense first**. Use a common sense when apply 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
- **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 many of the
implementation details. Experience comes with practice and time.
What's next?
@ -303,8 +303,7 @@ Choose one of the following as a next step:
Useful links
------------
There are some useful links related to dependency injection design pattern
that could be used for further reading:
A few useful links related to a dependency injection design pattern for further reading:
+ https://en.wikipedia.org/wiki/Dependency_injection
+ https://martinfowler.com/articles/injection.html

View File

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

View File

@ -2,7 +2,7 @@ Installation
============
``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
@ -10,7 +10,7 @@ To install latest version you can use ``pip``:
Some modules of the ``Dependency Injector`` are implemented as C extensions.
``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>`_.
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
>>> dependency_injector.__version__
'4.37.0'
'4.39.0'
.. note::
When add ``Dependency Injector`` to the ``requirements.txt`` don't forget to pin version
to the current major:
When adding ``Dependency Injector`` to ``pyproject.toml`` or ``requirements.txt``
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>`_.
Each release has appropriate tag. The tags are available on
All releases are available on the `PyPI release history page <https://pypi.org/project/dependency-injector/#history>`_.
Each release has an appropriate tag. The tags are available on the
`GitHub releases page <https://github.com/ets-labs/python-dependency-injector/releases>`_.
.. disqus::

View File

@ -11,23 +11,23 @@ Key features
Key features of the ``Dependency Injector``:
- **Providers**. Provides ``Factory``, ``Singleton``, ``Callable``, ``Coroutine``, ``Object``,
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency`` and ``Selector`` providers
that help assembling your objects. See :ref:`providers`.
``List``, ``Dict``, ``Configuration``, ``Resource``, ``Dependency``, and ``Selector`` providers
that help assemble your objects. See :ref:`providers`.
- **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`.
- **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`.
- **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.
See :ref:`resource-provider`.
- **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`.
- **Asynchronous**. Supports asynchronous injections. See :ref:`async-injections`.
- **Typing**. Provides typing stubs, ``mypy``-friendly. See :ref:`provider-typing`.
- **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:
@ -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.
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.
.. disqus::

View File

@ -7,6 +7,25 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly
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
------
- 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.*
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
--------------------------------

View File

@ -22,6 +22,82 @@ To use wiring you need:
:local:
: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
-------

View File

@ -3,18 +3,18 @@ import os
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.timeout = timeout # <-- dependency is injected
class Service:
def __init__(self, api_client: ApiClient):
def __init__(self, api_client: ApiClient) -> None:
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(
api_client=ApiClient(
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
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::
redis_1 | 1:C 04 Jan 2021 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 2021 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 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 2021 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 2021 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 2021 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:C 04 Jan 2022 02:42:14.115 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
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 2022 02:42:14.115 # Configuration loaded
redis_1 | 1:M 04 Jan 2022 02:42:14.116 * Running mode=standalone, port=6379.
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 2022 02:42:14.116 # Server initialized
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 2022 02:42:14.117 * RDB age 1 seconds
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 2022 02:42:14.117 * DB loaded from disk: 0.000 seconds
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:M 04 Jan 2021 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:C 04 Jan 2021 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 2021 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 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 2021 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 2021 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 2021 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:17.984 # User requested shutdown...
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 2022 02:42:22.035 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
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 2022 02:42:22.035 # Configuration loaded
redis_1 | 1:M 04 Jan 2022 02:42:22.037 * Running mode=standalone, port=6379.
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 2022 02:42:22.037 # Server initialized
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 2022 02:42:22.037 * RDB age 9 seconds
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 2022 02:42:22.037 * DB loaded from disk: 0.000 seconds
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: Waiting for application startup.
example_1 | INFO: Application startup complete.

View File

@ -29,15 +29,15 @@ The output should be something like:
Starting fastapi-sqlalchemy_webapp_1 ... done
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 | 2021-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 | 2021-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 | 2021-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 | 2021-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,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 ()
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 | 2022-02-04 22:07:19,804 INFO sqlalchemy.engine.base.Engine ()
webapp_1 | 2022-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 ()
webapp_1 | 2022-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 ()
webapp_1 | 2022-02-04 22:07:19,809 INFO sqlalchemy.engine.base.Engine
webapp_1 | CREATE TABLE users (
webapp_1 | id INTEGER NOT NULL,
webapp_1 | email VARCHAR,
@ -49,8 +49,8 @@ The output should be something like:
webapp_1 | )
webapp_1 |
webapp_1 |
webapp_1 | 2021-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,810 INFO sqlalchemy.engine.base.Engine ()
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: Waiting for application startup.
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__":
container = Container()
container.config.option1.from_value(date(2021, 6, 13))
container.config.option2.from_value(date(2021, 6, 14))
container.config.option1.from_value(date(2022, 3, 13))
container.config.option2.from_value(date(2022, 3, 14))
assert container.config() == {
"option1": date(2021, 6, 13),
"option2": date(2021, 6, 14),
"option1": date(2022, 3, 13),
"option2": date(2022, 3, 14),
}
assert container.config.option1() == date(2021, 6, 13)
assert container.config.option2() == date(2021, 6, 14)
assert container.config.option1() == date(2022, 3, 13)
assert container.config.option2() == date(2022, 3, 14)

View File

@ -1,4 +1,4 @@
cython==0.29.24
cython==0.29.30
pytest
pytest-asyncio
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
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
-r requirements-ext.txt

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,12 +7,12 @@ import inspect
import types
from . import providers
from .wiring import _Marker
from .wiring import _Marker, PatchedCallable
from .providers cimport Provider
def _get_sync_patched(fn):
def _get_sync_patched(fn, patched: PatchedCallable):
@functools.wraps(fn)
def _patched(*args, **kwargs):
cdef object result
@ -21,14 +21,14 @@ def _get_sync_patched(fn):
cdef Provider provider
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):
to_inject[arg_key] = provider()
result = fn(*args, **to_inject)
if _patched.__closing__:
for arg_key, provider in _patched.__closing__.items():
if patched.closing:
for arg_key, provider in patched.closing.items():
if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker):
continue
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 bint __strict
cdef dict __children
cdef list __yaml_files
cdef list __ini_files
cdef list __yaml_files
cdef list __json_files
cdef list __pydantic_settings
cdef object __weakref__

View File

@ -218,6 +218,7 @@ class ConfigurationOption(Provider[Any]):
def update(self, value: Any) -> 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_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_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: ...
@ -237,8 +238,9 @@ class Configuration(Object[Any]):
default: Optional[Any] = None,
*,
strict: bool = False,
yaml_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,
) -> None: ...
def __enter__(self) -> Configuration : ...
@ -258,11 +260,14 @@ class Configuration(Object[Any]):
def get_children(self) -> _Dict[str, ConfigurationOption]: ...
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 set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_ini_files(self) -> _List[Union[Path, str]]: ...
def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_json_files(self) -> _List[Union[Path, str]]: ...
def set_json_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ...
def get_pydantic_settings(self) -> _List[PydanticSettings]: ...
def set_pydantic_settings(self, settings: _Iterable[PydanticSettings]) -> Configuration: ...
@ -275,6 +280,7 @@ class Configuration(Object[Any]):
def update(self, value: Any) -> 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_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_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: ...

View File

@ -5,13 +5,14 @@ from __future__ import absolute_import
import copy
import errno
import functools
import inspect
import importlib
import inspect
import json
import os
import re
import sys
import types
import threading
import types
import warnings
try:
@ -1741,6 +1742,44 @@ cdef class ConfigurationOption(Provider):
current_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):
"""Load configuration from pydantic settings.
@ -1886,24 +1925,29 @@ cdef class Configuration(Object):
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.__strict = strict
self.__children = {}
self.__yaml_files = []
self.__ini_files = []
self.__yaml_files = []
self.__json_files = []
self.__pydantic_settings = []
super().__init__(provides={})
self.set_default(default)
if ini_files is None:
ini_files = []
self.set_ini_files(ini_files)
if yaml_files is None:
yaml_files = []
self.set_yaml_files(yaml_files)
if ini_files is None:
ini_files = []
self.set_ini_files(ini_files)
if json_files is None:
json_files = []
self.set_json_files(json_files)
if pydantic_settings is None:
pydantic_settings = []
@ -1919,8 +1963,9 @@ cdef class Configuration(Object):
copied.set_default(self.get_default())
copied.set_strict(self.get_strict())
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_yaml_files(self.get_yaml_files())
copied.set_json_files(self.get_json_files())
copied.set_pydantic_settings(self.get_pydantic_settings())
self._copy_overridings(copied, memo)
@ -1995,6 +2040,15 @@ cdef class Configuration(Object):
self.__children = children
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):
"""Return list of YAML files."""
return list(self.__yaml_files)
@ -2004,13 +2058,13 @@ cdef class Configuration(Object):
self.__yaml_files = list(files)
return self
def get_ini_files(self):
"""Return list of INI files."""
return list(self.__ini_files)
def get_json_files(self):
"""Return list of JSON files."""
return list(self.__json_files)
def set_ini_files(self, files):
"""Set list of INI files."""
self.__ini_files = list(files)
def set_json_files(self, files):
"""Set list of JSON files."""
self.__json_files = list(files)
return 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.
: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():
self.from_yaml(file, required=required, envs_required=envs_required)
for file in self.get_ini_files():
self.from_ini(file, required=required, envs_required=envs_required)
for file in self.get_json_files():
self.from_json(file, required=required, envs_required=envs_required)
for settings in self.get_pydantic_settings():
self.from_pydantic(settings, required=required)
@ -2254,6 +2311,44 @@ cdef class Configuration(Object):
current_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):
"""Load configuration from pydantic settings.

View File

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

View File

@ -1,7 +1,10 @@
[pytest]
testpaths = tests/unit/
python_files = test_*_py2_py3.py
asyncio_mode = auto
filterwarnings =
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:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning
ignore:The \`scipy(.*?)\` namespace is deprecated(.*):DeprecationWarning
ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.*

View File

@ -1,7 +1,10 @@
[pytest]
testpaths = tests/unit/
python_files = test_*_py3.py
asyncio_mode = auto
filterwarnings =
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:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning
ignore:The \`scipy(.*?)\` namespace is deprecated(.*):DeprecationWarning
ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.*

View File

@ -1,7 +1,10 @@
[pytest]
testpaths = tests/unit/
python_files = test_*_py3*.py
asyncio_mode = auto
filterwarnings =
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:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning
ignore:The \`scipy(.*?)\` namespace is deprecated(.*):DeprecationWarning
ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.*

View File

@ -1,5 +1,7 @@
from pathlib import Path
from dependency_injector import providers
from pydantic import BaseSettings as PydanticSettings
# Test 1: to check the getattr
@ -8,18 +10,26 @@ provider1 = providers.Factory(dict, a=config1.a)
# Test 2: to check the from_*() method
config2 = providers.Configuration()
config2.from_dict({})
config2.from_value({})
config2.from_ini("config.ini")
config2.from_ini(Path("config.ini"))
config2.from_yaml("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", as_=int, default=123)
config2.from_env("ENV", as_=float, required=True)
config2.from_env("ENV", as_=lambda env: str(env))
config2.from_pydantic(PydanticSettings())
# Test 3: to check as_*() methods
config3 = providers.Configuration()
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
config4 = providers.Configuration()
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."""
import json
import os
from dependency_injector import providers
@ -51,14 +52,14 @@ def ini_config_file_2(tmp_path):
@fixture
def ini_config_file_3(tmp_path):
ini_config_file_3 = str(tmp_path / "config_3.ini")
with open(ini_config_file_3, "w") as file:
config_file = str(tmp_path / "config_3.ini")
with open(config_file, "w") as file:
file.write(
"[section1]\n"
"value1=${CONFIG_TEST_ENV}\n"
"value2=${CONFIG_TEST_PATH}/path\n"
)
return ini_config_file_3
return config_file
@fixture
@ -91,14 +92,70 @@ def yaml_config_file_2(tmp_path):
@fixture
def yaml_config_file_3(tmp_path):
yaml_config_file_3 = str(tmp_path / "config_3.yml")
with open(yaml_config_file_3, "w") as file:
config_file = str(tmp_path / "config_3.yml")
with open(config_file, "w") as file:
file.write(
"section1:\n"
" value1: ${CONFIG_TEST_ENV}\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)

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"]
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):
config.set_ini_files(["./does_not_exist.ini"])
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]
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):
class Settings3(pydantic.BaseSettings):
...

View File

@ -47,6 +47,11 @@ def test_set_files(config):
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):
config.set_yaml_files(["./does_not_exist.yml"])
config.load()

View File

@ -4,7 +4,6 @@ import os
import sqlite3
from dependency_injector import containers
from pytest import mark
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):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-single.yml")
container.config.from_dict({
"database": {
"dsn": ":memory:",
},
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
})
container.config.from_dict(
{
"database": {
"dsn": ":memory:",
},
"aws": {
"access_key_id": "KEY",
"secret_access_key": "SECRET",
},
"auth": {
"token_ttl": 3600,
},
},
)
# 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):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple.yml")
container.core.config.from_dict({
"database": {
"dsn": ":memory:",
container.core.config.from_dict(
{
"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_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):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple-reordered.yml")
container.core.config.from_dict({
"database": {
"dsn": ":memory:",
container.core.config.from_dict(
{
"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_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):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-multiple-inline.yml")
container.core.config.from_dict({
"database": {
"dsn": ":memory:",
container.core.config.from_dict(
{
"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_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()
@mark.skip(reason="Boto3 tries to connect to the internet")
def test_schema_with_boto3_session(container: containers.DynamicContainer):
container.from_yaml_schema(f"{SAMPLES_DIR}/schema/container-boto3-session.yml")
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