diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 18ca9672..4ebbcbc3 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -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 ------ diff --git a/pyproject.toml b/pyproject.toml index 88553178..ef0b946d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index ec63edf0..af252a31 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.48.0" +__version__ = "4.48.1" """Version number. :type: str diff --git a/src/dependency_injector/_cwiring.pyx b/src/dependency_injector/_cwiring.pyx index 3e2775c7..01871243 100644 --- a/src/dependency_injector/_cwiring.pyx +++ b/src/dependency_injector/_cwiring.pyx @@ -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): diff --git a/src/dependency_injector/ext/starlette.py b/src/dependency_injector/ext/starlette.py index becadf0a..620d7d26 100644 --- a/src/dependency_injector/ext/starlette.py +++ b/src/dependency_injector/ext/starlette.py @@ -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 @@ from typing_extensions import Self from dependency_injector.containers import Container +from dependency_injector.providers import Resource class Lifespan: @@ -29,24 +30,32 @@ class Container(DeclarativeContainer): 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 diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 5856622b..6d5d1510 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -6,6 +6,8 @@ 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 @@ Dict, Iterable, Iterator, + List, Optional, Protocol, Set, @@ -24,6 +27,7 @@ Union, cast, ) +from warnings import warn try: from typing import Self @@ -59,13 +63,11 @@ def get_origin(tp): 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 @@ def extract_marker_from_fastapi(param: Any) -> Any: 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 @@ def extract_marker_from_fast_depends(param: Any) -> Any: 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) -try: - import werkzeug.local -except ImportError: - werkzeug = None + INSPECT_EXCLUSION_FILTERS.append(is_starlette_request_cls) + + +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 @@ def extract_marker_from_fast_depends(param: Any) -> Any: 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 @@ def _create_providers_map( return providers_map -class InspectFilter: - - def is_excluded(self, instance: object) -> bool: - if self._is_werkzeug_local_proxy(instance): - return True - elif self._is_starlette_request_cls(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_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 diff --git a/tests/unit/ext/test_starlette.py b/tests/unit/ext/test_starlette.py index e569a382..f50d6f46 100644 --- a/tests/unit/ext/test_starlette.py +++ b/tests/unit/ext/test_starlette.py @@ -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.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 @@ async def async_resource() -> AsyncIterator[None]: 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