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:
- uses: actions/checkout@v3
- name: Build wheels
uses: pypa/cibuildwheel@v2.23.3
uses: pypa/cibuildwheel@v3.0.0
- uses: actions/upload-artifact@v4
with:
name: cibw-wheels-x86-${{ matrix.os }}-${{ strategy.job-index }}
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
path: ./wheelhouse/*.whl
test-publish:

View File

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

View File

@ -52,6 +52,11 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = ["version"]
dependencies = [
# typing.Annotated since v3.9
# typing.Self since v3.11
"typing-extensions; python_version<'3.11'",
]
[project.optional-dependencies]
yaml = ["pyyaml"]
@ -108,6 +113,7 @@ markers = [
"pydantic: Tests with Pydantic as a dependency",
]
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.flask\" is deprecated since version 4\\.0\\.0:DeprecationWarning",
"ignore:Please use \\`.*?\\` from the \\`scipy.*?\\`(.*?)namespace is deprecated\\.:DeprecationWarning",

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import sys
from typing import Any
from typing import Any, Type
if sys.version_info >= (3, 11): # pragma: no cover
from typing import Self
@ -7,6 +7,7 @@ else: # pragma: no cover
from typing_extensions import Self
from dependency_injector.containers import Container
from dependency_injector.providers import Resource
class Lifespan:
@ -29,24 +30,32 @@ class Lifespan:
app = Factory(Starlette, lifespan=lifespan)
: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
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.resource_type = resource_type
def __call__(self, app: Any) -> Self:
return self
async def __aenter__(self) -> None:
result = self.container.init_resources()
result = self.container.init_resources(self.resource_type)
if result is not None:
await result
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:
await result

View File

@ -6,6 +6,8 @@ import importlib.machinery
import inspect
import pkgutil
import sys
from contextlib import suppress
from inspect import isbuiltin, isclass
from types import ModuleType
from typing import (
TYPE_CHECKING,
@ -15,6 +17,7 @@ from typing import (
Dict,
Iterable,
Iterator,
List,
Optional,
Protocol,
Set,
@ -24,6 +27,7 @@ from typing import (
Union,
cast,
)
from warnings import warn
try:
from typing import Self
@ -59,13 +63,11 @@ else:
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
except ImportError:
pass
else:
def extract_marker_from_fastapi(param: Any) -> Any:
if isinstance(param, FastAPIDepends):
@ -74,11 +76,8 @@ else:
MARKER_EXTRACTORS.append(extract_marker_from_fastapi)
try:
with suppress(ImportError):
from fast_depends.dependencies import Depends as FastDepends
except ImportError:
pass
else:
def extract_marker_from_fast_depends(param: Any) -> Any:
if isinstance(param, FastDepends):
@ -88,16 +87,22 @@ else:
MARKER_EXTRACTORS.append(extract_marker_from_fast_depends)
try:
import starlette.requests
except ImportError:
starlette = None
with suppress(ImportError):
from starlette.requests import Request as StarletteRequest
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:
import werkzeug.local
except ImportError:
werkzeug = None
with suppress(ImportError):
from werkzeug.local import LocalProxy as WerkzeugLocalProxy
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
@ -130,6 +135,10 @@ else:
Container = Any
class DIWiringWarning(RuntimeWarning):
"""Base class for all warnings raised by the wiring module."""
class PatchedRegistry:
def __init__(self) -> None:
@ -411,30 +420,11 @@ class ProvidersMap:
return providers_map
class InspectFilter:
def is_excluded(self, instance: object) -> bool:
if self._is_werkzeug_local_proxy(instance):
def is_excluded_from_inspect(obj: Any) -> bool:
for is_excluded in INSPECT_EXCLUSION_FILTERS:
if is_excluded(obj):
return True
elif self._is_starlette_request_cls(instance):
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)
return False
def wire( # noqa: C901
@ -455,7 +445,7 @@ def wire( # noqa: C901
for module in modules:
for member_name, member in _get_members_and_annotated(module):
if _inspect_filter.is_excluded(member):
if is_excluded_from_inspect(member):
continue
if _is_marker(member):
@ -520,6 +510,11 @@ def unwire( # noqa: C901
def inject(fn: F) -> F:
"""Decorate callable with injecting decorator."""
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)
return cast(F, patched)
@ -1054,7 +1049,6 @@ def is_loader_installed() -> bool:
_patched_registry = PatchedRegistry()
_inspect_filter = InspectFilter()
_loader = AutoLoader()
# Optimizations

View File

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