Compare commits

...

29 Commits

Author SHA1 Message Date
ZipFile
2c0aede4aa Merge branch 'release/4.48.1' 2025-06-20 10:49:54 +00:00
ZipFile
84a14f2ca7 Update changelog 2025-06-20 10:03:00 +00:00
ZipFile
0c58064a36 Make wiring inspect exclsuions extensible 2025-06-20 10:03:00 +00:00
ZipFile
eb74b1e9d0 Minor improvements for _cwiring.DependencyResolver code generation
* Remove KWPair
* Avoid type checks around _is_injectable
2025-06-20 10:03:00 +00:00
ZipFile
be7d25518d Add typing-extensions as a dependency for older Python versions 2025-06-20 10:03:00 +00:00
ZipFile
04b5907f21 Add warning on extra @inject 2025-06-20 10:03:00 +00:00
ZipFile
e6cc12762f Add support for resource_type in Lifespans 2025-06-18 21:58:00 +00:00
ZipFile
bf2ddbce32 Upgrade cibuildwheel 2025-06-16 10:51:39 +00:00
ZipFile
9d6994391f Merge branch 'release/4.48.0' 2025-06-16 09:03:49 +00:00
ZipFile
dd84a1b5d6 Bump version 2025-06-16 08:53:36 +00:00
ZipFile
31a98f7731 Update changelog 2025-06-16 08:51:10 +00:00
ZipFile
b261251b34 Fix Sphinx warning 2025-06-16 08:50:00 +00:00
Aran Moncusí Ramírez
4bfe64563e
Add resource type parameter to init and shutdown resources using specialized providers (#858) 2025-06-16 11:34:02 +03:00
AndrianEquestrian
b411807572
Add support for Fast Stream Depends (#898) 2025-06-16 10:37:31 +03:00
ZipFile
f2da51e0d4 Use typing_extensions.Self as fallback (fixes #902) 2025-06-05 16:26:40 +00:00
ZipFile
2293251986 Add docs for ASGI Lifespan support 2025-06-03 20:43:06 +00:00
ZipFile
1b4b3d349f Fix some more Sphinx warnings 2025-06-03 20:33:13 +00:00
ZipFile
d8e49f7dd5
Add support for async generator injections (#900) 2025-06-03 21:45:43 +03:00
ZipFile
c1f14a876a Expose null awaitables 2025-06-02 22:46:57 +00:00
ZipFile
c97a0cc515 Fix mypy warnings in dependency_injector.ext 2025-06-01 18:57:47 +00:00
ZipFile
0ada62acbf Add .editorconfig 2025-06-01 18:50:07 +00:00
ZipFile
67827a36d1 Fix mypy warnigns in containers.pyi 2025-06-01 18:46:30 +00:00
ZipFile
ceed6a8222 Add combine_as_imports = true isort option 2025-06-01 18:45:47 +00:00
ZipFile
6766ef3eba Remove __init__.pyi 2025-06-01 18:45:12 +00:00
ZipFile
a8914e54e0 Fix Sphinx warnings 2025-06-01 18:08:37 +00:00
ZipFile
cdd9ce5048 Add doc section on wire() caching 2025-06-01 17:51:04 +00:00
ZipFile
51c7db771d Fix csv newline handling in movie-lister example 2025-06-01 17:35:32 +00:00
ZipFile
a322584308 Add context manager support to Resource provider 2025-06-01 15:48:57 +00:00
ZipFile
4b3476cb48 Use cache in _fetch_reference_injections() 2025-05-31 12:31:54 +00:00
46 changed files with 1099 additions and 436 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{py,pyi,pxd,pyx}]
ij_visual_guides = 80,88

View File

@ -70,10 +70,10 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Build wheels - name: Build wheels
uses: pypa/cibuildwheel@v2.23.3 uses: pypa/cibuildwheel@v3.0.0
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }} name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl path: ./wheelhouse/*.whl
test-publish: test-publish:

View File

@ -0,0 +1,9 @@
dependency_injector.ext.starlette
=================================
.. automodule:: dependency_injector.ext.starlette
:members:
:inherited-members:
:show-inheritance:
.. disqus::

View File

@ -9,3 +9,4 @@ API Documentation
containers containers
wiring wiring
errors errors
asgi-lifespan

View File

@ -72,7 +72,7 @@ release = version
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language = "en"
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:

View File

@ -78,7 +78,7 @@ Container is wired to the ``views`` module in the app config ``web/apps.py``:
.. literalinclude:: ../../examples/miniapps/django/web/apps.py .. literalinclude:: ../../examples/miniapps/django/web/apps.py
:language: python :language: python
:emphasize-lines: 13 :emphasize-lines: 12
Tests Tests
----- -----

View File

@ -0,0 +1,48 @@
.. _fastdepends-example:
FastDepends example
===================
.. meta::
:keywords: Python,Dependency Injection,FastDepends,Example
:description: This example demonstrates a usage of the FastDepends and Dependency Injector.
This example demonstrates how to use ``Dependency Injector`` with `FastDepends <https://github.com/Lancetnik/FastDepends>`_, a lightweight dependency injection framework inspired by FastAPI's dependency system, but without the web framework components.
Basic Usage
-----------
The integration between FastDepends and Dependency Injector is straightforward. Simply use Dependency Injector's ``Provide`` marker within FastDepends' ``Depends`` function:
.. code-block:: python
import sys
from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide
from fast_depends import Depends
class CoefficientService:
@staticmethod
def get_coefficient() -> float:
return 1.2
class Container(containers.DeclarativeContainer):
service = providers.Factory(CoefficientService)
@inject
def apply_coefficient(
a: int,
coefficient_provider: CoefficientService = Depends(Provide[Container.service]),
) -> float:
return a * coefficient_provider.get_coefficient()
container = Container()
container.wire(modules=[sys.modules[__name__]])
apply_coefficient(100) == 120.0

View File

@ -22,5 +22,6 @@ Explore the examples to see the ``Dependency Injector`` in action.
fastapi fastapi
fastapi-redis fastapi-redis
fastapi-sqlalchemy fastapi-sqlalchemy
fastdepends
.. disqus:: .. disqus::

View File

@ -31,7 +31,7 @@ Key features of the ``Dependency Injector``:
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:
.. code-block:: plain .. code-block:: text
Explicit is better than implicit Explicit is better than implicit

View File

@ -7,15 +7,39 @@ 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.48.1
------
* Improve performance of ``dependency_injector._cwiring.DependencyResolver``
* Add ``typing-extensions`` as a dependency for older Python versions (<3.11)
* Produce warning on ``@inject``s without ``Provide[...]`` marks
* Add support for `resource_type` in ``Lifespan``s
4.48.0
------
- Improve performance of wiring (`#897 <https://github.com/ets-labs/python-dependency-injector/pull/897>`_)
- Add Context Manager support to Resource provider (`#899 <https://github.com/ets-labs/python-dependency-injector/pull/899>`_)
- Add support for async generator injections (`#900 <https://github.com/ets-labs/python-dependency-injector/pull/900>`_)
- Fix unintended dependency on ``typing_extensions`` (`#902 <https://github.com/ets-labs/python-dependency-injector/pull/902>`_)
- Add support for Fast Depends (`#898 <https://github.com/ets-labs/python-dependency-injector/pull/898>`_)
- Add ``resource_type`` parameter to init and shutdown resources using specialized providers (`#858 <https://github.com/ets-labs/python-dependency-injector/pull/858>`_)
4.47.1
------
- Fix typing for wiring marker (`#892 <https://github.com/ets-labs/python-dependency-injector/pull/896>`_)
- Strip debug symbols in wheels
4.47.0 4.47.0
------- ------
- Add support for ``Annotated`` type for module and class attribute injection in wiring, - Add support for ``Annotated`` type for module and class attribute injection in wiring,
with updated documentation and examples. with updated documentation and examples.
See discussion: See discussion:
https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718 https://github.com/ets-labs/python-dependency-injector/pull/721#issuecomment-2025263718
- Fix ``root`` property shadowing in ``ConfigurationOption`` (`#875 https://github.com/ets-labs/python-dependency-injector/pull/875`_) - Fix ``root`` property shadowing in ``ConfigurationOption`` (`#875 <https://github.com/ets-labs/python-dependency-injector/pull/875>`_)
- Fix incorrect monkeypatching during ``wire()`` that could violate MRO in some classes (`#886 https://github.com/ets-labs/python-dependency-injector/pull/886`_) - Fix incorrect monkeypatching during ``wire()`` that could violate MRO in some classes (`#886 <https://github.com/ets-labs/python-dependency-injector/pull/886>`_)
- ABI3 wheels are now published for CPython. - ABI3 wheels are now published for CPython.
- Drop support of Python 3.7. - Drop support of Python 3.7.

View File

@ -61,11 +61,12 @@ When you call ``.shutdown()`` method on a resource provider, it will remove the
if any, and switch to uninitialized state. Some of resource initializer types support specifying custom if any, and switch to uninitialized state. Some of resource initializer types support specifying custom
resource shutdown. resource shutdown.
Resource provider supports 3 types of initializers: Resource provider supports 4 types of initializers:
- Function - Function
- Generator - Context Manager
- Subclass of ``resources.Resource`` - Generator (legacy)
- Subclass of ``resources.Resource`` (legacy)
Function initializer Function initializer
-------------------- --------------------
@ -103,8 +104,44 @@ you configure global resource:
Function initializer does not provide a way to specify custom resource shutdown. Function initializer does not provide a way to specify custom resource shutdown.
Generator initializer Context Manager initializer
--------------------- ---------------------------
This is an extension to the Function initializer. Resource provider automatically detects if the initializer returns a
context manager and uses it to manage the resource lifecycle.
.. code-block:: python
from dependency_injector import containers, providers
class DatabaseConnection:
def __init__(self, host, port, user, password):
self.host = host
self.port = port
self.user = user
self.password = password
def __enter__(self):
print(f"Connecting to {self.host}:{self.port} as {self.user}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("Closing connection")
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
db = providers.Resource(
DatabaseConnection,
host=config.db.host,
port=config.db.port,
user=config.db.user,
password=config.db.password,
)
Generator initializer (legacy)
------------------------------
Resource provider can use 2-step generators: Resource provider can use 2-step generators:
@ -154,8 +191,13 @@ object is not mandatory. You can leave ``yield`` statement empty:
argument2=..., argument2=...,
) )
Subclass initializer .. note::
--------------------
Generator initializers are automatically wrapped with ``contextmanager`` or ``asynccontextmanager`` decorator when
provided to a ``Resource`` provider.
Subclass initializer (legacy)
-----------------------------
You can create resource initializer by implementing a subclass of the ``resources.Resource``: You can create resource initializer by implementing a subclass of the ``resources.Resource``:
@ -210,6 +252,72 @@ first argument.
.. _resource-provider-wiring-closing: .. _resource-provider-wiring-closing:
Scoping Resources using specialized subclasses
----------------------------------------------
You can use specialized subclasses of ``Resource`` provider to initialize and shutdown resources by type.
Allowing for example to only initialize a subgroup of resources.
.. code-block:: python
class ScopedResource(resources.Resource):
pass
def init_service(name) -> Service:
print(f"Init {name}")
yield Service()
print(f"Shutdown {name}")
class Container(containers.DeclarativeContainer):
scoped = ScopedResource(
init_service,
"scoped",
)
generic = providers.Resource(
init_service,
"generic",
)
To initialize resources by type you can use ``init_resources(resource_type)`` and ``shutdown_resources(resource_type)``
methods adding the resource type as an argument:
.. code-block:: python
def main():
container = Container()
container.init_resources(ScopedResource)
# Generates:
# >>> Init scoped
container.shutdown_resources(ScopedResource)
# Generates:
# >>> Shutdown scoped
And to initialize all resources you can use ``init_resources()`` and ``shutdown_resources()`` without arguments:
.. code-block:: python
def main():
container = Container()
container.init_resources()
# Generates:
# >>> Init scoped
# >>> Init generic
container.shutdown_resources()
# Generates:
# >>> Shutdown scoped
# >>> Shutdown generic
It works using the ``traverse()`` method to find all resources of the specified type, selecting all resources
which are instances of the specified type.
Resources, wiring, and per-function execution scope Resources, wiring, and per-function execution scope
--------------------------------------------------- ---------------------------------------------------
@ -263,10 +371,11 @@ Asynchronous function initializer:
argument2=..., argument2=...,
) )
Asynchronous generator initializer: Asynchronous Context Manager initializer:
.. code-block:: python .. code-block:: python
@asynccontextmanager
async def init_async_resource(argument1=..., argument2=...): async def init_async_resource(argument1=..., argument2=...):
connection = await connect() connection = await connect()
yield connection yield connection
@ -358,5 +467,54 @@ See also:
- Wiring :ref:`async-injections-wiring` - Wiring :ref:`async-injections-wiring`
- :ref:`fastapi-redis-example` - :ref:`fastapi-redis-example`
ASGI Lifespan Protocol Support
------------------------------
The :mod:`dependency_injector.ext.starlette` module provides a :class:`~dependency_injector.ext.starlette.Lifespan`
class that integrates resource providers with ASGI applications using the `Lifespan Protocol`_. This allows resources to
be automatically initialized at application startup and properly shut down when the application stops.
.. code-block:: python
from contextlib import asynccontextmanager
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
from dependency_injector.ext.starlette import Lifespan
from fastapi import FastAPI, Request, Depends, APIRouter
class Connection: ...
@asynccontextmanager
async def init_database():
print("opening database connection")
yield Connection()
print("closing database connection")
router = APIRouter()
@router.get("/")
@inject
async def index(request: Request, db: Connection = Depends(Provide["db"])):
# use the database connection here
return "OK!"
class Container(containers.DeclarativeContainer):
__self__ = providers.Self()
db = providers.Resource(init_database)
lifespan = providers.Singleton(Lifespan, __self__)
app = providers.Singleton(FastAPI, lifespan=lifespan)
_include_router = providers.Resource(
app.provided.include_router.call(),
router,
)
if __name__ == "__main__":
import uvicorn
container = Container()
app = container.app()
uvicorn.run(app, host="localhost", port=8000)
.. _Lifespan Protocol: https://asgi.readthedocs.io/en/latest/specs/lifespan.html
.. disqus:: .. disqus::

View File

@ -257,7 +257,7 @@ Let's check that it works. Open another terminal session and use ``httpie``:
You should see: You should see:
.. code-block:: json .. code-block:: http
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Length: 844 Content-Length: 844
@ -596,7 +596,7 @@ and make a request to the API in the terminal:
You should see: You should see:
.. code-block:: json .. code-block:: http
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Length: 492 Content-Length: 492

View File

@ -84,7 +84,7 @@ Create next structure in the project root directory. All files are empty. That's
Initial project layout: Initial project layout:
.. code-block:: bash .. code-block:: text
./ ./
├── movies/ ├── movies/
@ -109,7 +109,7 @@ Now it's time to install the project requirements. We will use next packages:
Put next lines into the ``requirements.txt`` file: Put next lines into the ``requirements.txt`` file:
.. code-block:: bash .. code-block:: text
dependency-injector dependency-injector
pyyaml pyyaml
@ -134,7 +134,7 @@ We will create a script that creates database files.
First add the folder ``data/`` in the root of the project and then add the file First add the folder ``data/`` in the root of the project and then add the file
``fixtures.py`` inside of it: ``fixtures.py`` inside of it:
.. code-block:: bash .. code-block:: text
:emphasize-lines: 2-3 :emphasize-lines: 2-3
./ ./
@ -205,13 +205,13 @@ Now run in the terminal:
You should see: You should see:
.. code-block:: bash .. code-block:: text
OK OK
Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder: Check that files ``movies.csv`` and ``movies.db`` have appeared in the ``data/`` folder:
.. code-block:: bash .. code-block:: text
:emphasize-lines: 4-5 :emphasize-lines: 4-5
./ ./
@ -289,7 +289,7 @@ After each step we will add the provider to the container.
Create the ``entities.py`` in the ``movies`` package: Create the ``entities.py`` in the ``movies`` package:
.. code-block:: bash .. code-block:: text
:emphasize-lines: 10 :emphasize-lines: 10
./ ./
@ -356,7 +356,7 @@ Let's move on to the finders.
Create the ``finders.py`` in the ``movies`` package: Create the ``finders.py`` in the ``movies`` package:
.. code-block:: bash .. code-block:: text
:emphasize-lines: 11 :emphasize-lines: 11
./ ./
@ -465,7 +465,7 @@ The configuration file is ready. Move on to the lister.
Create the ``listers.py`` in the ``movies`` package: Create the ``listers.py`` in the ``movies`` package:
.. code-block:: bash .. code-block:: text
:emphasize-lines: 12 :emphasize-lines: 12
./ ./
@ -613,7 +613,7 @@ Run in the terminal:
You should see: You should see:
.. code-block:: plain .. code-block:: text
Francis Lawrence movies: Francis Lawrence movies:
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence') - Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
@ -752,7 +752,7 @@ Run in the terminal:
You should see: You should see:
.. code-block:: plain .. code-block:: text
Francis Lawrence movies: Francis Lawrence movies:
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence') - Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
@ -868,7 +868,7 @@ Run in the terminal line by line:
The output should be similar for each command: The output should be similar for each command:
.. code-block:: plain .. code-block:: text
Francis Lawrence movies: Francis Lawrence movies:
- Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence') - Movie(title='The Hunger Games: Mockingjay - Part 2', year=2015, director='Francis Lawrence')
@ -888,7 +888,7 @@ We will use `pytest <https://docs.pytest.org/en/stable/>`_ and
Create ``tests.py`` in the ``movies`` package: Create ``tests.py`` in the ``movies`` package:
.. code-block:: bash .. code-block:: text
:emphasize-lines: 13 :emphasize-lines: 13
./ ./
@ -977,7 +977,7 @@ Run in the terminal:
You should see: You should see:
.. code-block:: .. code-block:: text
platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 platform darwin -- Python 3.10.0, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
plugins: cov-3.0.0 plugins: cov-3.0.0

View File

@ -280,7 +280,7 @@ Now let's fill in the layout.
Put next into the ``base.html``: Put next into the ``base.html``:
.. code-block:: html .. code-block:: jinja
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@ -313,7 +313,7 @@ And put something to the index page.
Put next into the ``index.html``: Put next into the ``index.html``:
.. code-block:: html .. code-block:: jinja
{% extends "base.html" %} {% extends "base.html" %}

View File

@ -127,6 +127,7 @@ To inject the provider itself use ``Provide[foo.provider]``:
def foo(bar_provider: Factory[Bar] = Provide[Container.bar.provider]): def foo(bar_provider: Factory[Bar] = Provide[Container.bar.provider]):
bar = bar_provider(argument="baz") bar = bar_provider(argument="baz")
... ...
You can also use ``Provider[foo]`` for injecting the provider itself: You can also use ``Provider[foo]`` for injecting the provider itself:
.. code-block:: python .. code-block:: python
@ -631,6 +632,36 @@ or with a single container ``register_loader_containers(container)`` multiple ti
To unregister a container use ``unregister_loader_containers(container)``. To unregister a container use ``unregister_loader_containers(container)``.
Wiring module will uninstall the import hook when unregister last container. Wiring module will uninstall the import hook when unregister last container.
Few notes on performance
------------------------
``.wire()`` utilize caching to speed up the wiring process. At the end it clears the cache to avoid memory leaks.
But this may not always be desirable, when you want to keep the cache for the next wiring
(e.g. due to usage of multiple containers or during unit tests).
To keep the cache after wiring, you can set flag ``keep_cache=True`` (works with ``WiringConfiguration`` too):
.. code-block:: python
container1.wire(
modules=["yourapp.module1", "yourapp.module2"],
keep_cache=True,
)
container2.wire(
modules=["yourapp.module2", "yourapp.module3"],
keep_cache=True,
)
...
and then clear it manually when you need it:
.. code-block:: python
from dependency_injector.wiring import clear_cache
clear_cache()
Integration with other frameworks Integration with other frameworks
--------------------------------- ---------------------------------
@ -662,5 +693,6 @@ Take a look at other application examples:
- :ref:`fastapi-example` - :ref:`fastapi-example`
- :ref:`fastapi-redis-example` - :ref:`fastapi-redis-example`
- :ref:`fastapi-sqlalchemy-example` - :ref:`fastapi-sqlalchemy-example`
- :ref:`fastdepends-example`
.. disqus:: .. disqus::

View File

@ -18,10 +18,9 @@ SQLITE_FILE = DIR / "movies.db"
def create_csv(movies_data, path): def create_csv(movies_data, path):
with open(path, "w") as opened_file: with open(path, "w", newline="") as opened_file:
writer = csv.writer(opened_file) writer = csv.writer(opened_file)
for row in movies_data: writer.writerows(movies_data)
writer.writerow(row)
def create_sqlite(movies_data, path): def create_sqlite(movies_data, path):

View File

@ -29,7 +29,7 @@ class CsvMovieFinder(MovieFinder):
super().__init__(movie_factory) super().__init__(movie_factory)
def find_all(self) -> List[Movie]: def find_all(self) -> List[Movie]:
with open(self._csv_file_path) as csv_file: with open(self._csv_file_path, newline="") as csv_file:
csv_reader = csv.reader(csv_file, delimiter=self._delimiter) csv_reader = csv.reader(csv_file, delimiter=self._delimiter)
return [self._movie_factory(*row) for row in csv_reader] return [self._movie_factory(*row) for row in csv_reader]

View File

@ -3,10 +3,12 @@
import sys import sys
import logging import logging
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from contextlib import contextmanager
from dependency_injector import containers, providers from dependency_injector import containers, providers
@contextmanager
def init_thread_pool(max_workers: int): def init_thread_pool(max_workers: int):
thread_pool = ThreadPoolExecutor(max_workers=max_workers) thread_pool = ThreadPoolExecutor(max_workers=max_workers)
yield thread_pool yield thread_pool

View File

@ -52,6 +52,11 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Python Modules",
] ]
dynamic = ["version"] dynamic = ["version"]
dependencies = [
# typing.Annotated since v3.9
# typing.Self since v3.11
"typing-extensions; python_version<'3.11'",
]
[project.optional-dependencies] [project.optional-dependencies]
yaml = ["pyyaml"] yaml = ["pyyaml"]
@ -91,6 +96,7 @@ show_missing = true
[tool.isort] [tool.isort]
profile = "black" profile = "black"
combine_as_imports = true
[tool.pylint.main] [tool.pylint.main]
ignore = ["tests"] ignore = ["tests"]
@ -107,6 +113,7 @@ markers = [
"pydantic: Tests with Pydantic as a dependency", "pydantic: Tests with Pydantic as a dependency",
] ]
filterwarnings = [ filterwarnings = [
"ignore::dependency_injector.wiring.DIWiringWarning",
"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:Please use \\`.*?\\` from the \\`scipy.*?\\`(.*?)namespace is deprecated\\.:DeprecationWarning",

View File

@ -20,5 +20,6 @@ scipy
boto3 boto3
mypy_boto3_s3 mypy_boto3_s3
typing_extensions typing_extensions
fast-depends
-r requirements-ext.txt -r requirements-ext.txt

View File

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

View File

@ -1,23 +1,18 @@
from typing import Any, Awaitable, Callable, Dict, Tuple, TypeVar from typing import Any, Dict
from .providers import Provider from .providers import Provider
T = TypeVar("T") class DependencyResolver:
def __init__(
self,
kwargs: Dict[str, Any],
injections: Dict[str, Provider[Any]],
closings: Dict[str, Provider[Any]],
/,
) -> None: ...
def __enter__(self) -> Dict[str, Any]: ...
def __exit__(self, *exc_info: Any) -> None: ...
async def __aenter__(self) -> Dict[str, Any]: ...
async def __aexit__(self, *exc_info: Any) -> None: ...
def _sync_inject(
fn: Callable[..., T],
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
injections: Dict[str, Provider[Any]],
closings: Dict[str, Provider[Any]],
/,
) -> T: ...
async def _async_inject(
fn: Callable[..., Awaitable[T]],
args: Tuple[Any, ...],
kwargs: Dict[str, Any],
injections: Dict[str, Provider[Any]],
closings: Dict[str, Provider[Any]],
/,
) -> T: ...
def _isawaitable(instance: Any) -> bool: ... def _isawaitable(instance: Any) -> bool: ...

View File

@ -1,83 +1,93 @@
"""Wiring optimizations module.""" """Wiring optimizations module."""
import asyncio from asyncio import gather
import collections.abc from collections.abc import Awaitable
import inspect from inspect import CO_ITERABLE_COROUTINE
import types from types import CoroutineType, GeneratorType
from .wiring import _Marker
from .providers cimport Provider, Resource from .providers cimport Provider, Resource
from .wiring import _Marker
def _sync_inject(object fn, tuple args, dict kwargs, dict injections, dict closings, /): cdef inline bint _is_injectable(dict kwargs, object name):
cdef object result return name not in kwargs or isinstance(kwargs[name], _Marker)
cdef class DependencyResolver:
cdef dict kwargs
cdef dict to_inject cdef dict to_inject
cdef object arg_key cdef dict injections
cdef dict closings
def __init__(self, dict kwargs, dict injections, dict closings, /):
self.kwargs = kwargs
self.to_inject = kwargs.copy()
self.injections = injections
self.closings = closings
async def _await_injection(self, name: str, value: object, /) -> None:
self.to_inject[name] = await value
cdef void _handle_injections_sync(self):
cdef Provider provider cdef Provider provider
to_inject = kwargs.copy() for name, provider in self.injections.items():
for arg_key, provider in injections.items(): if _is_injectable(self.kwargs, name):
if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): self.to_inject[name] = provider()
to_inject[arg_key] = provider()
result = fn(*args, **to_inject) cdef list _handle_injections_async(self):
cdef list to_await = []
cdef Provider provider
if closings: for name, provider in self.injections.items():
for arg_key, provider in closings.items(): if _is_injectable(self.kwargs, name):
if arg_key in kwargs and not isinstance(kwargs[arg_key], _Marker): provide = provider()
continue
if not isinstance(provider, Resource): if provider.is_async_mode_enabled() or _isawaitable(provide):
continue to_await.append(self._await_injection(name, provide))
else:
self.to_inject[name] = provide
return to_await
cdef void _handle_closings_sync(self):
cdef Provider provider
for name, provider in self.closings.items():
if _is_injectable(self.kwargs, name) and isinstance(provider, Resource):
provider.shutdown() provider.shutdown()
return result cdef list _handle_closings_async(self):
cdef list to_await = []
async def _async_inject(object fn, tuple args, dict kwargs, dict injections, dict closings, /):
cdef object result
cdef dict to_inject
cdef list to_inject_await = []
cdef list to_close_await = []
cdef object arg_key
cdef Provider provider cdef Provider provider
to_inject = kwargs.copy() for name, provider in self.closings.items():
for arg_key, provider in injections.items(): if _is_injectable(self.kwargs, name) and isinstance(provider, Resource):
if arg_key not in kwargs or isinstance(kwargs[arg_key], _Marker): if _isawaitable(shutdown := provider.shutdown()):
provide = provider() to_await.append(shutdown)
if provider.is_async_mode_enabled():
to_inject_await.append((arg_key, provide))
elif _isawaitable(provide):
to_inject_await.append((arg_key, provide))
else:
to_inject[arg_key] = provide
if to_inject_await: return to_await
async_to_inject = await asyncio.gather(*(provide for _, provide in to_inject_await))
for provide, (injection, _) in zip(async_to_inject, to_inject_await):
to_inject[injection] = provide
result = await fn(*args, **to_inject) def __enter__(self):
self._handle_injections_sync()
return self.to_inject
if closings: def __exit__(self, *_):
for arg_key, provider in closings.items(): self._handle_closings_sync()
if arg_key in kwargs and isinstance(kwargs[arg_key], _Marker):
continue
if not isinstance(provider, Resource):
continue
shutdown = provider.shutdown()
if _isawaitable(shutdown):
to_close_await.append(shutdown)
await asyncio.gather(*to_close_await) async def __aenter__(self):
if to_await := self._handle_injections_async():
await gather(*to_await)
return self.to_inject
return result async def __aexit__(self, *_):
if to_await := self._handle_closings_async():
await gather(*to_await)
cdef bint _isawaitable(object instance): cdef bint _isawaitable(object instance):
"""Return true if object can be passed to an ``await`` expression.""" """Return true if object can be passed to an ``await`` expression."""
return (isinstance(instance, types.CoroutineType) or return (isinstance(instance, CoroutineType) or
isinstance(instance, types.GeneratorType) and isinstance(instance, GeneratorType) and
bool(instance.gi_code.co_flags & inspect.CO_ITERABLE_COROUTINE) or bool(instance.gi_code.co_flags & CO_ITERABLE_COROUTINE) or
isinstance(instance, collections.abc.Awaitable)) isinstance(instance, Awaitable))

View File

@ -1,23 +1,28 @@
from pathlib import Path from pathlib import Path
from typing import ( from typing import (
Generic,
Type,
Dict,
List,
Tuple,
Optional,
Any, Any,
Union, Awaitable,
ClassVar,
Callable as _Callable, Callable as _Callable,
ClassVar,
Dict,
Generic,
Iterable, Iterable,
Iterator, Iterator,
List,
Optional,
Tuple,
Type,
TypeVar, TypeVar,
Awaitable, Union,
overload, overload,
) )
from .providers import Provider, Self, ProviderParent try:
from typing import Self as _Self
except ImportError:
from typing_extensions import Self as _Self
from .providers import Provider, Resource, Self, ProviderParent
C_Base = TypeVar("C_Base", bound="Container") C_Base = TypeVar("C_Base", bound="Container")
C = TypeVar("C", bound="DeclarativeContainer") C = TypeVar("C", bound="DeclarativeContainer")
@ -30,32 +35,34 @@ class WiringConfiguration:
packages: List[Any] packages: List[Any]
from_package: Optional[str] from_package: Optional[str]
auto_wire: bool auto_wire: bool
keep_cache: bool
def __init__( def __init__(
self, self,
modules: Optional[Iterable[Any]] = None, modules: Optional[Iterable[Any]] = None,
packages: Optional[Iterable[Any]] = None, packages: Optional[Iterable[Any]] = None,
from_package: Optional[str] = None, from_package: Optional[str] = None,
auto_wire: bool = True, auto_wire: bool = True,
keep_cache: bool = False,
) -> None: ... ) -> None: ...
class Container: class Container:
provider_type: Type[Provider] = Provider provider_type: Type[Provider[Any]] = Provider
providers: Dict[str, Provider] providers: Dict[str, Provider[Any]]
dependencies: Dict[str, Provider[Any]] dependencies: Dict[str, Provider[Any]]
overridden: Tuple[Provider] overridden: Tuple[Provider[Any], ...]
wiring_config: WiringConfiguration wiring_config: WiringConfiguration
auto_load_config: bool = True auto_load_config: bool = True
__self__: Self __self__: Self
def __init__(self) -> None: ... def __init__(self) -> None: ...
def __deepcopy__(self, memo: Optional[Dict[str, Any]]) -> Provider: ... def __deepcopy__(self, memo: Optional[Dict[str, Any]]) -> _Self: ...
def __setattr__(self, name: str, value: Union[Provider, Any]) -> None: ... def __setattr__(self, name: str, value: Union[Provider[Any], Any]) -> None: ...
def __getattr__(self, name: str) -> Provider: ... def __getattr__(self, name: str) -> Provider[Any]: ...
def __delattr__(self, name: str) -> None: ... def __delattr__(self, name: str) -> None: ...
def set_providers(self, **providers: Provider): ... def set_providers(self, **providers: Provider[Any]) -> None: ...
def set_provider(self, name: str, provider: Provider) -> None: ... def set_provider(self, name: str, provider: Provider[Any]) -> None: ...
def override(self, overriding: Union[Container, Type[Container]]) -> None: ... def override(self, overriding: Union[Container, Type[Container]]) -> None: ...
def override_providers( def override_providers(
self, **overriding_providers: Union[Provider, Any] self, **overriding_providers: Union[Provider[Any], Any]
) -> ProvidersOverridingContext[C_Base]: ... ) -> ProvidersOverridingContext[C_Base]: ...
def reset_last_overriding(self) -> None: ... def reset_last_overriding(self) -> None: ...
def reset_override(self) -> None: ... def reset_override(self) -> None: ...
@ -67,8 +74,8 @@ class Container:
from_package: Optional[str] = None, from_package: Optional[str] = None,
) -> None: ... ) -> None: ...
def unwire(self) -> None: ... def unwire(self) -> None: ...
def init_resources(self) -> Optional[Awaitable]: ... def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...
def shutdown_resources(self) -> Optional[Awaitable]: ... def shutdown_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ...
def load_config(self) -> None: ... def load_config(self) -> None: ...
def apply_container_providers_overridings(self) -> None: ... def apply_container_providers_overridings(self) -> None: ...
def reset_singletons(self) -> SingletonResetContext[C_Base]: ... def reset_singletons(self) -> SingletonResetContext[C_Base]: ...
@ -79,10 +86,10 @@ class Container:
) -> None: ... ) -> None: ...
def from_json_schema(self, filepath: Union[Path, str]) -> None: ... def from_json_schema(self, filepath: Union[Path, str]) -> None: ...
@overload @overload
def resolve_provider_name(self, provider: Provider) -> str: ... def resolve_provider_name(self, provider: Provider[Any]) -> str: ...
@classmethod @classmethod
@overload @overload
def resolve_provider_name(cls, provider: Provider) -> str: ... def resolve_provider_name(cls, provider: Provider[Any]) -> str: ...
@property @property
def parent(self) -> Optional[ProviderParent]: ... def parent(self) -> Optional[ProviderParent]: ...
@property @property
@ -97,14 +104,14 @@ class Container:
class DynamicContainer(Container): ... class DynamicContainer(Container): ...
class DeclarativeContainer(Container): class DeclarativeContainer(Container):
cls_providers: ClassVar[Dict[str, Provider]] cls_providers: ClassVar[Dict[str, Provider[Any]]]
inherited_providers: ClassVar[Dict[str, Provider]] inherited_providers: ClassVar[Dict[str, Provider[Any]]]
def __init__(self, **overriding_providers: Union[Provider, Any]) -> None: ... def __init__(self, **overriding_providers: Union[Provider[Any], Any]) -> None: ...
@classmethod @classmethod
def override(cls, overriding: Union[Container, Type[Container]]) -> None: ... def override(cls, overriding: Union[Container, Type[Container]]) -> None: ...
@classmethod @classmethod
def override_providers( def override_providers(
cls, **overriding_providers: Union[Provider, Any] cls, **overriding_providers: Union[Provider[Any], Any]
) -> ProvidersOverridingContext[C_Base]: ... ) -> ProvidersOverridingContext[C_Base]: ...
@classmethod @classmethod
def reset_last_overriding(cls) -> None: ... def reset_last_overriding(cls) -> None: ...
@ -113,7 +120,7 @@ class DeclarativeContainer(Container):
class ProvidersOverridingContext(Generic[T]): class ProvidersOverridingContext(Generic[T]):
def __init__( def __init__(
self, container: T, overridden_providers: Iterable[Union[Provider, Any]] self, container: T, overridden_providers: Iterable[Union[Provider[Any], Any]]
) -> None: ... ) -> None: ...
def __enter__(self) -> T: ... def __enter__(self) -> T: ...
def __exit__(self, *_: Any) -> None: ... def __exit__(self, *_: Any) -> None: ...

View File

@ -20,14 +20,15 @@ from .wiring import wire, unwire
class WiringConfiguration: class WiringConfiguration:
"""Container wiring configuration.""" """Container wiring configuration."""
def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True): def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True, keep_cache=False):
self.modules = [*modules] if modules else [] self.modules = [*modules] if modules else []
self.packages = [*packages] if packages else [] self.packages = [*packages] if packages else []
self.from_package = from_package self.from_package = from_package
self.auto_wire = auto_wire self.auto_wire = auto_wire
self.keep_cache = keep_cache
def __deepcopy__(self, memo=None): def __deepcopy__(self, memo=None):
return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire) return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire, self.keep_cache)
class Container: class Container:
@ -258,7 +259,7 @@ class DynamicContainer(Container):
"""Check if auto wiring is needed.""" """Check if auto wiring is needed."""
return self.wiring_config.auto_wire is True return self.wiring_config.auto_wire is True
def wire(self, modules=None, packages=None, from_package=None): def wire(self, modules=None, packages=None, from_package=None, keep_cache=None):
"""Wire container providers with provided packages and modules. """Wire container providers with provided packages and modules.
:rtype: None :rtype: None
@ -289,10 +290,14 @@ class DynamicContainer(Container):
if not modules and not packages: if not modules and not packages:
return return
if keep_cache is None:
keep_cache = self.wiring_config.keep_cache
wire( wire(
container=self, container=self,
modules=modules, modules=modules,
packages=packages, packages=packages,
keep_cache=keep_cache,
) )
if modules: if modules:
@ -310,11 +315,15 @@ class DynamicContainer(Container):
self.wired_to_modules.clear() self.wired_to_modules.clear()
self.wired_to_packages.clear() self.wired_to_packages.clear()
def init_resources(self): def init_resources(self, resource_type=providers.Resource):
"""Initialize all container resources.""" """Initialize all container resources."""
if not issubclass(resource_type, providers.Resource):
raise TypeError("resource_type must be a subclass of Resource provider")
futures = [] futures = []
for provider in self.traverse(types=[providers.Resource]): for provider in self.traverse(types=[resource_type]):
resource = provider.init() resource = provider.init()
if __is_future_or_coroutine(resource): if __is_future_or_coroutine(resource):
@ -323,8 +332,12 @@ class DynamicContainer(Container):
if futures: if futures:
return asyncio.gather(*futures) return asyncio.gather(*futures)
def shutdown_resources(self): def shutdown_resources(self, resource_type=providers.Resource):
"""Shutdown all container resources.""" """Shutdown all container resources."""
if not issubclass(resource_type, providers.Resource):
raise TypeError("resource_type must be a subclass of Resource provider")
def _independent_resources(resources): def _independent_resources(resources):
for resource in resources: for resource in resources:
for other_resource in resources: for other_resource in resources:
@ -355,7 +368,7 @@ class DynamicContainer(Container):
for resource in resources_to_shutdown: for resource in resources_to_shutdown:
resource.shutdown() resource.shutdown()
resources = list(self.traverse(types=[providers.Resource])) resources = list(self.traverse(types=[resource_type]))
if any(resource.is_async_mode_enabled() for resource in resources): if any(resource.is_async_mode_enabled() for resource in resources):
return _async_ordered_shutdown(resources) return _async_ordered_shutdown(resources)
else: else:

View File

@ -7,7 +7,6 @@ import warnings
from dependency_injector import providers from dependency_injector import providers
warnings.warn( warnings.warn(
'Module "dependency_injector.ext.aiohttp" is deprecated since ' 'Module "dependency_injector.ext.aiohttp" is deprecated since '
'version 4.0.0. Use "dependency_injector.wiring" module instead.', 'version 4.0.0. Use "dependency_injector.wiring" module instead.',

View File

@ -1,14 +1,16 @@
from typing import Awaitable as _Awaitable from typing import Any, Awaitable as _Awaitable, TypeVar
from dependency_injector import providers from dependency_injector import providers
class Application(providers.Singleton): ... T = TypeVar("T")
class Extension(providers.Singleton): ...
class Middleware(providers.DelegatedCallable): ...
class MiddlewareFactory(providers.Factory): ...
class View(providers.Callable): class Application(providers.Singleton[T]): ...
def as_view(self) -> _Awaitable: ... class Extension(providers.Singleton[T]): ...
class Middleware(providers.DelegatedCallable[T]): ...
class MiddlewareFactory(providers.Factory[T]): ...
class ClassBasedView(providers.Factory): class View(providers.Callable[T]):
def as_view(self) -> _Awaitable: ... def as_view(self) -> _Awaitable[T]: ...
class ClassBasedView(providers.Factory[T]):
def as_view(self) -> _Awaitable[T]: ...

View File

@ -1,12 +1,12 @@
"""Flask extension module.""" """Flask extension module."""
from __future__ import absolute_import from __future__ import absolute_import
import warnings import warnings
from flask import request as flask_request from flask import request as flask_request
from dependency_injector import providers, errors from dependency_injector import errors, providers
warnings.warn( warnings.warn(
'Module "dependency_injector.ext.flask" is deprecated since ' 'Module "dependency_injector.ext.flask" is deprecated since '

View File

@ -1,19 +1,21 @@
from typing import Union, Optional, Callable as _Callable, Any from typing import Any, Callable as _Callable, Optional, TypeVar, Union
from flask.wrappers import Request
from flask import request as flask_request
from dependency_injector import providers from dependency_injector import providers
request: providers.Object[flask_request] request: providers.Object[Request]
T = TypeVar("T")
class Application(providers.Singleton): ... class Application(providers.Singleton[T]): ...
class Extension(providers.Singleton): ... class Extension(providers.Singleton[T]): ...
class View(providers.Callable): class View(providers.Callable[T]):
def as_view(self) -> _Callable[..., Any]: ... def as_view(self) -> _Callable[..., T]: ...
class ClassBasedView(providers.Factory): class ClassBasedView(providers.Factory[T]):
def as_view(self, name: str) -> _Callable[..., Any]: ... def as_view(self, name: str) -> _Callable[..., T]: ...
def as_view( def as_view(
provider: Union[View, ClassBasedView], name: Optional[str] = None provider: Union[View[T], ClassBasedView[T]], name: Optional[str] = None
) -> _Callable[..., Any]: ... ) -> _Callable[..., T]: ...

View File

@ -1,5 +1,5 @@
import sys import sys
from typing import Any from typing import Any, Type
if sys.version_info >= (3, 11): # pragma: no cover if sys.version_info >= (3, 11): # pragma: no cover
from typing import Self from typing import Self
@ -7,6 +7,7 @@ else: # pragma: no cover
from typing_extensions import Self from typing_extensions import Self
from dependency_injector.containers import Container from dependency_injector.containers import Container
from dependency_injector.providers import Resource
class Lifespan: class Lifespan:
@ -29,24 +30,32 @@ class Lifespan:
app = Factory(Starlette, lifespan=lifespan) app = Factory(Starlette, lifespan=lifespan)
:param container: container instance :param container: container instance
:param resource_type: A :py:class:`~dependency_injector.resources.Resource`
subclass. Limits the resources to be initialized and shutdown.
""" """
container: Container container: Container
resource_type: Type[Resource[Any]]
def __init__(self, container: Container) -> None: def __init__(
self,
container: Container,
resource_type: Type[Resource[Any]] = Resource,
) -> None:
self.container = container self.container = container
self.resource_type = resource_type
def __call__(self, app: Any) -> Self: def __call__(self, app: Any) -> Self:
return self return self
async def __aenter__(self) -> None: async def __aenter__(self) -> None:
result = self.container.init_resources() result = self.container.init_resources(self.resource_type)
if result is not None: if result is not None:
await result await result
async def __aexit__(self, *exc_info: Any) -> None: async def __aexit__(self, *exc_info: Any) -> None:
result = self.container.shutdown_resources() result = self.container.shutdown_resources(self.resource_type)
if result is not None: if result is not None:
await result await result

View File

@ -697,3 +697,10 @@ cdef inline object __future_result(object instance):
future_result = asyncio.Future() future_result = asyncio.Future()
future_result.set_result(instance) future_result.set_result(instance)
return future_result return future_result
cdef class NullAwaitable:
pass
cdef NullAwaitable NULL_AWAITABLE

View File

@ -15,8 +15,11 @@ import re
import sys import sys
import threading import threading
import warnings import warnings
from asyncio import ensure_future
from configparser import ConfigParser as IniConfigParser from configparser import ConfigParser as IniConfigParser
from contextlib import asynccontextmanager, contextmanager
from contextvars import ContextVar from contextvars import ContextVar
from inspect import isasyncgenfunction, isgeneratorfunction
try: try:
from inspect import _is_coroutine_mark as _is_coroutine_marker from inspect import _is_coroutine_mark as _is_coroutine_marker
@ -3598,6 +3601,17 @@ cdef class Dict(Provider):
return __provide_keyword_args(kwargs, self._kwargs, self._kwargs_len, self._async_mode) return __provide_keyword_args(kwargs, self._kwargs, self._kwargs_len, self._async_mode)
@cython.no_gc
cdef class NullAwaitable:
def __next__(self):
raise StopIteration from None
def __await__(self):
return self
cdef NullAwaitable NULL_AWAITABLE = NullAwaitable()
cdef class Resource(Provider): cdef class Resource(Provider):
"""Resource provider provides a component with initialization and shutdown.""" """Resource provider provides a component with initialization and shutdown."""
@ -3653,6 +3667,12 @@ cdef class Resource(Provider):
def set_provides(self, provides): def set_provides(self, provides):
"""Set provider provides.""" """Set provider provides."""
provides = _resolve_string_import(provides) provides = _resolve_string_import(provides)
if isasyncgenfunction(provides):
provides = asynccontextmanager(provides)
elif isgeneratorfunction(provides):
provides = contextmanager(provides)
self._provides = provides self._provides = provides
return self return self
@ -3753,28 +3773,21 @@ cdef class Resource(Provider):
"""Shutdown resource.""" """Shutdown resource."""
if not self._initialized: if not self._initialized:
if self._async_mode == ASYNC_MODE_ENABLED: if self._async_mode == ASYNC_MODE_ENABLED:
result = asyncio.Future() return NULL_AWAITABLE
result.set_result(None)
return result
return return
if self._shutdowner: if self._shutdowner:
try: future = self._shutdowner(None, None, None)
shutdown = self._shutdowner(self._resource)
except StopIteration: if __is_future_or_coroutine(future):
pass return ensure_future(self._shutdown_async(future))
else:
if inspect.isawaitable(shutdown):
return self._create_shutdown_future(shutdown)
self._resource = None self._resource = None
self._initialized = False self._initialized = False
self._shutdowner = None self._shutdowner = None
if self._async_mode == ASYNC_MODE_ENABLED: if self._async_mode == ASYNC_MODE_ENABLED:
result = asyncio.Future() return NULL_AWAITABLE
result.set_result(None)
return result
@property @property
def related(self): def related(self):
@ -3784,164 +3797,74 @@ cdef class Resource(Provider):
yield from filter(is_provider, self.kwargs.values()) yield from filter(is_provider, self.kwargs.values())
yield from super().related yield from super().related
cpdef object _provide(self, tuple args, dict kwargs): async def _shutdown_async(self, future) -> None:
if self._initialized:
return self._resource
if self._is_resource_subclass(self._provides):
initializer = self._provides()
self._resource = __call(
initializer.init,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
self._shutdowner = initializer.shutdown
elif self._is_async_resource_subclass(self._provides):
initializer = self._provides()
async_init = __call(
initializer.init,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
self._initialized = True
return self._create_init_future(async_init, initializer.shutdown)
elif inspect.isgeneratorfunction(self._provides):
initializer = __call(
self._provides,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
self._resource = next(initializer)
self._shutdowner = initializer.send
elif iscoroutinefunction(self._provides):
initializer = __call(
self._provides,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
self._initialized = True
return self._create_init_future(initializer)
elif isasyncgenfunction(self._provides):
initializer = __call(
self._provides,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
self._initialized = True
return self._create_async_gen_init_future(initializer)
elif callable(self._provides):
self._resource = __call(
self._provides,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
else:
raise Error("Unknown type of resource initializer")
self._initialized = True
return self._resource
def _create_init_future(self, future, shutdowner=None):
callback = self._async_init_callback
if shutdowner:
callback = functools.partial(callback, shutdowner=shutdowner)
future = asyncio.ensure_future(future)
future.add_done_callback(callback)
self._resource = future
return future
def _create_async_gen_init_future(self, initializer):
if inspect.isasyncgen(initializer):
return self._create_init_future(initializer.__anext__(), initializer.asend)
future = asyncio.Future()
create_initializer = asyncio.ensure_future(initializer)
create_initializer.add_done_callback(functools.partial(self._async_create_gen_callback, future))
self._resource = future
return future
def _async_init_callback(self, initializer, shutdowner=None):
try: try:
resource = initializer.result() await future
except Exception: finally:
self._initialized = False
else:
self._resource = resource
self._shutdowner = shutdowner
def _async_create_gen_callback(self, future, initializer_future):
initializer = initializer_future.result()
init_future = self._create_init_future(initializer.__anext__(), initializer.asend)
init_future.add_done_callback(functools.partial(self._async_trigger_result, future))
def _async_trigger_result(self, future, future_result):
future.set_result(future_result.result())
def _create_shutdown_future(self, shutdown_future):
future = asyncio.Future()
shutdown_future = asyncio.ensure_future(shutdown_future)
shutdown_future.add_done_callback(functools.partial(self._async_shutdown_callback, future))
return future
def _async_shutdown_callback(self, future_result, shutdowner):
try:
shutdowner.result()
except StopAsyncIteration:
pass
self._resource = None self._resource = None
self._initialized = False self._initialized = False
self._shutdowner = None self._shutdowner = None
future_result.set_result(None) async def _handle_async_cm(self, obj) -> None:
try:
self._resource = resource = await obj.__aenter__()
self._shutdowner = obj.__aexit__
return resource
except:
self._initialized = False
raise
@staticmethod async def _provide_async(self, future) -> None:
def _is_resource_subclass(instance): try:
if not isinstance(instance, type): obj = await future
return
from . import resources
return issubclass(instance, resources.Resource)
@staticmethod if hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'):
def _is_async_resource_subclass(instance): self._resource = await obj.__aenter__()
if not isinstance(instance, type): self._shutdowner = obj.__aexit__
return elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'):
from . import resources self._resource = obj.__enter__()
return issubclass(instance, resources.AsyncResource) self._shutdowner = obj.__exit__
else:
self._resource = obj
self._shutdowner = None
return self._resource
except:
self._initialized = False
raise
cpdef object _provide(self, tuple args, dict kwargs):
if self._initialized:
return self._resource
obj = __call(
self._provides,
args,
self._args,
self._args_len,
kwargs,
self._kwargs,
self._kwargs_len,
self._async_mode,
)
if __is_future_or_coroutine(obj):
self._initialized = True
self._resource = resource = ensure_future(self._provide_async(obj))
return resource
elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'):
self._resource = obj.__enter__()
self._shutdowner = obj.__exit__
elif hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'):
self._initialized = True
self._resource = resource = ensure_future(self._handle_async_cm(obj))
return resource
else:
self._resource = obj
self._shutdowner = None
self._initialized = True
return self._resource
cdef class Container(Provider): cdef class Container(Provider):
@ -4993,14 +4916,6 @@ def iscoroutinefunction(obj):
return False return False
def isasyncgenfunction(obj):
"""Check if object is an asynchronous generator function."""
try:
return inspect.isasyncgenfunction(obj)
except AttributeError:
return False
def _resolve_string_import(provides): def _resolve_string_import(provides):
if provides is None: if provides is None:
return provides return provides

View File

@ -1,23 +1,54 @@
"""Resources module.""" """Resources module."""
import abc from abc import ABCMeta, abstractmethod
from typing import TypeVar, Generic, Optional from typing import Any, ClassVar, Generic, Optional, Tuple, TypeVar
T = TypeVar("T") T = TypeVar("T")
class Resource(Generic[T], metaclass=abc.ABCMeta): class Resource(Generic[T], metaclass=ABCMeta):
__slots__: ClassVar[Tuple[str, ...]] = ("args", "kwargs", "obj")
@abc.abstractmethod obj: Optional[T]
def init(self, *args, **kwargs) -> Optional[T]: ...
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.args = args
self.kwargs = kwargs
self.obj = None
@abstractmethod
def init(self, *args: Any, **kwargs: Any) -> Optional[T]: ...
def shutdown(self, resource: Optional[T]) -> None: ... def shutdown(self, resource: Optional[T]) -> None: ...
def __enter__(self) -> Optional[T]:
self.obj = obj = self.init(*self.args, **self.kwargs)
return obj
class AsyncResource(Generic[T], metaclass=abc.ABCMeta): def __exit__(self, *exc_info: Any) -> None:
self.shutdown(self.obj)
self.obj = None
@abc.abstractmethod
async def init(self, *args, **kwargs) -> Optional[T]: ... class AsyncResource(Generic[T], metaclass=ABCMeta):
__slots__: ClassVar[Tuple[str, ...]] = ("args", "kwargs", "obj")
obj: Optional[T]
def __init__(self, *args: Any, **kwargs: Any) -> None:
self.args = args
self.kwargs = kwargs
self.obj = None
@abstractmethod
async def init(self, *args: Any, **kwargs: Any) -> Optional[T]: ...
async def shutdown(self, resource: Optional[T]) -> None: ... async def shutdown(self, resource: Optional[T]) -> None: ...
async def __aenter__(self) -> Optional[T]:
self.obj = obj = await self.init(*self.args, **self.kwargs)
return obj
async def __aexit__(self, *exc_info: Any) -> None:
await self.shutdown(self.obj)
self.obj = None

View File

@ -6,14 +6,18 @@ import importlib.machinery
import inspect import inspect
import pkgutil import pkgutil
import sys import sys
from contextlib import suppress
from inspect import isbuiltin, isclass
from types import ModuleType from types import ModuleType
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Any, Any,
AsyncIterator,
Callable, Callable,
Dict, Dict,
Iterable, Iterable,
Iterator, Iterator,
List,
Optional, Optional,
Protocol, Protocol,
Set, Set,
@ -23,9 +27,20 @@ from typing import (
Union, Union,
cast, cast,
) )
from warnings import warn
try:
from typing import Self
except ImportError:
from typing_extensions import Self from typing_extensions import Self
try:
from functools import cache
except ImportError:
from functools import lru_cache
cache = lru_cache(maxsize=None)
# Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/362 # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/362
if sys.version_info >= (3, 9): if sys.version_info >= (3, 9):
from types import GenericAlias from types import GenericAlias
@ -48,25 +63,48 @@ else:
return None return None
try: MARKER_EXTRACTORS: List[Callable[[Any], Any]] = []
import fastapi.params INSPECT_EXCLUSION_FILTERS: List[Callable[[Any], bool]] = [isbuiltin]
except ImportError:
fastapi = None with suppress(ImportError):
from fastapi.params import Depends as FastAPIDepends
def extract_marker_from_fastapi(param: Any) -> Any:
if isinstance(param, FastAPIDepends):
return param.dependency
return None
MARKER_EXTRACTORS.append(extract_marker_from_fastapi)
with suppress(ImportError):
from fast_depends.dependencies import Depends as FastDepends
def extract_marker_from_fast_depends(param: Any) -> Any:
if isinstance(param, FastDepends):
return param.dependency
return None
MARKER_EXTRACTORS.append(extract_marker_from_fast_depends)
try: with suppress(ImportError):
import starlette.requests from starlette.requests import Request as StarletteRequest
except ImportError:
starlette = None def is_starlette_request_cls(obj: Any) -> bool:
return isclass(obj) and _safe_is_subclass(obj, StarletteRequest)
INSPECT_EXCLUSION_FILTERS.append(is_starlette_request_cls)
try: with suppress(ImportError):
import werkzeug.local from werkzeug.local import LocalProxy as WerkzeugLocalProxy
except ImportError:
werkzeug = None
def is_werkzeug_local_proxy(obj: Any) -> bool:
return isinstance(obj, WerkzeugLocalProxy)
from . import providers INSPECT_EXCLUSION_FILTERS.append(is_werkzeug_local_proxy)
from . import providers # noqa: E402
__all__ = ( __all__ = (
"wire", "wire",
@ -97,6 +135,10 @@ else:
Container = Any Container = Any
class DIWiringWarning(RuntimeWarning):
"""Base class for all warnings raised by the wiring module."""
class PatchedRegistry: class PatchedRegistry:
def __init__(self) -> None: def __init__(self) -> None:
@ -378,37 +420,19 @@ class ProvidersMap:
return providers_map return providers_map
class InspectFilter: def is_excluded_from_inspect(obj: Any) -> bool:
for is_excluded in INSPECT_EXCLUSION_FILTERS:
def is_excluded(self, instance: object) -> bool: if is_excluded(obj):
if self._is_werkzeug_local_proxy(instance):
return True return True
elif self._is_starlette_request_cls(instance):
return True
elif self._is_builtin(instance):
return True
else:
return False return False
def _is_werkzeug_local_proxy(self, instance: object) -> bool:
return werkzeug and isinstance(instance, werkzeug.local.LocalProxy)
def _is_starlette_request_cls(self, instance: object) -> bool:
return (
starlette
and isinstance(instance, type)
and _safe_is_subclass(instance, starlette.requests.Request)
)
def _is_builtin(self, instance: object) -> bool:
return inspect.isbuiltin(instance)
def wire( # noqa: C901 def wire( # noqa: C901
container: Container, container: Container,
*, *,
modules: Optional[Iterable[ModuleType]] = None, modules: Optional[Iterable[ModuleType]] = None,
packages: Optional[Iterable[ModuleType]] = None, packages: Optional[Iterable[ModuleType]] = None,
keep_cache: bool = False,
) -> None: ) -> None:
"""Wire container providers with provided packages and modules.""" """Wire container providers with provided packages and modules."""
modules = [*modules] if modules else [] modules = [*modules] if modules else []
@ -421,7 +445,7 @@ def wire( # noqa: C901
for module in modules: for module in modules:
for member_name, member in _get_members_and_annotated(module): for member_name, member in _get_members_and_annotated(module):
if _inspect_filter.is_excluded(member): if is_excluded_from_inspect(member):
continue continue
if _is_marker(member): if _is_marker(member):
@ -449,6 +473,9 @@ def wire( # noqa: C901
for patched in _patched_registry.get_callables_from_module(module): for patched in _patched_registry.get_callables_from_module(module):
_bind_injections(patched, providers_map) _bind_injections(patched, providers_map)
if not keep_cache:
clear_cache()
def unwire( # noqa: C901 def unwire( # noqa: C901
*, *,
@ -483,6 +510,11 @@ def unwire( # noqa: C901
def inject(fn: F) -> F: 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)
if not reference_injections:
warn("@inject is not required here", DIWiringWarning, stacklevel=2)
return fn
patched = _get_patched(fn, reference_injections, reference_closing) patched = _get_patched(fn, reference_injections, reference_closing)
return cast(F, patched) return cast(F, patched)
@ -592,11 +624,10 @@ def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]:
else: else:
marker = parameter.default marker = parameter.default
if not isinstance(marker, _Marker) and not _is_fastapi_depends(marker): for marker_extractor in MARKER_EXTRACTORS:
return None if _marker := marker_extractor(marker):
marker = _marker
if _is_fastapi_depends(marker): break
marker = marker.dependency
if not isinstance(marker, _Marker): if not isinstance(marker, _Marker):
return None return None
@ -604,6 +635,7 @@ def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]:
return marker return marker
@cache
def _fetch_reference_injections( # noqa: C901 def _fetch_reference_injections( # noqa: C901
fn: Callable[..., Any], fn: Callable[..., Any],
) -> Tuple[Dict[str, Any], Dict[str, Any]]: ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
@ -708,6 +740,8 @@ def _get_patched(
if inspect.iscoroutinefunction(fn): if inspect.iscoroutinefunction(fn):
patched = _get_async_patched(fn, patched_object) patched = _get_async_patched(fn, patched_object)
elif inspect.isasyncgenfunction(fn):
patched = _get_async_gen_patched(fn, patched_object)
else: else:
patched = _get_sync_patched(fn, patched_object) patched = _get_sync_patched(fn, patched_object)
@ -717,10 +751,6 @@ def _get_patched(
return patched return patched
def _is_fastapi_depends(param: Any) -> bool:
return fastapi and isinstance(param, fastapi.params.Depends)
def _is_patched(fn) -> bool: def _is_patched(fn) -> bool:
return _patched_registry.has_callable(fn) return _patched_registry.has_callable(fn)
@ -1019,40 +1049,44 @@ def is_loader_installed() -> bool:
_patched_registry = PatchedRegistry() _patched_registry = PatchedRegistry()
_inspect_filter = InspectFilter()
_loader = AutoLoader() _loader = AutoLoader()
# Optimizations # Optimizations
from ._cwiring import _async_inject # noqa from ._cwiring import DependencyResolver # noqa: E402
from ._cwiring import _sync_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: F, patched: PatchedCallable) -> F: def _get_async_patched(fn: F, patched: PatchedCallable) -> F:
@functools.wraps(fn) @functools.wraps(fn)
async def _patched(*args, **kwargs): async def _patched(*args: Any, **raw_kwargs: Any) -> Any:
return await _async_inject( resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing)
fn,
args, async with resolver as kwargs:
kwargs, return await fn(*args, **kwargs)
patched.injections,
patched.closing, return cast(F, _patched)
)
def _get_async_gen_patched(fn: F, patched: PatchedCallable) -> F:
@functools.wraps(fn)
async def _patched(*args: Any, **raw_kwargs: Any) -> AsyncIterator[Any]:
resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing)
async with resolver as kwargs:
async for obj in fn(*args, **kwargs):
yield obj
return cast(F, _patched) return cast(F, _patched)
def _get_sync_patched(fn: F, patched: PatchedCallable) -> F: def _get_sync_patched(fn: F, patched: PatchedCallable) -> F:
@functools.wraps(fn) @functools.wraps(fn)
def _patched(*args, **kwargs): def _patched(*args: Any, **raw_kwargs: Any) -> Any:
return _sync_inject( resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing)
fn,
args, with resolver as kwargs:
kwargs, return fn(*args, **kwargs)
patched.injections,
patched.closing,
)
return cast(F, _patched) return cast(F, _patched)
@ -1078,3 +1112,8 @@ def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]:
member = args[1] member = args[1]
members.append((annotation_name, member)) members.append((annotation_name, member))
return members return members
def clear_cache() -> None:
"""Clear all caches used by :func:`wire`."""
_fetch_reference_injections.cache_clear()

View File

@ -145,3 +145,121 @@ async def test_shutdown_sync_and_async_ordering():
await container.shutdown_resources() await container.shutdown_resources()
assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"] assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"]
assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"] assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"]
@mark.asyncio
async def test_init_and_shutdown_scoped_resources():
initialized_resources = []
shutdown_resources = []
def _sync_resource(name, **_):
initialized_resources.append(name)
yield name
shutdown_resources.append(name)
async def _async_resource(name, **_):
initialized_resources.append(name)
yield name
shutdown_resources.append(name)
class ResourceA(providers.Resource):
pass
class ResourceB(providers.Resource):
pass
class Container(containers.DeclarativeContainer):
resource_a = ResourceA(
_sync_resource,
name="ra1",
)
resource_b1 = ResourceB(
_sync_resource,
name="rb1",
r1=resource_a,
)
resource_b2 = ResourceB(
_async_resource,
name="rb2",
r2=resource_b1,
)
container = Container()
container.init_resources(resource_type=ResourceA)
assert initialized_resources == ["ra1"]
assert shutdown_resources == []
container.shutdown_resources(resource_type=ResourceA)
assert initialized_resources == ["ra1"]
assert shutdown_resources == ["ra1"]
await container.init_resources(resource_type=ResourceB)
assert initialized_resources == ["ra1", "ra1", "rb1", "rb2"]
assert shutdown_resources == ["ra1"]
await container.shutdown_resources(resource_type=ResourceB)
assert initialized_resources == ["ra1", "ra1", "rb1", "rb2"]
assert shutdown_resources == ["ra1", "rb2", "rb1"]
@mark.asyncio
async def test_init_and_shutdown_all_scoped_resources_using_default_value():
initialized_resources = []
shutdown_resources = []
def _sync_resource(name, **_):
initialized_resources.append(name)
yield name
shutdown_resources.append(name)
async def _async_resource(name, **_):
initialized_resources.append(name)
yield name
shutdown_resources.append(name)
class ResourceA(providers.Resource):
pass
class ResourceB(providers.Resource):
pass
class Container(containers.DeclarativeContainer):
resource_a = ResourceA(
_sync_resource,
name="r1",
)
resource_b1 = ResourceB(
_sync_resource,
name="r2",
r1=resource_a,
)
resource_b2 = ResourceB(
_async_resource,
name="r3",
r2=resource_b1,
)
container = Container()
await container.init_resources()
assert initialized_resources == ["r1", "r2", "r3"]
assert shutdown_resources == []
await container.shutdown_resources()
assert initialized_resources == ["r1", "r2", "r3"]
assert shutdown_resources == ["r3", "r2", "r1"]
await container.init_resources()
assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"]
assert shutdown_resources == ["r3", "r2", "r1"]
await container.shutdown_resources()
assert initialized_resources == ["r1", "r2", "r3", "r1", "r2", "r3"]
assert shutdown_resources == ["r3", "r2", "r1", "r3", "r2", "r1"]

View File

@ -325,6 +325,19 @@ def test_init_shutdown_nested_resources():
assert _init2.shutdown_counter == 2 assert _init2.shutdown_counter == 2
def test_init_shutdown_resources_wrong_type() -> None:
class Container(containers.DeclarativeContainer):
pass
c = Container()
with raises(TypeError, match=r"resource_type must be a subclass of Resource provider"):
c.init_resources(int) # type: ignore[arg-type]
with raises(TypeError, match=r"resource_type must be a subclass of Resource provider"):
c.shutdown_resources(int) # type: ignore[arg-type]
def test_reset_singletons(): def test_reset_singletons():
class SubSubContainer(containers.DeclarativeContainer): class SubSubContainer(containers.DeclarativeContainer):
singleton = providers.Singleton(object) singleton = providers.Singleton(object)

View File

@ -1,4 +1,4 @@
from typing import AsyncIterator, Iterator from typing import AsyncIterator, Iterator, TypeVar
from unittest.mock import ANY from unittest.mock import ANY
from pytest import mark from pytest import mark
@ -7,6 +7,12 @@ from dependency_injector.containers import DeclarativeContainer
from dependency_injector.ext.starlette import Lifespan from dependency_injector.ext.starlette import Lifespan
from dependency_injector.providers import Resource from dependency_injector.providers import Resource
T = TypeVar("T")
class XResource(Resource[T]):
"""A test provider"""
class TestLifespan: class TestLifespan:
@mark.parametrize("sync", [False, True]) @mark.parametrize("sync", [False, True])
@ -28,11 +34,15 @@ class TestLifespan:
yield yield
shutdown = True shutdown = True
def nope():
assert False, "should not be called"
class Container(DeclarativeContainer): class Container(DeclarativeContainer):
x = Resource(sync_resource if sync else async_resource) x = XResource(sync_resource if sync else async_resource)
y = Resource(nope)
container = Container() container = Container()
lifespan = Lifespan(container) lifespan = Lifespan(container, resource_type=XResource)
async with lifespan(ANY) as scope: async with lifespan(ANY) as scope:
assert scope is None assert scope is None

View File

@ -2,12 +2,13 @@
import asyncio import asyncio
import inspect import inspect
import sys from contextlib import asynccontextmanager
from typing import Any from typing import Any
from dependency_injector import containers, providers, resources
from pytest import mark, raises from pytest import mark, raises
from dependency_injector import containers, providers, resources
@mark.asyncio @mark.asyncio
async def test_init_async_function(): async def test_init_async_function():
@ -70,6 +71,46 @@ async def test_init_async_generator():
assert _init.shutdown_counter == 2 assert _init.shutdown_counter == 2
@mark.asyncio
async def test_init_async_context_manager() -> None:
resource = object()
init_counter = 0
shutdown_counter = 0
@asynccontextmanager
async def _init():
nonlocal init_counter, shutdown_counter
await asyncio.sleep(0.001)
init_counter += 1
yield resource
await asyncio.sleep(0.001)
shutdown_counter += 1
provider = providers.Resource(_init)
result1 = await provider()
assert result1 is resource
assert init_counter == 1
assert shutdown_counter == 0
await provider.shutdown()
assert init_counter == 1
assert shutdown_counter == 1
result2 = await provider()
assert result2 is resource
assert init_counter == 2
assert shutdown_counter == 1
await provider.shutdown()
assert init_counter == 2
assert shutdown_counter == 2
@mark.asyncio @mark.asyncio
async def test_init_async_class(): async def test_init_async_class():
resource = object() resource = object()

View File

@ -2,10 +2,12 @@
import decimal import decimal
import sys import sys
from contextlib import contextmanager
from typing import Any from typing import Any
from dependency_injector import containers, providers, resources, errors from pytest import mark, raises
from pytest import raises, mark
from dependency_injector import containers, errors, providers, resources
def init_fn(*args, **kwargs): def init_fn(*args, **kwargs):
@ -123,6 +125,41 @@ def test_init_generator():
assert _init.shutdown_counter == 2 assert _init.shutdown_counter == 2
def test_init_context_manager() -> None:
init_counter, shutdown_counter = 0, 0
@contextmanager
def _init():
nonlocal init_counter, shutdown_counter
init_counter += 1
yield
shutdown_counter += 1
init_counter = 0
shutdown_counter = 0
provider = providers.Resource(_init)
result1 = provider()
assert result1 is None
assert init_counter == 1
assert shutdown_counter == 0
provider.shutdown()
assert init_counter == 1
assert shutdown_counter == 1
result2 = provider()
assert result2 is None
assert init_counter == 2
assert shutdown_counter == 1
provider.shutdown()
assert init_counter == 2
assert shutdown_counter == 2
def test_init_class(): def test_init_class():
class TestResource(resources.Resource): class TestResource(resources.Resource):
init_counter = 0 init_counter = 0
@ -190,7 +227,7 @@ def test_init_class_abc_shutdown_definition_is_not_required():
def test_init_not_callable(): def test_init_not_callable():
provider = providers.Resource(1) provider = providers.Resource(1)
with raises(errors.Error): with raises(TypeError, match=r"object is not callable"):
provider.init() provider.init()

View File

@ -1,7 +1,9 @@
import asyncio import asyncio
from typing_extensions import Annotated
from dependency_injector import containers, providers from dependency_injector import containers, providers
from dependency_injector.wiring import inject, Provide, Closing from dependency_injector.wiring import Closing, Provide, inject
class TestResource: class TestResource:
@ -42,6 +44,15 @@ async def async_injection(
return resource1, resource2 return resource1, resource2
@inject
async def async_generator_injection(
resource1: object = Provide[Container.resource1],
resource2: object = Closing[Provide[Container.resource2]],
):
yield resource1
yield resource2
@inject @inject
async def async_injection_with_closing( async def async_injection_with_closing(
resource1: object = Closing[Provide[Container.resource1]], resource1: object = Closing[Provide[Container.resource1]],

View File

@ -0,0 +1,39 @@
import sys
from fast_depends import Depends
from typing_extensions import Annotated
from dependency_injector import containers, providers
from dependency_injector.wiring import Provide, inject
class CoefficientService:
@staticmethod
def get_coefficient() -> float:
return 1.2
class Container(containers.DeclarativeContainer):
service = providers.Factory(CoefficientService)
@inject
def apply_coefficient(
a: int,
coefficient_provider: CoefficientService = Depends(Provide[Container.service]),
) -> float:
return a * coefficient_provider.get_coefficient()
@inject
def apply_coefficient_annotated(
a: int,
coefficient_provider: Annotated[
CoefficientService, Depends(Provide[Container.service])
],
) -> float:
return a * coefficient_provider.get_coefficient()
container = Container()
container.wire(modules=[sys.modules[__name__]])

View File

@ -32,6 +32,23 @@ async def test_async_injections():
assert asyncinjections.resource2.shutdown_counter == 0 assert asyncinjections.resource2.shutdown_counter == 0
@mark.asyncio
async def test_async_generator_injections() -> None:
resources = []
async for resource in asyncinjections.async_generator_injection():
resources.append(resource)
assert len(resources) == 2
assert resources[0] is asyncinjections.resource1
assert asyncinjections.resource1.init_counter == 1
assert asyncinjections.resource1.shutdown_counter == 0
assert resources[1] is asyncinjections.resource2
assert asyncinjections.resource2.init_counter == 1
assert asyncinjections.resource2.shutdown_counter == 1
@mark.asyncio @mark.asyncio
async def test_async_injections_with_closing(): async def test_async_injections_with_closing():
resource1, resource2 = await asyncinjections.async_injection_with_closing() resource1, resource2 = await asyncinjections.async_injection_with_closing()

View File

@ -0,0 +1,46 @@
"""Tests for string module and package names."""
from typing import Iterator, Optional
from pytest import fixture, mark
from samples.wiring.container import Container
from dependency_injector.wiring import _fetch_reference_injections
@fixture
def container() -> Iterator[Container]:
container = Container()
yield container
container.unwire()
@mark.parametrize(
["arg_value", "wc_value", "empty_cache"],
[
(None, False, True),
(False, True, True),
(True, False, False),
(None, True, False),
],
)
def test_fetch_reference_injections_cache(
container: Container,
arg_value: Optional[bool],
wc_value: bool,
empty_cache: bool,
) -> None:
container.wiring_config.keep_cache = wc_value
container.wire(
modules=["samples.wiring.module"],
packages=["samples.wiring.package"],
keep_cache=arg_value,
)
cache_info = _fetch_reference_injections.cache_info()
if empty_cache:
assert cache_info == (0, 0, None, 0)
else:
assert cache_info.hits > 0
assert cache_info.misses > 0
assert cache_info.currsize > 0

View File

@ -0,0 +1,9 @@
from wiringfastdepends import sample
def test_apply_coefficient() -> None:
assert sample.apply_coefficient(100) == 120.0
def test_apply_coefficient_annotated() -> None:
assert sample.apply_coefficient_annotated(100) == 120.0

View File

@ -17,6 +17,7 @@ deps=
mypy_boto3_s3 mypy_boto3_s3
pydantic-settings pydantic-settings
werkzeug werkzeug
fast-depends
extras= extras=
yaml yaml
commands = pytest commands = pytest
@ -44,6 +45,7 @@ deps =
boto3 boto3
mypy_boto3_s3 mypy_boto3_s3
werkzeug werkzeug
fast-depends
commands = pytest -m pydantic commands = pytest -m pydantic
[testenv:coveralls] [testenv:coveralls]