Merge branch 'release/4.48.1'

This commit is contained in:
ZipFile 2025-06-20 10:49:54 +00:00
commit 2c0aede4aa
8 changed files with 87 additions and 77 deletions

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

@ -7,6 +7,14 @@ 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 4.48.0
------ ------

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"]
@ -108,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

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

View File

@ -5,24 +5,11 @@ from collections.abc import Awaitable
from inspect import CO_ITERABLE_COROUTINE from inspect import CO_ITERABLE_COROUTINE
from types import CoroutineType, GeneratorType from types import CoroutineType, GeneratorType
from .providers cimport Provider, Resource, NULL_AWAITABLE from .providers cimport Provider, Resource
from .wiring import _Marker from .wiring import _Marker
cimport cython
cdef inline bint _is_injectable(dict kwargs, object name):
@cython.internal
@cython.no_gc
cdef class KWPair:
cdef str name
cdef object value
def __cinit__(self, str name, object value, /):
self.name = name
self.value = value
cdef inline bint _is_injectable(dict kwargs, str name):
return name not in kwargs or isinstance(kwargs[name], _Marker) return name not in kwargs or isinstance(kwargs[name], _Marker)
@ -38,11 +25,8 @@ cdef class DependencyResolver:
self.injections = injections self.injections = injections
self.closings = closings self.closings = closings
async def _await_injection(self, kw_pair: KWPair, /) -> None: async def _await_injection(self, name: str, value: object, /) -> None:
self.to_inject[kw_pair.name] = await kw_pair.value self.to_inject[name] = await value
cdef object _await_injections(self, to_await: list):
return gather(*map(self._await_injection, to_await))
cdef void _handle_injections_sync(self): cdef void _handle_injections_sync(self):
cdef Provider provider cdef Provider provider
@ -60,7 +44,7 @@ cdef class DependencyResolver:
provide = provider() provide = provider()
if provider.is_async_mode_enabled() or _isawaitable(provide): if provider.is_async_mode_enabled() or _isawaitable(provide):
to_await.append(KWPair(name, provide)) to_await.append(self._await_injection(name, provide))
else: else:
self.to_inject[name] = provide self.to_inject[name] = provide
@ -93,13 +77,12 @@ cdef class DependencyResolver:
async def __aenter__(self): async def __aenter__(self):
if to_await := self._handle_injections_async(): if to_await := self._handle_injections_async():
await self._await_injections(to_await) await gather(*to_await)
return self.to_inject return self.to_inject
def __aexit__(self, *_): async def __aexit__(self, *_):
if to_await := self._handle_closings_async(): if to_await := self._handle_closings_async():
return gather(*to_await) await gather(*to_await)
return NULL_AWAITABLE
cdef bint _isawaitable(object instance): cdef bint _isawaitable(object instance):

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

@ -6,6 +6,8 @@ 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,
@ -15,6 +17,7 @@ from typing import (
Dict, Dict,
Iterable, Iterable,
Iterator, Iterator,
List,
Optional, Optional,
Protocol, Protocol,
Set, Set,
@ -24,6 +27,7 @@ from typing import (
Union, Union,
cast, cast,
) )
from warnings import warn
try: try:
from typing import Self from typing import Self
@ -59,13 +63,11 @@ else:
return None return None
MARKER_EXTRACTORS = [] MARKER_EXTRACTORS: List[Callable[[Any], Any]] = []
INSPECT_EXCLUSION_FILTERS: List[Callable[[Any], bool]] = [isbuiltin]
try: with suppress(ImportError):
from fastapi.params import Depends as FastAPIDepends from fastapi.params import Depends as FastAPIDepends
except ImportError:
pass
else:
def extract_marker_from_fastapi(param: Any) -> Any: def extract_marker_from_fastapi(param: Any) -> Any:
if isinstance(param, FastAPIDepends): if isinstance(param, FastAPIDepends):
@ -74,11 +76,8 @@ else:
MARKER_EXTRACTORS.append(extract_marker_from_fastapi) MARKER_EXTRACTORS.append(extract_marker_from_fastapi)
try: with suppress(ImportError):
from fast_depends.dependencies import Depends as FastDepends from fast_depends.dependencies import Depends as FastDepends
except ImportError:
pass
else:
def extract_marker_from_fast_depends(param: Any) -> Any: def extract_marker_from_fast_depends(param: Any) -> Any:
if isinstance(param, FastDepends): if isinstance(param, FastDepends):
@ -88,16 +87,22 @@ else:
MARKER_EXTRACTORS.append(extract_marker_from_fast_depends) 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)
INSPECT_EXCLUSION_FILTERS.append(is_werkzeug_local_proxy)
from . import providers # noqa: E402 from . import providers # noqa: E402
@ -130,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:
@ -411,30 +420,11 @@ 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 False
return True
elif self._is_builtin(instance):
return True
else:
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
@ -455,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):
@ -520,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)
@ -1054,7 +1049,6 @@ def is_loader_installed() -> bool:
_patched_registry = PatchedRegistry() _patched_registry = PatchedRegistry()
_inspect_filter = InspectFilter()
_loader = AutoLoader() _loader = AutoLoader()
# Optimizations # Optimizations

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