diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..27dc8e3e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + open-pull-requests-limit: 2 + schedule: + interval: monthly + target-branch: "develop" diff --git a/.github/workflows/publishing.yml b/.github/workflows/publishing.yml index 776256c7..acc4e752 100644 --- a/.github/workflows/publishing.yml +++ b/.github/workflows/publishing.yml @@ -12,14 +12,14 @@ jobs: name: Run tests runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - run: pip install tox - run: tox env: - TOXENV: 3.13 + TOXENV: 3.14 linters: name: Run linters @@ -28,10 +28,10 @@ jobs: matrix: toxenv: [flake8, pydocstyle, mypy, pylint] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - run: pip install tox - run: tox env: @@ -42,14 +42,14 @@ jobs: needs: [tests, linters] runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - run: | python -m pip install --upgrade build python -m build --sdist - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@v7 with: name: cibw-sdist path: ./dist/* @@ -63,15 +63,16 @@ jobs: os: [ubuntu-24.04, ubuntu-24.04-arm, windows-2022, macos-14] env: CIBW_ENABLE: pypy + CIBW_SKIP: "cp3??t-*" CIBW_ENVIRONMENT: >- PIP_CONFIG_SETTINGS="build_ext=-j4" DEPENDENCY_INJECTOR_LIMITED_API="1" CFLAGS="-g0" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 - name: Build wheels - uses: pypa/cibuildwheel@v3.0.0 - - uses: actions/upload-artifact@v4 + uses: pypa/cibuildwheel@v3.4.1 + - uses: actions/upload-artifact@v7 with: name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} path: ./wheelhouse/*.whl @@ -84,7 +85,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: pattern: cibw-* path: dist @@ -101,7 +102,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v8 with: pattern: cibw-* path: dist @@ -113,10 +114,10 @@ jobs: needs: [publish] runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - run: pip install awscli - run: pip install -r requirements-doc.txt - run: pip install -e . diff --git a/.github/workflows/tests-and-linters.yml b/.github/workflows/tests-and-linters.yml index bf07a2d9..e81f6e10 100644 --- a/.github/workflows/tests-and-linters.yml +++ b/.github/workflows/tests-and-linters.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - run: pip install tox @@ -25,10 +25,10 @@ jobs: name: Run tests with different pydantic versions runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: "3.12" + python-version: "3.14" - run: pip install tox - run: tox -e pydantic-v1,pydantic-v2 @@ -41,10 +41,10 @@ jobs: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.12 + python-version: 3.14 - run: pip install tox - run: tox -vv env: @@ -57,10 +57,10 @@ jobs: matrix: toxenv: [flake8, pydocstyle, mypy, pylint] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - run: pip install tox - run: tox env: diff --git a/.gitignore b/.gitignore index 86ecf7c7..2a25c1e7 100644 --- a/.gitignore +++ b/.gitignore @@ -70,6 +70,11 @@ src/**/*.h src/**/*.so src/**/*.html +# Cython test fixture build outputs +tests/unit/samples/wiringcython/*.c +tests/unit/samples/wiringcython/*.so +tests/unit/samples/wiringcython/_build/ + # Workspace for samples .workspace/ diff --git a/Makefile b/Makefile index 29e4086f..a796f484 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ VERSION := $(shell python setup.py --version) export COVERAGE_RCFILE := pyproject.toml +export CIBW_ENVIRONMENT_PASS_LINUX := CFLAGS PIP_CONFIG_SETTINGS DEPENDENCY_INJECTOR_LIMITED_API +export PIP_CONFIG_SETTINGS ?= build_ext=-j4 +export DEPENDENCY_INJECTOR_LIMITED_API ?= 1 +export CFLAGS ?= -g0 clean: # Clean sources @@ -63,3 +67,6 @@ publish: # Create and upload tag git tag -a $(VERSION) -m 'version $(VERSION)' git push --tags + +wheels: + cibuildwheel --output-dir wheelhouse diff --git a/docs/containers/declarative.rst b/docs/containers/declarative.rst index febc0bf9..e464f49e 100644 --- a/docs/containers/declarative.rst +++ b/docs/containers/declarative.rst @@ -16,7 +16,7 @@ The declarative container providers should only be used when you have the contai Working with the providers of the container on the class level will influence all further instances. -The declarative container can not have any methods or any other attributes then providers. +A declarative container cannot have any methods or attributes other than providers. The container class provides next attributes: diff --git a/docs/examples/faststream.rst b/docs/examples/faststream.rst new file mode 100644 index 00000000..46543009 --- /dev/null +++ b/docs/examples/faststream.rst @@ -0,0 +1,42 @@ +.. _faststream-example: + +FastStream example +================== + +.. meta:: + :keywords: Python,Dependency Injection,FastStream,Example + :description: This example demonstrates a usage of FastStream with Dependency Injector. + + +This example shows how to use ``Dependency Injector`` with `FastStream `_. + +The source code is available on the `Github `_. + +Despite ``FastStream`` uses ``FastDepends`` library for dependency injection, the integration between +``Dependency injector`` and ``FastStream`` has a small difference from already existing :ref:`fastdepends-example`. + +Since ``FastStream`` also leverages function signatures to determine input data types you have to use ``Depends()`` function +with ``cast=False`` argument to make ``FastStream`` ignore your injected dependency argument in the function signature. + +Example below shows how to inject ``Counter`` class into ``FastStream`` redis handler so that it will distinguish between +message schema (``User``) and injected dependency (``Counter``) and use them both correctly. + +Listing of ``consumer.py``: + +.. literalinclude:: ../../examples/miniapps/faststream/consumer.py + :language: python + +Listing of ``producer.py``: + +.. literalinclude:: ../../examples/miniapps/faststream/producer.py + :language: python + +Sources +------- + +Explore the sources on the `Github `_. + +.. include:: ../sponsor.rst + +.. disqus:: + diff --git a/docs/examples/index.rst b/docs/examples/index.rst index b166ceae..40f15470 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -23,5 +23,6 @@ Explore the examples to see the ``Dependency Injector`` in action. fastapi-redis fastapi-sqlalchemy fastdepends + faststream .. disqus:: diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 4ebbcbc3..88221091 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,41 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +4.49.1 +------ + +- Fix sync resource shutdown with async deps. +- Add support for wiring of Cython-compiled modules. Thanks to `@keyz182 `_ for `#965 `_ for `#963 `_ for `#958 `_ for `#951 `_. +- Add keep_cache argument to Container.wire typings. Thanks to `@romantolkachyov `_ for `#952 `_. +- Use assert_type for type-stub checks. Thanks to `@leonarduschen `_ for `#934 `_, `#953 `_. +- Add provided().call ``*args``, ``**kwargs`` arguments. Thanks to `@pavalso `_ for `#946 `_. +- Add context local resource. Thanks to `@elina-israyelyan `_ for `#931 `_. +- Update CI/CD to ``actions/checkout@v6``, ``actions/setup-python@v6``, ``actions/download-artifact@v8``, ``actions/upload-artifact@v7`` and ``pypa/cibuildwheel@v3.4.0``. +- Add dependabot config for GitHub Actions + +4.48.3 +------ + +- Allow annotated marker to be anywhere in the annotation list. Thanks to `@BrianPugh `_ for `#939 `_. +- Fix FastDepends v3 compatibility. Thanks to `@AndrianEquestrian `_ for `#933 `_. +- Various type annotation improvements for providers. Thanks to `@leonarduschen `_ for `#927 `_, `#932 `_ and `#935 `_. + +4.48.2 +------ + +- Add ``warn_unresolved=True`` to ``WiringConfiguration`` and ``container.wire()`` + to produce warnings on unresolved string identifiers. +- ABI3 wheels are now built only for CPython version >=3.10 (see issue `#919 `_). + 4.48.1 ------ diff --git a/docs/providers/configuration.rst b/docs/providers/configuration.rst index 66c299f3..651bf772 100644 --- a/docs/providers/configuration.rst +++ b/docs/providers/configuration.rst @@ -212,6 +212,19 @@ the container will call ``config.from_pydantic()`` automatically: if __name__ == "__main__": container = Container() # Config is loaded from Settings() +In addition, if you need the pydantic instance to be initialized on use, you can provide ``pydantic_settings.BaseSettings`` type instead. +The container will initialize a pydantic instance on load without kwargs. + +.. code-block:: python + :emphasize-lines: 3 + + class Container(containers.DeclarativeContainer): + + config = providers.Configuration(pydantic_settings=[Settings]) + + + if __name__ == "__main__": + container = Container() # Config is loaded from Settings instance that is initialized .. note:: diff --git a/docs/providers/context_local_resource.rst b/docs/providers/context_local_resource.rst new file mode 100644 index 00000000..c169b6a1 --- /dev/null +++ b/docs/providers/context_local_resource.rst @@ -0,0 +1,32 @@ +.. _context-local-resource-provider: + +Context Local Resource provider +================================ + +.. meta:: + :keywords: Python,DI,Dependency injection,IoC,Inversion of Control,Resource,Context Local, + Context Variables,Singleton,Per-context + :description: Context Local Resource provider provides a component with initialization and shutdown + that is scoped to execution context using contextvars. This page demonstrates how to + use context local resource provider. + +.. currentmodule:: dependency_injector.providers + +``ContextLocalResource`` inherits from :ref:`resource-provider` and uses the same initialization and shutdown logic +as the standard ``Resource`` provider. +It extends it with context-local storage using Python's ``contextvars`` module. +This means that objects are context local singletons - the same context will +receive the same instance, but different execution contexts will have their own separate instances. + +This is particularly useful in asynchronous applications where you need per-request resource instances +(such as database sessions) that are automatically cleaned up when the request context ends. +Example: + +.. literalinclude:: ../../examples/providers/context_local_resource.py + :language: python + :lines: 3- + + + +.. disqus:: + diff --git a/docs/providers/index.rst b/docs/providers/index.rst index 3edbf127..0dacb826 100644 --- a/docs/providers/index.rst +++ b/docs/providers/index.rst @@ -46,6 +46,7 @@ Providers module API docs - :py:mod:`dependency_injector.providers` dict configuration resource + context_local_resource aggregate selector dependency diff --git a/docs/providers/resource.rst b/docs/providers/resource.rst index b07c2db0..02863a47 100644 --- a/docs/providers/resource.rst +++ b/docs/providers/resource.rst @@ -21,6 +21,9 @@ Resource provider Resource providers help to initialize and configure logging, event loop, thread or process pool, etc. Resource provider is similar to ``Singleton``. Resource initialization happens only once. +If you need a context local singleton (where each execution context has its own instance), +see :ref:`context-local-resource-provider`. + You can make injections and use provided instance the same way like you do with any other provider. .. code-block:: python diff --git a/docs/providers/singleton.rst b/docs/providers/singleton.rst index 5c2d517f..eeb8ca2e 100644 --- a/docs/providers/singleton.rst +++ b/docs/providers/singleton.rst @@ -33,7 +33,7 @@ factories: - :ref:`factory-specialize-provided-type` - :ref:`abstract-factory` -``Singleton`` provider scope is tied to the container. Two different containers will provider +``Singleton`` provider scope is tied to the container. Two different containers will provide two different singleton objects: .. literalinclude:: ../../examples/providers/singleton_multiple_containers.py diff --git a/docs/wiring.rst b/docs/wiring.rst index bb6ba156..e0cbed5f 100644 --- a/docs/wiring.rst +++ b/docs/wiring.rst @@ -251,6 +251,32 @@ To inject a container use special identifier ````: def foo(container: Container = Provide[""]) -> None: ... +Caveats +~~~~~~~ + +While using string identifiers you may not notice a typo in the identifier until the code is executed. +In order to aid with catching such errors early, you may pass `warn_unresolved=True` to the ``wire`` method and/or :class:`WiringConfiguration`: + +.. code-block:: python + :emphasize-lines: 4 + + class Container(containers.DeclarativeContainer): + wiring_config = containers.WiringConfiguration( + modules=["yourapp.module"], + warn_unresolved=True, + ) + +Or: + +.. code-block:: python + :emphasize-lines: 4 + + container = Container() + container.wire( + modules=["yourapp.module"], + warn_unresolved=True, + ) + Making injections into modules and class attributes --------------------------------------------------- @@ -632,6 +658,57 @@ or with a single container ``register_loader_containers(container)`` multiple ti To unregister a container use ``unregister_loader_containers(container)``. Wiring module will uninstall the import hook when unregister last container. +Wiring of Cython-compiled modules +--------------------------------- + +Modules compiled with Cython (e.g. to ship business logic as ``.so`` +extensions in source-protected container images) are wired transparently +provided the compile sets two directives: + +* ``binding=True`` — preserve descriptor / bound-method semantics so + :func:`inspect.signature` and the wiring discovery pass work as they do + for pure-Python functions. +* ``embedsignature=True`` — embed the Python-style signature so + :func:`inspect.signature` can recover parameter names, annotations, and + ``Provide[...]`` / ``Provider[...]`` markers from the compiled function. + +A typical ``cythonize`` invocation that produces wiring-compatible +extensions for a FastAPI / dependency-injector codebase: + +.. code-block:: python + + from Cython.Build import cythonize + + cythonize( + ["my_package/handlers/*.py"], + compiler_directives={ + "language_level": 3, + "binding": True, + "embedsignature": True, + }, + ) + +FastAPI views and dependencies that rely on parameter defaults as +markers (``param: str = Header(...)``, ``svc: Service = Depends(...)``, +``Provide[Container.x]``) need Cython's C-level annotation typing +disabled. The default in Cython 3.x is ``annotation_typing=True``, which +generates ``isinstance`` checks against the annotated types and rejects +the marker objects at call time. Opt out per-function: + +.. code-block:: python + + import cython + + @cython.annotation_typing(False) + async def list_users( + svc: UserService = Depends(Provide[Container.user_service]), + ) -> list[User]: + return await svc.list() + +Apply the decorator to every FastAPI view or dependency callable that +takes a marker-style default. Module-level ``annotation_typing=False`` +works too if the whole module is FastAPI-bound. + Few notes on performance ------------------------ diff --git a/examples/miniapps/faststream/Dockerfile b/examples/miniapps/faststream/Dockerfile new file mode 100644 index 00000000..a29c4f41 --- /dev/null +++ b/examples/miniapps/faststream/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.13-bookworm + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip install -r requirements.txt + +COPY . ./ + +ENV PYTHONUNBUFFERED=1 diff --git a/examples/miniapps/faststream/README.rst b/examples/miniapps/faststream/README.rst new file mode 100644 index 00000000..aef51b4f --- /dev/null +++ b/examples/miniapps/faststream/README.rst @@ -0,0 +1,41 @@ +FastStream + Dependency Injector Example +======================================== + +This is a `FastStream `_ + +`Dependency Injector `_ example application. + +The example application is a simple consumer that counts messages sent to redis channel by producer. + +Counter is provided to faststream handler as a dependency injected by ``dependency_injector`` library. + +Run +--- + +Everything can be run via docker compose. + +A convenient ``run.sh`` script runs consumer, producer and redis services, prints logs from consumer +and shuts down once producer exits. + + +Run the sciprt: + +.. code-block:: bash + + ./run.sh + +The output should be something like: + +.. code-block:: + + faststream-example-consumer | Message #1 from John: 'As you can see' + faststream-example-consumer | Message #2 from John: 'messages are counted correctly' + faststream-example-consumer | Message #3 from John: 'by the counter that is injected' + faststream-example-consumer | Message #4 from John: 'into faststream handler' + faststream-example-consumer | Message #5 from John: 'via awesome dependency_injector library.' + + +Once you've done working with this example you can clean up docker images and containers it produced: + +.. code-block:: bash + + docker compose down --rmi local diff --git a/examples/miniapps/faststream/consumer.py b/examples/miniapps/faststream/consumer.py new file mode 100644 index 00000000..ce40e2b8 --- /dev/null +++ b/examples/miniapps/faststream/consumer.py @@ -0,0 +1,67 @@ +import asyncio +from typing import Annotated + +from dependency_injector import containers, providers +from dependency_injector.wiring import Provide, inject +from faststream import Depends, FastStream +from faststream.redis import RedisBroker, RedisRouter +from pydantic import BaseModel + + +class Counter: + def __init__(self): + self.count = 0 + + def next(self) -> int: + self.count += 1 + return self.count + + +class Container(containers.DeclarativeContainer): + counter = providers.Singleton(Counter) + + config = providers.Configuration() + + broker = providers.Singleton(RedisBroker, config.redis_url, logger=None) + app = providers.Factory(FastStream, broker, logger=None) + + +class Message(BaseModel): + user: str + text: str + + +router = RedisRouter() + + +@router.subscriber("messages") +@inject +async def handle_user_message( + message: Message, + counter: Annotated[ + Counter, + Depends( + Provide[Container.counter], + cast=False, # <-- this is the key part + ), + ], +) -> None: + count = counter.next() + print(f"Message #{count} from {message.user}: '{message.text}'") + + +async def main() -> None: + container = Container() + container.wire(modules=[__name__]) + + container.config.redis_url.from_env("REDIS_URL") + + broker = container.broker() + broker.include_router(router) + + app = container.app() + await app.run() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/miniapps/faststream/docker-compose.yml b/examples/miniapps/faststream/docker-compose.yml new file mode 100644 index 00000000..b760e1df --- /dev/null +++ b/examples/miniapps/faststream/docker-compose.yml @@ -0,0 +1,24 @@ +name: faststream-example + +services: + + redis: + image: redis + + consumer: + build: . + environment: + REDIS_URL: "redis://redis" + depends_on: + - redis + entrypoint: python3 consumer.py + + producer: + build: . + environment: + REDIS_HOST: "redis" + REDIS_PORT: "6379" + depends_on: + - consumer + entrypoint: python3 producer.py + diff --git a/examples/miniapps/faststream/producer.py b/examples/miniapps/faststream/producer.py new file mode 100644 index 00000000..5ebc59de --- /dev/null +++ b/examples/miniapps/faststream/producer.py @@ -0,0 +1,37 @@ +import json +import time + +from dependency_injector import containers, providers +from redis import Redis + + +class Container(containers.DeclarativeContainer): + config = providers.Configuration() + + redis = providers.Singleton(Redis, config.redis_host, config.redis_port.as_int()) + + +def main(): + container = Container() + container.wire(modules=[__name__]) + + container.config.redis_host.from_env("REDIS_HOST") + container.config.redis_port.from_env("REDIS_PORT") + + redis = container.redis() + + for text in ( + "As you can see", + "messages are counted correctly", + "by the counter that is injected", + "into faststream handler", + "via awesome dependency_injector library.", + ): + time.sleep(2) + + message = {"user": "John", "text": text} + redis.publish("messages", json.dumps(message)) + + +if __name__ == "__main__": + main() diff --git a/examples/miniapps/faststream/requirements.txt b/examples/miniapps/faststream/requirements.txt new file mode 100644 index 00000000..8d9a0c60 --- /dev/null +++ b/examples/miniapps/faststream/requirements.txt @@ -0,0 +1,4 @@ +dependency_injector +faststream +pydantic +redis diff --git a/examples/miniapps/faststream/run.sh b/examples/miniapps/faststream/run.sh new file mode 100755 index 00000000..f5fd00b9 --- /dev/null +++ b/examples/miniapps/faststream/run.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +docker compose up \ + --no-attach=redis \ + --abort-on-container-exit \ + --exit-code-from producer + diff --git a/examples/providers/context_local_resource.py b/examples/providers/context_local_resource.py new file mode 100644 index 00000000..9b6e503f --- /dev/null +++ b/examples/providers/context_local_resource.py @@ -0,0 +1,49 @@ +from uuid import uuid4 + +from fastapi import Depends, FastAPI + +from dependency_injector import containers, providers +from dependency_injector.wiring import Closing, Provide, inject + +global_list = [] + + +class AsyncSessionLocal: + def __init__(self): + self.id = uuid4() + + async def __aenter__(self): + print("Entering session !") + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + print("Closing session !") + + async def execute(self, user_input): + return f"Executing {user_input} in session {self.id}" + + +app = FastAPI() + + +class Container(containers.DeclarativeContainer): + db_session = providers.ContextLocalResource(AsyncSessionLocal) + + +@app.get("/") +@inject +async def index(db: AsyncSessionLocal = Depends(Closing[Provide["db_session"]])): + if db.id in global_list: + raise Exception("The db session was already used") # never reaches here + global_list.append(db.id) + res = await db.execute("SELECT 1") + return str(res) + + +if __name__ == "__main__": + import uvicorn + + container = Container() + container.wire(modules=["__main__"]) + uvicorn.run(app, host="localhost", port=8000) + container.unwire() diff --git a/pyproject.toml b/pyproject.toml index ef0b946d..5eaace54 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "Cython>=3.1.1"] +requires = ["setuptools", "Cython>=3.1.4"] build-backend = "setuptools.build_meta" [project] @@ -37,6 +37,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Framework :: AsyncIO", @@ -54,8 +55,9 @@ classifiers = [ dynamic = ["version"] dependencies = [ # typing.Annotated since v3.9 - # typing.Self since v3.11 - "typing-extensions; python_version<'3.11'", + # typing.Self and typing.assert_never since v3.11 + # typing.TypeVar default since v3.13 + "typing-extensions; python_version<'3.13'", ] [project.optional-dependencies] diff --git a/requirements-dev.txt b/requirements-dev.txt index 408b9bb6..9c33d385 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -cython==3.1.1 +cython==3.1.4 setuptools pytest pytest-asyncio diff --git a/setup.py b/setup.py index 5f4669e4..ba9d3068 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ import os import sys +import sysconfig from Cython.Build import cythonize from Cython.Compiler import Options @@ -11,6 +12,8 @@ limited_api = ( os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1" and sys.implementation.name == "cpython" + and sys.version_info >= (3, 10) + and not sysconfig.get_config_var("Py_GIL_DISABLED") ) defined_macros = [] options = {} @@ -34,8 +37,8 @@ if limited_api: options.setdefault("bdist_wheel", {}) - options["bdist_wheel"]["py_limited_api"] = "cp38" - defined_macros.append(("Py_LIMITED_API", "0x03080000")) + options["bdist_wheel"]["py_limited_api"] = "cp310" + defined_macros.append(("Py_LIMITED_API", "0x030A0000")) setup( options=options, diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index af252a31..a8978308 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Top-level package.""" -__version__ = "4.48.1" +__version__ = "4.49.1" """Version number. :type: str diff --git a/src/dependency_injector/_cwiring.pyx b/src/dependency_injector/_cwiring.pyx index 01871243..13b29235 100644 --- a/src/dependency_injector/_cwiring.pyx +++ b/src/dependency_injector/_cwiring.pyx @@ -5,7 +5,7 @@ from collections.abc import Awaitable from inspect import CO_ITERABLE_COROUTINE from types import CoroutineType, GeneratorType -from .providers cimport Provider, Resource +from .providers cimport Provider, BaseResource from .wiring import _Marker @@ -54,7 +54,7 @@ cdef class DependencyResolver: cdef Provider provider for name, provider in self.closings.items(): - if _is_injectable(self.kwargs, name) and isinstance(provider, Resource): + if _is_injectable(self.kwargs, name) and isinstance(provider, BaseResource): provider.shutdown() cdef list _handle_closings_async(self): @@ -62,7 +62,7 @@ cdef class DependencyResolver: cdef Provider provider for name, provider in self.closings.items(): - if _is_injectable(self.kwargs, name) and isinstance(provider, Resource): + if _is_injectable(self.kwargs, name) and isinstance(provider, BaseResource): if _isawaitable(shutdown := provider.shutdown()): to_await.append(shutdown) diff --git a/src/dependency_injector/containers.pyi b/src/dependency_injector/containers.pyi index f21a8791..3a928646 100644 --- a/src/dependency_injector/containers.pyi +++ b/src/dependency_injector/containers.pyi @@ -22,7 +22,7 @@ try: except ImportError: from typing_extensions import Self as _Self -from .providers import Provider, Resource, Self, ProviderParent +from .providers import Provider, BaseResource, Self, ProviderParent C_Base = TypeVar("C_Base", bound="Container") C = TypeVar("C", bound="DeclarativeContainer") @@ -43,6 +43,7 @@ class WiringConfiguration: from_package: Optional[str] = None, auto_wire: bool = True, keep_cache: bool = False, + warn_unresolved: bool = False, ) -> None: ... class Container: @@ -72,10 +73,12 @@ class Container: modules: Optional[Iterable[Any]] = None, packages: Optional[Iterable[Any]] = None, from_package: Optional[str] = None, + keep_cache: Optional[bool] = None, + warn_unresolved: bool = False, ) -> None: ... def unwire(self) -> None: ... - def init_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ... - def shutdown_resources(self, resource_type: Type[Resource[Any]] = Resource) -> Optional[Awaitable[None]]: ... + def init_resources(self, resource_type: Type[BaseResource[Any]] = BaseResource) -> Optional[Awaitable[None]]: ... + def shutdown_resources(self, resource_type: Type[BaseResource[Any]] = BaseResource) -> Optional[Awaitable[None]]: ... def load_config(self) -> None: ... def apply_container_providers_overridings(self) -> None: ... def reset_singletons(self) -> SingletonResetContext[C_Base]: ... diff --git a/src/dependency_injector/containers.pyx b/src/dependency_injector/containers.pyx index 99762da2..cb55083f 100644 --- a/src/dependency_injector/containers.pyx +++ b/src/dependency_injector/containers.pyx @@ -1,34 +1,52 @@ """Containers module.""" -import asyncio -import contextlib -import copy as copy_module -import json -import importlib -import inspect +from asyncio import gather +from contextlib import suppress +from copy import deepcopy as copy_deepcopy +from importlib import import_module +from inspect import getmodule, stack as inspect_stack +from json import load as json_load try: import yaml except ImportError: yaml = None -from . import providers, errors +from . import errors, providers + from .providers cimport __is_future_or_coroutine -from .wiring import wire, unwire + +from .wiring import unwire, wire class WiringConfiguration: """Container wiring configuration.""" - def __init__(self, modules=None, packages=None, from_package=None, auto_wire=True, keep_cache=False): + def __init__( + self, + modules=None, + packages=None, + from_package=None, + auto_wire=True, + keep_cache=False, + warn_unresolved=False, + ): self.modules = [*modules] if modules else [] self.packages = [*packages] if packages else [] self.from_package = from_package self.auto_wire = auto_wire self.keep_cache = keep_cache + self.warn_unresolved = warn_unresolved def __deepcopy__(self, memo=None): - return self.__class__(self.modules, self.packages, self.from_package, self.auto_wire, self.keep_cache) + return self.__class__( + self.modules, + self.packages, + self.from_package, + self.auto_wire, + self.keep_cache, + self.warn_unresolved, + ) class Container: @@ -97,7 +115,7 @@ class DynamicContainer(Container): copied.provider_type = providers.Provider copied.overridden = providers.deepcopy(self.overridden, memo) - copied.wiring_config = copy_module.deepcopy(self.wiring_config, memo) + copied.wiring_config = copy_deepcopy(self.wiring_config, memo) copied.declarative_parent = self.declarative_parent for name, provider in providers.deepcopy(self.providers, memo).items(): @@ -259,7 +277,14 @@ class DynamicContainer(Container): """Check if auto wiring is needed.""" return self.wiring_config.auto_wire is True - def wire(self, modules=None, packages=None, from_package=None, keep_cache=None): + def wire( + self, + modules=None, + packages=None, + from_package=None, + keep_cache=None, + warn_unresolved=False, + ): """Wire container providers with provided packages and modules. :rtype: None @@ -278,10 +303,10 @@ class DynamicContainer(Container): from_package = self.wiring_config.from_package elif self.declarative_parent is not None \ and (self.wiring_config.modules or self.wiring_config.packages): - with contextlib.suppress(Exception): + with suppress(Exception): from_package = _resolve_package_name_from_cls(self.declarative_parent) else: - with contextlib.suppress(Exception): + with suppress(Exception): from_package = _resolve_calling_package_name() modules = _resolve_string_imports(modules, from_package) @@ -298,6 +323,7 @@ class DynamicContainer(Container): modules=modules, packages=packages, keep_cache=keep_cache, + warn_unresolved=warn_unresolved, ) if modules: @@ -315,10 +341,10 @@ class DynamicContainer(Container): self.wired_to_modules.clear() self.wired_to_packages.clear() - def init_resources(self, resource_type=providers.Resource): + def init_resources(self, resource_type=providers.BaseResource): """Initialize all container resources.""" - if not issubclass(resource_type, providers.Resource): + if not issubclass(resource_type, providers.BaseResource): raise TypeError("resource_type must be a subclass of Resource provider") futures = [] @@ -330,12 +356,12 @@ class DynamicContainer(Container): futures.append(resource) if futures: - return asyncio.gather(*futures) + return gather(*futures) - def shutdown_resources(self, resource_type=providers.Resource): + def shutdown_resources(self, resource_type=providers.BaseResource): """Shutdown all container resources.""" - if not issubclass(resource_type, providers.Resource): + if not issubclass(resource_type, providers.BaseResource): raise TypeError("resource_type must be a subclass of Resource provider") def _independent_resources(resources): @@ -358,7 +384,7 @@ class DynamicContainer(Container): result = resource.shutdown() if __is_future_or_coroutine(result): futures.append(result) - await asyncio.gather(*futures) + await gather(*futures) def _sync_ordered_shutdown(resources): while any(resource.initialized for resource in resources): @@ -445,7 +471,7 @@ class DynamicContainer(Container): def from_json_schema(self, filepath): """Build container providers from JSON schema.""" with open(filepath) as file: - schema = json.load(file) + schema = json_load(file) self.from_schema(schema) def resolve_provider_name(self, provider): @@ -711,7 +737,7 @@ class DeclarativeContainer(Container, metaclass=DeclarativeContainerMetaClass): """ container = cls.instance_type() container.provider_type = cls.provider_type - container.wiring_config = copy_module.deepcopy(cls.wiring_config) + container.wiring_config = copy_deepcopy(cls.wiring_config) container.declarative_parent = cls copied_providers = providers.deepcopy({ **cls.providers, **{"@@self@@": cls.__self__}}) @@ -900,18 +926,18 @@ cpdef bint _any_relative_string_imports_in(object modules): cpdef list _resolve_string_imports(object modules, object from_package): return [ - importlib.import_module(module, from_package) if isinstance(module, str) else module + import_module(module, from_package) if isinstance(module, str) else module for module in modules ] cpdef object _resolve_calling_package_name(): - stack = inspect.stack() + stack = inspect_stack() pre_last_frame = stack[0] - module = inspect.getmodule(pre_last_frame[0]) + module = getmodule(pre_last_frame[0]) return module.__package__ cpdef object _resolve_package_name_from_cls(cls): - module = importlib.import_module(cls.__module__) + module = import_module(cls.__module__) return module.__package__ diff --git a/src/dependency_injector/providers.pxd b/src/dependency_injector/providers.pxd index 21ed7f22..4332118d 100644 --- a/src/dependency_injector/providers.pxd +++ b/src/dependency_injector/providers.pxd @@ -1,7 +1,7 @@ """Providers module.""" -import asyncio -import functools +from asyncio import Future, ensure_future, gather +from functools import partial cimport cython @@ -224,11 +224,20 @@ cdef class Dict(Provider): cpdef object _provide(self, tuple args, dict kwargs) -cdef class Resource(Provider): +cdef class ResourceState: + cdef object resource + cdef object shutdowner + cdef bint shutdowner_is_async + cdef bint is_async + cdef bint async_done + + cdef void from_coro(self, coro, error_callback) + cdef void from_async_context_manager(self, acm, error_callback) + cdef object from_context_manager(self, cm, error_callback) + + +cdef class BaseResource(Provider): cdef object _provides - cdef bint _initialized - cdef object _shutdowner - cdef object _resource cdef tuple _args cdef int _args_len @@ -237,6 +246,16 @@ cdef class Resource(Provider): cdef int _kwargs_len cpdef object _provide(self, tuple args, dict kwargs) + cdef void set_state(self, ResourceState state) + cdef ResourceState get_state(self) + + +cdef class Resource(BaseResource): + cdef ResourceState _state + + +cdef class ContextLocalResource(BaseResource): + cdef object _cvar cdef class Container(Provider): @@ -469,18 +488,18 @@ cdef inline object __provide_keyword_args( cdef inline object __combine_future_injections(object injections, list future_injections): - future_result = asyncio.Future() + future_result = Future() - injections_ready = asyncio.gather(*[value for _, value in future_injections]) + injections_ready = gather(*[value for _, value in future_injections]) injections_ready.add_done_callback( - functools.partial( + partial( __async_prepare_args_kwargs_callback, future_result, injections, future_injections, ), ) - asyncio.ensure_future(injections_ready) + ensure_future(injections_ready) return future_result @@ -523,16 +542,16 @@ cdef inline object __provide_attributes(tuple attributes, int attributes_len): cdef inline object __async_inject_attributes(future_instance, future_attributes): - future_result = asyncio.Future() + future_result = Future() - attributes_ready = asyncio.gather(future_instance, future_attributes) + attributes_ready = gather(future_instance, future_attributes) attributes_ready.add_done_callback( - functools.partial( + partial( __async_inject_attributes_callback, future_result, ), ) - asyncio.ensure_future(attributes_ready) + ensure_future(attributes_ready) return future_result @@ -587,17 +606,17 @@ cdef inline object __call( future_args = args if is_future_args else __future_result(args) future_kwargs = kwargs if is_future_kwargs else __future_result(kwargs) - future_result = asyncio.Future() + future_result = Future() - args_kwargs_ready = asyncio.gather(future_args, future_kwargs) + args_kwargs_ready = gather(future_args, future_kwargs) args_kwargs_ready.add_done_callback( - functools.partial( + partial( __async_call_callback, future_result, call, ), ) - asyncio.ensure_future(args_kwargs_ready) + ensure_future(args_kwargs_ready) return future_result @@ -612,8 +631,8 @@ cdef inline void __async_call_callback(object future_result, object call, object future_result.set_exception(exception) else: if __is_future_or_coroutine(result): - result = asyncio.ensure_future(result) - result.add_done_callback(functools.partial(__async_result_callback, future_result)) + result = ensure_future(result) + result.add_done_callback(partial(__async_result_callback, future_result)) return future_result.set_result(result) @@ -694,7 +713,7 @@ cdef inline bint __iscoroutine(object obj): cdef inline object __future_result(object instance): - future_result = asyncio.Future() + future_result = Future() future_result.set_result(instance) return future_result diff --git a/src/dependency_injector/providers.pyi b/src/dependency_injector/providers.pyi index e4d62506..e8542eeb 100644 --- a/src/dependency_injector/providers.pyi +++ b/src/dependency_injector/providers.pyi @@ -1,35 +1,41 @@ from __future__ import annotations +from contextlib import AbstractAsyncContextManager, AbstractContextManager from pathlib import Path from typing import ( + Any, + AsyncIterator as _AsyncIterator, Awaitable, - TypeVar, - Generic, - Type, Callable as _Callable, - Any, - Tuple, - List as _List, - Dict as _Dict, - Optional, - Union, Coroutine as _Coroutine, + Dict as _Dict, + Generator as _Generator, + Generic, Iterable as _Iterable, Iterator as _Iterator, - AsyncIterator as _AsyncIterator, - Generator as _Generator, + List as _List, + Mapping, + Optional, + Tuple, + Type, + Union, overload, ) +from typing_extensions import Self as _Self, TypeVar + try: import yaml except ImportError: yaml = None try: - import pydantic + from pydantic_settings import BaseSettings as PydanticSettings except ImportError: - pydantic = None + try: + from pydantic import BaseSettings as PydanticSettings + except ImportError: + PydanticSettings = Any from . import resources @@ -37,6 +43,7 @@ Injection = Any ProviderParent = Union["Provider", Any] T = TypeVar("T") TT = TypeVar("TT") +T_Any = TypeVar("T_Any", default=Any) P = TypeVar("P", bound="Provider") BS = TypeVar("BS", bound="BaseSingleton") @@ -65,7 +72,7 @@ class Provider(Generic[T]): @property def provider(self) -> Provider[T]: ... @property - def provided(self) -> ProvidedInstance[T]: ... + def provided(self) -> ProvidedInstance: ... def enable_async_mode(self) -> None: ... def disable_async_mode(self) -> None: ... def reset_async_mode(self) -> None: ... @@ -85,7 +92,7 @@ class Object(Provider[T]): def __init__(self, provides: Optional[T] = None) -> None: ... @property def provides(self) -> Optional[T]: ... - def set_provides(self, provides: Optional[T]) -> Object: ... + def set_provides(self, provides: Optional[T]) -> _Self: ... class Self(Provider[T]): def __init__(self, container: Optional[T] = None) -> None: ... @@ -98,12 +105,12 @@ class Delegate(Provider[Provider]): def __init__(self, provides: Optional[Provider] = None) -> None: ... @property def provides(self) -> Optional[Provider]: ... - def set_provides(self, provides: Optional[Provider]) -> Delegate: ... + def set_provides(self, provides: Optional[Provider]) -> _Self: ... class Aggregate(Provider[T]): def __init__( self, - provider_dict: Optional[_Dict[Any, Provider[T]]] = None, + provider_dict: Optional[Mapping[Any, Provider[T]]] = None, **provider_kwargs: Provider[T], ): ... def __getattr__(self, provider_name: Any) -> Provider[T]: ... @@ -122,9 +129,9 @@ class Aggregate(Provider[T]): def providers(self) -> _Dict[Any, Provider[T]]: ... def set_providers( self, - provider_dict: Optional[_Dict[Any, Provider[T]]] = None, + provider_dict: Optional[Mapping[Any, Provider[T]]] = None, **provider_kwargs: Provider[T], - ) -> Aggregate[T]: ... + ) -> _Self: ... class Dependency(Provider[T]): def __init__( @@ -135,10 +142,10 @@ class Dependency(Provider[T]): def __getattr__(self, name: str) -> Any: ... @property def instance_of(self) -> Type[T]: ... - def set_instance_of(self, instance_of: Type[T]) -> Dependency[T]: ... + def set_instance_of(self, instance_of: Type[T]) -> _Self: ... @property def default(self) -> Provider[T]: ... - def set_default(self, default: Optional[Union[Provider, Any]]) -> Dependency[T]: ... + def set_default(self, default: Optional[Union[Provider, Any]]) -> _Self: ... @property def is_defined(self) -> bool: ... def provided_by(self, provider: Provider) -> OverridingContext[P]: ... @@ -162,28 +169,28 @@ class DependenciesContainer(Object): def parent_name(self) -> Optional[str]: ... def assign_parent(self, parent: ProviderParent) -> None: ... -class Callable(Provider[T]): +class Callable(Provider[T_Any]): def __init__( self, - provides: Optional[Union[_Callable[..., T], str]] = None, + provides: Optional[Union[_Callable[..., T_Any], str]] = None, *args: Injection, **kwargs: Injection, ) -> None: ... @property - def provides(self) -> Optional[_Callable[..., T]]: ... + def provides(self) -> Optional[_Callable[..., T_Any]]: ... def set_provides( - self, provides: Optional[Union[_Callable[..., T], str]] - ) -> Callable[T]: ... + self, provides: Optional[Union[_Callable[..., T_Any], str]] + ) -> _Self: ... @property def args(self) -> Tuple[Injection]: ... - def add_args(self, *args: Injection) -> Callable[T]: ... - def set_args(self, *args: Injection) -> Callable[T]: ... - def clear_args(self) -> Callable[T]: ... + def add_args(self, *args: Injection) -> _Self: ... + def set_args(self, *args: Injection) -> _Self: ... + def clear_args(self) -> _Self: ... @property - def kwargs(self) -> _Dict[Any, Injection]: ... - def add_kwargs(self, **kwargs: Injection) -> Callable[T]: ... - def set_kwargs(self, **kwargs: Injection) -> Callable[T]: ... - def clear_kwargs(self) -> Callable[T]: ... + def kwargs(self) -> _Dict[str, Injection]: ... + def add_kwargs(self, **kwargs: Injection) -> _Self: ... + def set_kwargs(self, **kwargs: Injection) -> _Self: ... + def clear_kwargs(self) -> _Self: ... class DelegatedCallable(Callable[T]): ... @@ -193,7 +200,7 @@ class AbstractCallable(Callable[T]): class CallableDelegate(Delegate): def __init__(self, callable: Callable) -> None: ... -class Coroutine(Callable[T]): ... +class Coroutine(Callable[T_Any]): ... class DelegatedCoroutine(Coroutine[T]): ... class AbstractCoroutine(Coroutine[T]): @@ -205,7 +212,7 @@ class CoroutineDelegate(Delegate): class ConfigurationOption(Provider[Any]): UNDEFINED: object def __init__(self, name: Tuple[str], root: Configuration) -> None: ... - def __enter__(self) -> ConfigurationOption: ... + def __enter__(self) -> _Self: ... def __exit__(self, *exc_info: Any) -> None: ... def __getattr__(self, item: str) -> ConfigurationOption: ... def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ... @@ -268,32 +275,28 @@ class Configuration(Object[Any]): ini_files: Optional[_Iterable[Union[Path, str]]] = None, yaml_files: Optional[_Iterable[Union[Path, str]]] = None, json_files: Optional[_Iterable[Union[Path, str]]] = None, - pydantic_settings: Optional[_Iterable[PydanticSettings]] = None, + pydantic_settings: Optional[_Iterable[Union[PydanticSettings, Type[PydanticSettings]]]] = None, ) -> None: ... - def __enter__(self) -> Configuration: ... + def __enter__(self) -> _Self: ... def __exit__(self, *exc_info: Any) -> None: ... def __getattr__(self, item: str) -> ConfigurationOption: ... def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ... def get_name(self) -> str: ... - def set_name(self, name: str) -> Configuration: ... + def set_name(self, name: str) -> _Self: ... def get_default(self) -> _Dict[Any, Any]: ... - def set_default(self, default: _Dict[Any, Any]): ... + def set_default(self, default: _Dict[Any, Any]) -> _Self: ... def get_strict(self) -> bool: ... - def set_strict(self, strict: bool) -> Configuration: ... + def set_strict(self, strict: bool) -> _Self: ... def get_children(self) -> _Dict[str, ConfigurationOption]: ... - def set_children( - self, children: _Dict[str, ConfigurationOption] - ) -> Configuration: ... + def set_children(self, children: _Dict[str, ConfigurationOption]) -> _Self: ... def get_ini_files(self) -> _List[Union[Path, str]]: ... - def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... + def set_ini_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ... def get_yaml_files(self) -> _List[Union[Path, str]]: ... - def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... + def set_yaml_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ... def get_json_files(self) -> _List[Union[Path, str]]: ... - def set_json_files(self, files: _Iterable[Union[Path, str]]) -> Configuration: ... - def get_pydantic_settings(self) -> _List[PydanticSettings]: ... - def set_pydantic_settings( - self, settings: _Iterable[PydanticSettings] - ) -> Configuration: ... + def set_json_files(self, files: _Iterable[Union[Path, str]]) -> _Self: ... + def get_pydantic_settings(self) -> _List[Union[PydanticSettings, Type[PydanticSettings]]]: ... + def set_pydantic_settings(self, settings: _Iterable[Union[PydanticSettings, Type[PydanticSettings]]]) -> _Self: ... def load(self, required: bool = False, envs_required: bool = False) -> None: ... def get(self, selector: str) -> Any: ... def set(self, selector: str, value: Any) -> OverridingContext[P]: ... @@ -319,7 +322,7 @@ class Configuration(Object[Any]): envs_required: bool = False, ) -> None: ... def from_pydantic( - self, settings: PydanticSettings, required: bool = False, **kwargs: Any + self, settings: Union[PydanticSettings, Type[PydanticSettings]], required: bool = False, **kwargs: Any ) -> None: ... def from_dict(self, options: _Dict[str, Any], required: bool = False) -> None: ... def from_env( @@ -345,22 +348,22 @@ class Factory(Provider[T]): def provides(self) -> Optional[_Callable[..., T]]: ... def set_provides( self, provides: Optional[Union[_Callable[..., T], str]] - ) -> Factory[T]: ... + ) -> _Self: ... @property def args(self) -> Tuple[Injection]: ... - def add_args(self, *args: Injection) -> Factory[T]: ... - def set_args(self, *args: Injection) -> Factory[T]: ... - def clear_args(self) -> Factory[T]: ... + def add_args(self, *args: Injection) -> _Self: ... + def set_args(self, *args: Injection) -> _Self: ... + def clear_args(self) -> _Self: ... @property - def kwargs(self) -> _Dict[Any, Injection]: ... - def add_kwargs(self, **kwargs: Injection) -> Factory[T]: ... - def set_kwargs(self, **kwargs: Injection) -> Factory[T]: ... - def clear_kwargs(self) -> Factory[T]: ... + def kwargs(self) -> _Dict[str, Injection]: ... + def add_kwargs(self, **kwargs: Injection) -> _Self: ... + def set_kwargs(self, **kwargs: Injection) -> _Self: ... + def clear_kwargs(self) -> _Self: ... @property - def attributes(self) -> _Dict[Any, Injection]: ... - def add_attributes(self, **kwargs: Injection) -> Factory[T]: ... - def set_attributes(self, **kwargs: Injection) -> Factory[T]: ... - def clear_attributes(self) -> Factory[T]: ... + def attributes(self) -> _Dict[str, Injection]: ... + def add_attributes(self, **kwargs: Injection) -> _Self: ... + def set_attributes(self, **kwargs: Injection) -> _Self: ... + def clear_attributes(self) -> _Self: ... class DelegatedFactory(Factory[T]): ... @@ -376,7 +379,7 @@ class FactoryAggregate(Aggregate[T]): def factories(self) -> _Dict[Any, Factory[T]]: ... def set_factories( self, - provider_dict: Optional[_Dict[Any, Factory[T]]] = None, + provider_dict: Optional[Mapping[Any, Factory[T]]] = None, **provider_kwargs: Factory[T], ) -> FactoryAggregate[T]: ... @@ -394,22 +397,22 @@ class BaseSingleton(Provider[T]): def provides(self) -> Optional[_Callable[..., T]]: ... def set_provides( self, provides: Optional[Union[_Callable[..., T], str]] - ) -> BaseSingleton[T]: ... + ) -> _Self: ... @property def args(self) -> Tuple[Injection]: ... - def add_args(self, *args: Injection) -> BaseSingleton[T]: ... - def set_args(self, *args: Injection) -> BaseSingleton[T]: ... - def clear_args(self) -> BaseSingleton[T]: ... - @property - def kwargs(self) -> _Dict[Any, Injection]: ... - def add_kwargs(self, **kwargs: Injection) -> BaseSingleton[T]: ... - def set_kwargs(self, **kwargs: Injection) -> BaseSingleton[T]: ... - def clear_kwargs(self) -> BaseSingleton[T]: ... - @property - def attributes(self) -> _Dict[Any, Injection]: ... - def add_attributes(self, **kwargs: Injection) -> BaseSingleton[T]: ... - def set_attributes(self, **kwargs: Injection) -> BaseSingleton[T]: ... - def clear_attributes(self) -> BaseSingleton[T]: ... + def add_args(self, *args: Injection) -> _Self: ... + def set_args(self, *args: Injection) -> _Self: ... + def clear_args(self) -> _Self: ... + @property + def kwargs(self) -> _Dict[str, Injection]: ... + def add_kwargs(self, **kwargs: Injection) -> _Self: ... + def set_kwargs(self, **kwargs: Injection) -> _Self: ... + def clear_kwargs(self) -> _Self: ... + @property + def attributes(self) -> _Dict[str, Injection]: ... + def add_attributes(self, **kwargs: Injection) -> _Self: ... + def set_attributes(self, **kwargs: Injection) -> _Self: ... + def clear_attributes(self) -> _Self: ... def reset(self) -> SingletonResetContext[BS]: ... def full_reset(self) -> SingletonFullResetContext[BS]: ... @@ -431,25 +434,25 @@ class List(Provider[_List]): def __init__(self, *args: Injection): ... @property def args(self) -> Tuple[Injection]: ... - def add_args(self, *args: Injection) -> List[T]: ... - def set_args(self, *args: Injection) -> List[T]: ... - def clear_args(self) -> List[T]: ... + def add_args(self, *args: Injection) -> _Self: ... + def set_args(self, *args: Injection) -> _Self: ... + def clear_args(self) -> _Self: ... class Dict(Provider[_Dict]): def __init__( - self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection + self, dict_: Optional[Mapping[Any, Injection]] = None, **kwargs: Injection ): ... @property def kwargs(self) -> _Dict[Any, Injection]: ... def add_kwargs( - self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection - ) -> Dict: ... + self, dict_: Optional[Mapping[Any, Injection]] = None, **kwargs: Injection + ) -> _Self: ... def set_kwargs( - self, dict_: Optional[_Dict[Any, Injection]] = None, **kwargs: Injection - ) -> Dict: ... - def clear_kwargs(self) -> Dict: ... + self, dict_: Optional[Mapping[Any, Injection]] = None, **kwargs: Injection + ) -> _Self: ... + def clear_kwargs(self) -> _Self: ... -class Resource(Provider[T]): +class BaseResource(Provider[T]): @overload def __init__( self, @@ -465,6 +468,20 @@ class Resource(Provider[T]): **kwargs: Injection, ) -> None: ... @overload + def __init__( + self, + provides: Optional[_Callable[..., AbstractContextManager[T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... + @overload + def __init__( + self, + provides: Optional[_Callable[..., AbstractAsyncContextManager[T]]] = None, + *args: Injection, + **kwargs: Injection, + ) -> None: ... + @overload def __init__( self, provides: Optional[_Callable[..., _Iterator[T]]] = None, @@ -494,22 +511,25 @@ class Resource(Provider[T]): ) -> None: ... @property def provides(self) -> Optional[_Callable[..., Any]]: ... - def set_provides(self, provides: Optional[Any]) -> Resource[T]: ... + def set_provides(self, provides: Optional[Any]) -> _Self: ... @property def args(self) -> Tuple[Injection]: ... - def add_args(self, *args: Injection) -> Resource[T]: ... - def set_args(self, *args: Injection) -> Resource[T]: ... - def clear_args(self) -> Resource[T]: ... + def add_args(self, *args: Injection) -> _Self: ... + def set_args(self, *args: Injection) -> _Self: ... + def clear_args(self) -> _Self: ... @property - def kwargs(self) -> _Dict[Any, Injection]: ... - def add_kwargs(self, **kwargs: Injection) -> Resource[T]: ... - def set_kwargs(self, **kwargs: Injection) -> Resource[T]: ... - def clear_kwargs(self) -> Resource[T]: ... + def kwargs(self) -> _Dict[str, Injection]: ... + def add_kwargs(self, **kwargs: Injection) -> _Self: ... + def set_kwargs(self, **kwargs: Injection) -> _Self: ... + def clear_kwargs(self) -> _Self: ... @property def initialized(self) -> bool: ... def init(self) -> Optional[Awaitable[T]]: ... def shutdown(self) -> Optional[Awaitable]: ... +class Resource(BaseResource[T]): ... +class ContextLocalResource(BaseResource[T]):... + class Container(Provider[T]): def __init__( self, @@ -527,17 +547,17 @@ class Container(Provider[T]): def parent_name(self) -> Optional[str]: ... def assign_parent(self, parent: ProviderParent) -> None: ... -class Selector(Provider[Any]): +class Selector(Provider[T_Any]): def __init__( self, selector: Optional[_Callable[..., Any]] = None, **providers: Provider ): ... - def __getattr__(self, name: str) -> Provider: ... + def __getattr__(self, name: str) -> Provider[T_Any]: ... @property def selector(self) -> Optional[_Callable[..., Any]]: ... - def set_selector(self, selector: Optional[_Callable[..., Any]]) -> Selector: ... + def set_selector(self, selector: Optional[_Callable[..., Any]]) -> _Self: ... @property - def providers(self) -> _Dict[str, Provider]: ... - def set_providers(self, **providers: Provider) -> Selector: ... + def providers(self) -> _Dict[str, Provider[T_Any]]: ... + def set_providers(self, **providers: Provider) -> _Self: ... class ProvidedInstanceFluentInterface: def __getattr__(self, item: Any) -> AttributeGetter: ... @@ -545,9 +565,7 @@ class ProvidedInstanceFluentInterface: def call(self, *args: Injection, **kwargs: Injection) -> MethodCaller: ... @property def provides(self) -> Optional[Provider]: ... - def set_provides( - self, provides: Optional[Provider] - ) -> ProvidedInstanceFluentInterface: ... + def set_provides(self, provides: Optional[Provider]) -> _Self: ... class ProvidedInstance(Provider, ProvidedInstanceFluentInterface): def __init__(self, provides: Optional[Provider] = None) -> None: ... @@ -558,7 +576,7 @@ class AttributeGetter(Provider, ProvidedInstanceFluentInterface): ) -> None: ... @property def name(self) -> Optional[str]: ... - def set_name(self, name: Optional[str]) -> ProvidedInstanceFluentInterface: ... + def set_name(self, name: Optional[str]) -> _Self: ... class ItemGetter(Provider, ProvidedInstanceFluentInterface): def __init__( @@ -566,7 +584,7 @@ class ItemGetter(Provider, ProvidedInstanceFluentInterface): ) -> None: ... @property def name(self) -> Optional[str]: ... - def set_name(self, name: Optional[str]) -> ProvidedInstanceFluentInterface: ... + def set_name(self, name: Optional[str]) -> _Self: ... class MethodCaller(Provider, ProvidedInstanceFluentInterface): def __init__( @@ -615,8 +633,3 @@ if yaml: else: class YamlLoader: ... - -if pydantic: - PydanticSettings = pydantic.BaseSettings -else: - PydanticSettings = Any diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 43e49d7e..92ea4f37 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -2,24 +2,27 @@ from __future__ import absolute_import -import asyncio import builtins -import copy import errno -import functools -import importlib -import inspect -import json -import os -import re import sys -import threading -import warnings -from asyncio import ensure_future +from asyncio import Future, ensure_future, gather from configparser import ConfigParser as IniConfigParser from contextlib import asynccontextmanager, contextmanager from contextvars import ContextVar -from inspect import isasyncgenfunction, isgeneratorfunction +from copy import deepcopy as copy_deepcopy +from functools import partial +from importlib import import_module +from inspect import ( + getmodule, + isasyncgenfunction, + isgeneratorfunction, + stack as inspect_stack, +) +from json import loads as json_loads +from os import getenv +from re import compile as re_compile +from threading import RLock, local as threading_local +from warnings import warn try: from inspect import _is_coroutine_mark as _is_coroutine_marker @@ -40,8 +43,9 @@ try: except ImportError: yaml = None -has_pydantic_settings = True +has_pydantic_settings = False cdef bint pydantic_v1 = False +cdef object PydanticSettings = None cdef str pydantic_module = "pydantic_settings" cdef str pydantic_extra = "pydantic2" @@ -49,27 +53,30 @@ try: from pydantic_settings import BaseSettings as PydanticSettings except ImportError: try: - # pydantic-settings requires pydantic v2, - # so it is safe to assume that we're dealing with v1: - from pydantic import BaseSettings as PydanticSettings - pydantic_v1 = True - pydantic_module = "pydantic" - pydantic_extra = "pydantic" + import pydantic except ImportError: - # if it is present, ofc - has_pydantic_settings = False + pass + else: + # Avoid triggering deprecation warning in v2+ + if pydantic.VERSION.startswith("1."): + PydanticSettings = pydantic.BaseSettings + pydantic_v1 = True + pydantic_module = "pydantic" + pydantic_extra = "pydantic" + has_pydantic_settings = True +else: + has_pydantic_settings = True from .errors import ( Error, - NoSuchProviderError, NonCopyableArgumentError, + NoSuchProviderError, ) cimport cython - -config_env_marker_pattern = re.compile( +config_env_marker_pattern = re_compile( r"\${(?P[^}^{:]+)(?P:?)(?P.*?)}", ) @@ -81,7 +88,7 @@ cdef str _resolve_config_env_markers(config_content: str, envs_required: bool): env_name = match.group("name") has_default = match.group("separator") == ":" - value = os.getenv(env_name) + value = getenv(env_name) if value is None: if not has_default and envs_required: raise ValueError(f"Missing required environment variable \"{env_name}\"") @@ -128,7 +135,18 @@ cdef int ASYNC_MODE_ENABLED = 1 cdef int ASYNC_MODE_DISABLED = 2 cdef set __iscoroutine_typecache = set() -cdef tuple __COROUTINE_TYPES = asyncio.coroutines._COROUTINE_TYPES +cdef tuple __COROUTINE_TYPES + +try: + from asyncio.coroutines import _COROUTINE_TYPES + + __COROUTINE_TYPES = _COROUTINE_TYPES +except ImportError: + from collections.abc import Coroutine as ABCCoroutine + from types import CoroutineType + + __COROUTINE_TYPES = (CoroutineType, ABCCoroutine) + cdef dict pydantic_settings_to_dict(settings, dict kwargs): if not has_pydantic_settings: @@ -139,10 +157,7 @@ cdef dict pydantic_settings_to_dict(settings, dict kwargs): ) if isinstance(settings, type) and issubclass(settings, PydanticSettings): - raise Error( - "Got settings class, but expect instance: " - "instead \"{0}\" use \"{0}()\"".format(settings.__name__) - ) + settings = settings() if not isinstance(settings, PydanticSettings): raise Error( @@ -206,7 +221,7 @@ cdef class Provider: __IS_PROVIDER__ = True - overriding_lock = threading.RLock() + overriding_lock = RLock() """Overriding reentrant lock. :type: :py:class:`threading.RLock` @@ -378,7 +393,7 @@ cdef class Provider: :rtype: :py:class:`Delegate` """ - warnings.warn( + warn( "Method \".delegate()\" is deprecated since version 4.0.0. " "Use \".provider\" attribute instead.", category=DeprecationWarning, @@ -453,15 +468,10 @@ cdef class Provider: cdef class Object(Provider): """Object provider returns provided instance "as is". - .. py:attribute:: provides - - Value that have to be provided. - - :type: object + :param provides: Value to be provided. """ def __init__(self, provides=None): - """Initialize provider.""" self._provides = None self.set_provides(provides) super(Object, self).__init__() @@ -576,15 +586,11 @@ cdef class Self(Provider): cdef class Delegate(Provider): """Delegate provider returns provider "as is". - .. py:attribute:: provides - - Value that have to be provided. - - :type: object + :param provides: Provider to be returned as is. + :type provides: Provider | None """ def __init__(self, provides=None): - """Initialize provider.""" self._provides = None self.set_provides(provides) super(Delegate, self).__init__() @@ -825,9 +831,9 @@ cdef class Dependency(Provider): return result elif self._async_mode == ASYNC_MODE_ENABLED: if __is_future_or_coroutine(result): - future_result = asyncio.Future() - result = asyncio.ensure_future(result) - result.add_done_callback(functools.partial(self._async_provide, future_result)) + future_result = Future() + result = ensure_future(result) + result.add_done_callback(partial(self._async_provide, future_result)) return future_result else: self._check_instance_type(result) @@ -836,9 +842,9 @@ cdef class Dependency(Provider): if __is_future_or_coroutine(result): self.enable_async_mode() - future_result = asyncio.Future() - result = asyncio.ensure_future(result) - result.add_done_callback(functools.partial(self._async_provide, future_result)) + future_result = Future() + result = ensure_future(result) + result.add_done_callback(partial(self._async_provide, future_result)) return future_result else: self.disable_async_mode() @@ -1395,19 +1401,11 @@ cdef class AbstractCallable(Callable): cdef class CallableDelegate(Delegate): """Callable delegate injects delegating callable "as is". - .. py:attribute:: provides - - Value that have to be provided. - - :type: object + :param callable: :class:`Callable` provider to be returned as is. + :type callable: Callable """ def __init__(self, callable): - """Initializer. - - :param callable: Value that have to be provided. - :type callable: object - """ if isinstance(callable, Callable) is False: raise Error("{0} can wrap only {1} providers".format(self.__class__, Callable)) super(CallableDelegate, self).__init__(callable) @@ -1443,7 +1441,7 @@ cdef class Coroutine(Callable): def set_provides(self, provides): """Set provider provides.""" provides = _resolve_string_import(provides) - if provides and not asyncio.iscoroutinefunction(provides): + if provides and not iscoroutinefunction(provides): raise Error(f"Provider {_class_qualname(self)} expected to get coroutine function, " f"got {provides} instead") return super().set_provides(provides) @@ -1502,19 +1500,11 @@ cdef class AbstractCoroutine(Coroutine): cdef class CoroutineDelegate(Delegate): """Coroutine delegate injects delegating coroutine "as is". - .. py:attribute:: provides - - Value that have to be provided. - - :type: object + :param coroutine: :class:`Coroutine` provider to be returned as is. + :type coroutine: Coroutine """ def __init__(self, coroutine): - """Initializer. - - :param coroutine: Value that have to be provided. - :type coroutine: object - """ if isinstance(coroutine, Coroutine) is False: raise Error("{0} can wrap only {1} providers".format(self.__class__, Callable)) super(CoroutineDelegate, self).__init__(coroutine) @@ -1599,7 +1589,7 @@ cdef class ConfigurationOption(Provider): return self._root def get_name(self): - return ".".join((self._root.get_name(), self._get_self_name())) + return f"{self._root.get_name()}.{self._get_self_name()}" def get_name_segments(self): return self._name @@ -1776,7 +1766,7 @@ cdef class ConfigurationOption(Provider): config_content, envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), ) - config = json.loads(config_content) + config = json_loads(config_content) current_config = self.__call__() if not current_config: @@ -1849,7 +1839,7 @@ cdef class ConfigurationOption(Provider): :rtype: None """ - value = os.environ.get(name, default) + value = getenv(name, default) if value is UNDEFINED: if required is not False \ @@ -2331,7 +2321,7 @@ cdef class Configuration(Object): config_content, envs_required if envs_required is not UNDEFINED else self._is_strict_mode_enabled(), ) - config = json.loads(config_content) + config = json_loads(config_content) current_config = self.__call__() if not current_config: @@ -2398,7 +2388,7 @@ cdef class Configuration(Object): :rtype: None """ - value = os.environ.get(name, default) + value = getenv(name, default) if value is UNDEFINED: if required is not False \ @@ -2726,19 +2716,11 @@ cdef class AbstractFactory(Factory): cdef class FactoryDelegate(Delegate): """Factory delegate injects delegating factory "as is". - .. py:attribute:: provides - - Value that have to be provided. - - :type: object + :param factory: :class:`Factory` provider to be returned as is. + :type factory: Factory """ def __init__(self, factory): - """Initializer. - - :param factory: Value that have to be provided. - :type factory: object - """ if isinstance(factory, Factory) is False: raise Error("{0} can wrap only {1} providers".format(self.__class__, Factory)) super(FactoryDelegate, self).__init__(factory) @@ -3011,7 +2993,7 @@ cdef class Singleton(BaseSingleton): :rtype: None """ if __is_future_or_coroutine(self._storage): - asyncio.ensure_future(self._storage).cancel() + ensure_future(self._storage).cancel() self._storage = None return SingletonResetContext(self) @@ -3021,9 +3003,9 @@ cdef class Singleton(BaseSingleton): instance = __factory_call(self._instantiator, args, kwargs) if __is_future_or_coroutine(instance): - future_result = asyncio.Future() - instance = asyncio.ensure_future(instance) - instance.add_done_callback(functools.partial(self._async_init_instance, future_result)) + future_result = Future() + instance = ensure_future(instance) + instance.add_done_callback(partial(self._async_init_instance, future_result)) self._storage = future_result return future_result @@ -3057,7 +3039,7 @@ cdef class DelegatedSingleton(Singleton): cdef class ThreadSafeSingleton(BaseSingleton): """Thread-safe singleton provider.""" - storage_lock = threading.RLock() + storage_lock = RLock() """Storage reentrant lock. :type: :py:class:`threading.RLock` @@ -3080,7 +3062,7 @@ cdef class ThreadSafeSingleton(BaseSingleton): """ with self._storage_lock: if __is_future_or_coroutine(self._storage): - asyncio.ensure_future(self._storage).cancel() + ensure_future(self._storage).cancel() self._storage = None return SingletonResetContext(self) @@ -3093,9 +3075,9 @@ cdef class ThreadSafeSingleton(BaseSingleton): if self._storage is None: result = __factory_call(self._instantiator, args, kwargs) if __is_future_or_coroutine(result): - future_result = asyncio.Future() - result = asyncio.ensure_future(result) - result.add_done_callback(functools.partial(self._async_init_instance, future_result)) + future_result = Future() + result = ensure_future(result) + result.add_done_callback(partial(self._async_init_instance, future_result)) result = future_result self._storage = result instance = self._storage @@ -3149,7 +3131,7 @@ cdef class ThreadLocalSingleton(BaseSingleton): :param provides: Provided type. :type provides: type """ - self._storage = threading.local() + self._storage = threading_local() super(ThreadLocalSingleton, self).__init__(provides, *args, **kwargs) def reset(self): @@ -3163,7 +3145,7 @@ cdef class ThreadLocalSingleton(BaseSingleton): return SingletonResetContext(self) if __is_future_or_coroutine(instance): - asyncio.ensure_future(instance).cancel() + ensure_future(instance).cancel() del self._storage.instance @@ -3179,14 +3161,14 @@ cdef class ThreadLocalSingleton(BaseSingleton): instance = __factory_call(self._instantiator, args, kwargs) if __is_future_or_coroutine(instance): - future_result = asyncio.Future() - instance = asyncio.ensure_future(instance) - instance.add_done_callback(functools.partial(self._async_init_instance, future_result)) + future_result = Future() + instance = ensure_future(instance) + instance.add_done_callback(partial(self._async_init_instance, future_result)) self._storage.instance = future_result return future_result self._storage.instance = instance - + return instance def _async_init_instance(self, future_result, result): @@ -3241,7 +3223,7 @@ cdef class ContextLocalSingleton(BaseSingleton): return SingletonResetContext(self) if __is_future_or_coroutine(instance): - asyncio.ensure_future(instance).cancel() + ensure_future(instance).cancel() self._storage.set(self._none) @@ -3257,9 +3239,9 @@ cdef class ContextLocalSingleton(BaseSingleton): instance = __factory_call(self._instantiator, args, kwargs) if __is_future_or_coroutine(instance): - future_result = asyncio.Future() - instance = asyncio.ensure_future(instance) - instance.add_done_callback(functools.partial(self._async_init_instance, future_result)) + future_result = Future() + instance = ensure_future(instance) + instance.add_done_callback(partial(self._async_init_instance, future_result)) self._storage.set(future_result) return future_result @@ -3348,19 +3330,11 @@ cdef class AbstractSingleton(BaseSingleton): cdef class SingletonDelegate(Delegate): """Singleton delegate injects delegating singleton "as is". - .. py:attribute:: provides - - Value that have to be provided. - - :type: object + :param singleton: :class:`Singleton` provider to be returned as is. + :type singleton: BaseSingleton """ def __init__(self, singleton): - """Initializer. - - :param singleton: Value that have to be provided. - :type singleton: py:class:`BaseSingleton` - """ if isinstance(singleton, BaseSingleton) is False: raise Error("{0} can wrap only {1} providers".format( self.__class__, BaseSingleton)) @@ -3613,17 +3587,113 @@ cdef class NullAwaitable: cdef NullAwaitable NULL_AWAITABLE = NullAwaitable() -cdef class Resource(Provider): +cdef class ResourceState: + def __cinit__(self, obj, error_callback, /): + self.resource = None + self.shutdowner = None + self.shutdowner_is_async = False + self.is_async = False + self.async_done = False + + def __repr__(self): + return ( + f"{self.__class__.__name__}(" + f"resource={self.resource!r}, " + f"shutdowner={self.shutdowner!r}, " + f"shutdowner_is_async={self.shutdowner_is_async}, " + f"is_async={self.is_async}, " + f"async_done={self.async_done})" + ) + + def __init__(self, obj, error_callback, /): + if __is_future_or_coroutine(obj): + self.from_coro(obj, error_callback) + elif hasattr(obj, "__enter__") and hasattr(obj, "__exit__"): + self.from_context_manager(obj, error_callback) + elif hasattr(obj, "__aenter__") and hasattr(obj, "__aexit__"): + self.from_async_context_manager(obj, error_callback) + else: + self.resource = obj + + async def async_shutdown(self): + if not self.async_done: + await self.resource + + shutdowner = self.shutdowner + self.shutdowner = None + + if shutdowner is not None: + future = shutdowner(None, None, None) + + if self.shutdowner_is_async: + await future + + async def from_awaitable(self, awaitable, error_callback, /): + try: + resource = obj = await awaitable + except: + error_callback() + raise + + if hasattr(obj, "__aenter__") and hasattr(obj, "__aexit__"): + resource = await self._from_async_context_manager(obj, error_callback) + elif hasattr(obj, "__enter__") and hasattr(obj, "__exit__"): + resource = self.from_context_manager(obj, error_callback) + else: + self.resource = obj + + self.async_done = True + + return resource + + cdef void from_coro(self, coro, error_callback): + self.is_async = True + self.resource = ensure_future(self.from_awaitable(coro, error_callback)) + + async def _from_async_context_manager(self, acm, error_callback, /): + try: + self.resource = resource = await acm.__aenter__() + except: + error_callback() + raise + + self.shutdowner = acm.__aexit__ + self.shutdowner_is_async = True + self.async_done = True + return resource + + cdef void from_async_context_manager(self, acm, error_callback): + self.is_async = True + self.resource = ensure_future(self._from_async_context_manager(acm, error_callback)) + + cdef object from_context_manager(self, cm, error_callback): + try: + self.resource = resource = cm.__enter__() + except: + error_callback() + raise + + self.shutdowner = cm.__exit__ + + return resource + + +cdef class BaseResource(Provider): """Resource provider provides a component with initialization and shutdown.""" + cdef void set_state(self, ResourceState state): + raise NotImplementedError + + cdef ResourceState get_state(self): + raise NotImplementedError + + def reset_state(self): + self.set_state(None) + def __init__(self, provides=None, *args, **kwargs): self._provides = None self.set_provides(provides) - self._initialized = False - self._resource = None - self._shutdowner = None - self._args = tuple() self._args_len = 0 self.set_args(*args) @@ -3640,7 +3710,7 @@ cdef class Resource(Provider): if copied is not None: return copied - if self._initialized: + if self.get_state() is not None: raise Error("Can not copy initialized resource") copied = _memorized_duplicate(self, memo) @@ -3763,7 +3833,7 @@ cdef class Resource(Provider): @property def initialized(self): """Check if resource is initialized.""" - return self._initialized + return self.get_state() is not None def init(self): """Initialize resource.""" @@ -3771,20 +3841,18 @@ cdef class Resource(Provider): def shutdown(self): """Shutdown resource.""" - if not self._initialized: - if self._async_mode == ASYNC_MODE_ENABLED: - return NULL_AWAITABLE - return - if self._shutdowner: - future = self._shutdowner(None, None, None) + state = self.get_state() - if __is_future_or_coroutine(future): - return ensure_future(self._shutdown_async(future)) + if state is not None: + self.set_state(None) - self._resource = None - self._initialized = False - self._shutdowner = None + if state.is_async: + return state.async_shutdown() + elif state.shutdowner is not None: + result = state.shutdowner(None, None, None) + + assert not __isfuture(result), "sync resource with async shutdowner" if self._async_mode == ASYNC_MODE_ENABLED: return NULL_AWAITABLE @@ -3797,45 +3865,11 @@ cdef class Resource(Provider): yield from filter(is_provider, self.kwargs.values()) yield from super().related - async def _shutdown_async(self, future) -> None: - try: - await future - finally: - self._resource = None - self._initialized = False - self._shutdowner = 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 - - async def _provide_async(self, future) -> None: - try: - obj = await future - - if hasattr(obj, '__aenter__') and hasattr(obj, '__aexit__'): - self._resource = await obj.__aenter__() - self._shutdowner = obj.__aexit__ - elif hasattr(obj, '__enter__') and hasattr(obj, '__exit__'): - self._resource = obj.__enter__() - 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 + state = self.get_state() + + if state is not None: + return state.resource obj = __call( self._provides, @@ -3848,23 +3882,31 @@ cdef class Resource(Provider): 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 + state = ResourceState(obj, self.reset_state) + + self.set_state(state) + + return state.resource + - self._initialized = True - return self._resource +cdef class Resource(BaseResource): + cdef void set_state(self, ResourceState state): + self._state = state + + cdef ResourceState get_state(self): + return self._state + + +cdef class ContextLocalResource(BaseResource): + def __init__(self, provides=None, *args, **kwargs): + self._cvar = ContextVar("_cvar", default=None) + super().__init__(provides, *args, **kwargs) + + cdef void set_state(self, ResourceState state): + self._cvar.set(state) + + cdef ResourceState get_state(self): + return self._cvar.get() cdef class Container(Provider): @@ -4273,9 +4315,9 @@ cdef class AttributeGetter(Provider): cpdef object _provide(self, tuple args, dict kwargs): provided = self.provides(*args, **kwargs) if __is_future_or_coroutine(provided): - future_result = asyncio.Future() - provided = asyncio.ensure_future(provided) - provided.add_done_callback(functools.partial(self._async_provide, future_result)) + future_result = Future() + provided = ensure_future(provided) + provided.add_done_callback(partial(self._async_provide, future_result)) return future_result return getattr(provided, self.name) @@ -4355,9 +4397,9 @@ cdef class ItemGetter(Provider): cpdef object _provide(self, tuple args, dict kwargs): provided = self.provides(*args, **kwargs) if __is_future_or_coroutine(provided): - future_result = asyncio.Future() - provided = asyncio.ensure_future(provided) - provided.add_done_callback(functools.partial(self._async_provide, future_result)) + future_result = Future() + provided = ensure_future(provided) + provided.add_done_callback(partial(self._async_provide, future_result)) return future_result return provided[self.name] @@ -4485,9 +4527,9 @@ cdef class MethodCaller(Provider): cpdef object _provide(self, tuple args, dict kwargs): call = self.provides() if __is_future_or_coroutine(call): - future_result = asyncio.Future() - call = asyncio.ensure_future(call) - call.add_done_callback(functools.partial(self._async_provide, future_result, args, kwargs)) + future_result = Future() + call = ensure_future(call) + call.add_done_callback(partial(self._async_provide, future_result, args, kwargs)) return future_result return __call( call, @@ -4800,7 +4842,7 @@ cpdef object deepcopy(object instance, dict memo=None): __add_sys_streams(memo) - return copy.deepcopy(instance, memo) + return copy_deepcopy(instance, memo) cpdef tuple deepcopy_args( @@ -4817,7 +4859,7 @@ cpdef tuple deepcopy_args( for i, arg in enumerate(args): try: - out.append(copy.deepcopy(arg, memo)) + out.append(copy_deepcopy(arg, memo)) except Exception as e: raise NonCopyableArgumentError(provider, index=i) from e @@ -4838,7 +4880,7 @@ cpdef dict[str, object] deepcopy_kwargs( for name, arg in kwargs.items(): try: - out[name] = copy.deepcopy(arg, memo) + out[name] = copy_deepcopy(arg, memo) except Exception as e: raise NonCopyableArgumentError(provider, keyword=name) from e @@ -4899,22 +4941,10 @@ def traverse(*providers, types=None): yield visiting - -def isawaitable(obj): - """Check if object is a coroutine function.""" - try: - return inspect.isawaitable(obj) - except AttributeError: - return False - - -def iscoroutinefunction(obj): - """Check if object is a coroutine function.""" - try: - return inspect.iscoroutinefunction(obj) - except AttributeError: - return False - +if sys.version_info >= (3, 11): + from inspect import iscoroutinefunction +else: + from asyncio import iscoroutinefunction def _resolve_string_import(provides): if provides is None: @@ -4939,14 +4969,14 @@ def _resolve_string_import(provides): if module_name.startswith(".") and package_name is None: raise ImportError("Attempted relative import with no known parent package") - module = importlib.import_module(module_name, package=package_name) + module = import_module(module_name, package=package_name) return getattr(module, member_name) def _resolve_calling_module(): - stack = inspect.stack() + stack = inspect_stack() pre_last_frame = stack[0] - return inspect.getmodule(pre_last_frame[0]) + return getmodule(pre_last_frame[0]) def _resolve_calling_package_name(): diff --git a/src/dependency_injector/schema.py b/src/dependency_injector/schema.py index 8547ebc2..1c6f0e39 100644 --- a/src/dependency_injector/schema.py +++ b/src/dependency_injector/schema.py @@ -1,7 +1,7 @@ """Schema module.""" import builtins -import importlib +from importlib import import_module from typing import Dict, Any, Type, Optional from . import containers, providers @@ -243,7 +243,7 @@ def _import_string(string_name: str) -> Optional[object]: return None member = segments[-1] - module = importlib.import_module(module_name) + module = import_module(module_name) return getattr(module, member, None) diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 6d5d1510..62267c47 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -1,13 +1,22 @@ """Wiring module.""" -import functools -import importlib import importlib.machinery -import inspect -import pkgutil import sys from contextlib import suppress -from inspect import isbuiltin, isclass +from functools import wraps +from importlib import import_module, invalidate_caches as invalidate_import_caches +from inspect import ( + Parameter, + getmembers, + isasyncgenfunction, + isbuiltin, + isclass, + iscoroutinefunction, + isfunction, + ismethod, + signature as inspect_signature, +) +from pkgutil import walk_packages from types import ModuleType from typing import ( TYPE_CHECKING, @@ -30,9 +39,9 @@ from warnings import warn try: - from typing import Self + from typing import Self, assert_never except ImportError: - from typing_extensions import Self + from typing_extensions import Self, assert_never try: from functools import cache @@ -76,8 +85,22 @@ def extract_marker_from_fastapi(param: Any) -> Any: MARKER_EXTRACTORS.append(extract_marker_from_fastapi) -with suppress(ImportError): - from fast_depends.dependencies import Depends as FastDepends +with suppress(ImportError): # fast_depends >=3.0.0 + from fast_depends.dependencies.model import ( # type: ignore[attr-defined] + Dependant as FastDependant, + ) + + def extract_marker_from_dependant_fast_depends(param: Any) -> Any: + if isinstance(param, FastDependant): + return param.dependency + return None + + MARKER_EXTRACTORS.append(extract_marker_from_dependant_fast_depends) + +with suppress(ImportError): # fast_depends <3.0.0 + from fast_depends.dependencies import ( # type: ignore[attr-defined] + Depends as FastDepends, + ) def extract_marker_from_fast_depends(param: Any) -> Any: if isinstance(param, FastDepends): @@ -104,6 +127,17 @@ def is_werkzeug_local_proxy(obj: Any) -> bool: INSPECT_EXCLUSION_FILTERS.append(is_werkzeug_local_proxy) + +def _is_cyfunction(obj: Any) -> bool: + """Return True for Cython-compiled functions/methods (non-fused).""" + return type(obj).__name__ == "cython_function_or_method" + + +def _is_function_like(obj: Any) -> bool: + """Return True for pure-Python functions and Cython-compiled functions.""" + return isfunction(obj) or _is_cyfunction(obj) + + from . import providers # noqa: E402 __all__ = ( @@ -139,6 +173,10 @@ class DIWiringWarning(RuntimeWarning): """Base class for all warnings raised by the wiring module.""" +class UnresolvedMarkerWarning(DIWiringWarning): + """Warning raised when a marker with string identifier cannot be resolved against container.""" + + class PatchedRegistry: def __init__(self) -> None: @@ -433,6 +471,7 @@ def wire( # noqa: C901 modules: Optional[Iterable[ModuleType]] = None, packages: Optional[Iterable[ModuleType]] = None, keep_cache: bool = False, + warn_unresolved: bool = False, ) -> None: """Wire container providers with provided packages and modules.""" modules = [*modules] if modules else [] @@ -449,10 +488,24 @@ def wire( # noqa: C901 continue if _is_marker(member): - _patch_attribute(module, member_name, member, providers_map) - elif inspect.isfunction(member): - _patch_fn(module, member_name, member, providers_map) - elif inspect.isclass(member): + _patch_attribute( + module, + member_name, + member, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=1, + ) + elif _is_function_like(member): + _patch_fn( + module, + member_name, + member, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=1, + ) + elif isclass(member): cls = member try: cls_members = _get_members_and_annotated(cls) @@ -463,15 +516,30 @@ def wire( # noqa: C901 for cls_member_name, cls_member in cls_members: if _is_marker(cls_member): _patch_attribute( - cls, cls_member_name, cls_member, providers_map + cls, + cls_member_name, + cls_member, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=1, ) elif _is_method(cls_member): _patch_method( - cls, cls_member_name, cls_member, providers_map + cls, + cls_member_name, + cls_member, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=1, ) for patched in _patched_registry.get_callables_from_module(module): - _bind_injections(patched, providers_map) + _bind_injections( + patched, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=1, + ) if not keep_cache: clear_cache() @@ -490,13 +558,11 @@ def unwire( # noqa: C901 modules.extend(_fetch_modules(package)) for module in modules: - for name, member in inspect.getmembers(module): - if inspect.isfunction(member): + for name, member in getmembers(module): + if _is_function_like(member): _unpatch(module, name, member) - elif inspect.isclass(member): - for method_name, method in inspect.getmembers( - member, inspect.isfunction - ): + elif isclass(member): + for method_name, method in getmembers(member, _is_function_like): _unpatch(member, method_name, method) for patched in _patched_registry.get_callables_from_module(module): @@ -524,6 +590,8 @@ def _patch_fn( name: str, fn: Callable[..., Any], providers_map: ProvidersMap, + warn_unresolved: bool = False, + warn_unresolved_stacklevel: int = 0, ) -> None: if not _is_patched(fn): reference_injections, reference_closing = _fetch_reference_injections(fn) @@ -531,7 +599,12 @@ def _patch_fn( return fn = _get_patched(fn, reference_injections, reference_closing) - _bind_injections(fn, providers_map) + _bind_injections( + fn, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=warn_unresolved_stacklevel + 1, + ) setattr(module, name, fn) @@ -541,6 +614,8 @@ def _patch_method( name: str, method: Callable[..., Any], providers_map: ProvidersMap, + warn_unresolved: bool = False, + warn_unresolved_stacklevel: int = 0, ) -> None: if ( hasattr(cls, "__dict__") @@ -558,7 +633,12 @@ def _patch_method( return fn = _get_patched(fn, reference_injections, reference_closing) - _bind_injections(fn, providers_map) + _bind_injections( + fn, + providers_map, + warn_unresolved=warn_unresolved, + warn_unresolved_stacklevel=warn_unresolved_stacklevel + 1, + ) if fn is method: # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/884 @@ -594,9 +674,17 @@ def _patch_attribute( name: str, marker: "_Marker", providers_map: ProvidersMap, + warn_unresolved: bool = False, + warn_unresolved_stacklevel: int = 0, ) -> None: provider = providers_map.resolve_provider(marker.provider, marker.modifier) if provider is None: + if warn_unresolved: + warn( + f"Unresolved marker {name} in {member!r}", + UnresolvedMarkerWarning, + stacklevel=warn_unresolved_stacklevel + 2, + ) return _patched_registry.register_attribute(PatchedAttribute(member, name, marker)) @@ -614,25 +702,20 @@ def _unpatch_attribute(patched: PatchedAttribute) -> None: setattr(patched.member, patched.name, patched.marker) -def _extract_marker(parameter: inspect.Parameter) -> Optional["_Marker"]: +def _extract_marker(parameter: Parameter) -> Optional["_Marker"]: if get_origin(parameter.annotation) is Annotated: - args = get_args(parameter.annotation) - if len(args) > 1: - marker = args[1] - else: - marker = None + candidates = get_args(parameter.annotation)[1:] else: - marker = parameter.default - - for marker_extractor in MARKER_EXTRACTORS: - if _marker := marker_extractor(marker): - marker = _marker - break + candidates = (parameter.default,) - if not isinstance(marker, _Marker): - return None - - return marker + for marker in candidates: + for marker_extractor in MARKER_EXTRACTORS: + if _marker := marker_extractor(marker): + marker = _marker + break + if _is_marker(marker): + return marker + return None @cache @@ -648,7 +731,7 @@ def _fetch_reference_injections( # noqa: C901 fn = fn.__init__ try: - signature = inspect.signature(fn) + signature = inspect_signature(fn) except ValueError as exception: if "no signature found" in str(exception): return {}, {} @@ -673,7 +756,12 @@ def _fetch_reference_injections( # noqa: C901 return injections, closing -def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None: +def _bind_injections( + fn: Callable[..., Any], + providers_map: ProvidersMap, + warn_unresolved: bool = False, + warn_unresolved_stacklevel: int = 0, +) -> None: patched_callable = _patched_registry.get_callable(fn) if patched_callable is None: return @@ -682,6 +770,12 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non provider = providers_map.resolve_provider(marker.provider, marker.modifier) if provider is None: + if warn_unresolved: + warn( + f"Unresolved marker {injection} in {fn.__qualname__}", + UnresolvedMarkerWarning, + stacklevel=warn_unresolved_stacklevel + 2, + ) continue if isinstance(marker, Provide): @@ -695,7 +789,7 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non if injection in patched_callable.reference_closing: patched_callable.add_closing(injection, provider) - for resource in provider.traverse(types=[providers.Resource]): + for resource in provider.traverse(types=[providers.BaseResource]): patched_callable.add_closing(str(id(resource)), resource) @@ -710,17 +804,17 @@ def _fetch_modules(package): modules = [package] if not hasattr(package, "__path__") or not hasattr(package, "__name__"): return modules - for module_info in pkgutil.walk_packages( + for module_info in walk_packages( path=package.__path__, prefix=package.__name__ + ".", ): - module = importlib.import_module(module_info.name) + module = import_module(module_info.name) modules.append(module) return modules def _is_method(member) -> bool: - return inspect.ismethod(member) or inspect.isfunction(member) + return ismethod(member) or _is_function_like(member) def _is_marker(member) -> bool: @@ -738,9 +832,9 @@ def _get_patched( reference_closing=reference_closing, ) - if inspect.iscoroutinefunction(fn): + if iscoroutinefunction(fn): patched = _get_async_patched(fn, patched_object) - elif inspect.isasyncgenfunction(fn): + elif isasyncgenfunction(fn): patched = _get_async_gen_patched(fn, patched_object) else: patched = _get_sync_patched(fn, patched_object) @@ -791,6 +885,9 @@ def modify( ) -> providers.Provider: return provider.as_(self.type_) + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.type_!r})" + def as_int() -> TypeModifier: """Return int type modifier.""" @@ -809,8 +906,8 @@ def as_(type_: Type) -> TypeModifier: class RequiredModifier(Modifier): - def __init__(self) -> None: - self.type_modifier = None + def __init__(self, type_modifier: Optional[TypeModifier] = None) -> None: + self.type_modifier = type_modifier def as_int(self) -> Self: self.type_modifier = TypeModifier(int) @@ -834,6 +931,11 @@ def modify( provider = provider.as_(self.type_modifier.type_) return provider + def __repr__(self) -> str: + if self.type_modifier: + return f"{self.__class__.__name__}({self.type_modifier!r})" + return f"{self.__class__.__name__}()" + def required() -> RequiredModifier: """Return required modifier.""" @@ -853,6 +955,9 @@ def modify( invariant_segment = providers_map.resolve_provider(self.id) return provider[invariant_segment] + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.id!r})" + def invariant(id: str) -> InvariantModifier: """Return invariant modifier.""" @@ -876,8 +981,8 @@ def __getitem__(self, item) -> Self: self.segments.append((self.TYPE_ITEM, item)) return self - def call(self) -> Self: - self.segments.append((self.TYPE_CALL, None)) + def call(self, *args, **kwargs) -> Self: + self.segments.append((self.TYPE_CALL, (args, kwargs))) return self def modify( @@ -892,9 +997,29 @@ def modify( elif type_ == ProvidedInstance.TYPE_ITEM: provider = provider[value] elif type_ == ProvidedInstance.TYPE_CALL: - provider = provider.call() + provider = provider.call(*value[0], **value[1]) + else: + assert_never(type_) return provider + def _format_segments(self) -> str: + segments = [] + for type_, value in self.segments: + if type_ == ProvidedInstance.TYPE_ATTRIBUTE: + segments.append(f".{value}") + elif type_ == ProvidedInstance.TYPE_ITEM: + segments.append(f"[{value!r}]") + elif type_ == ProvidedInstance.TYPE_CALL: + segments.append(".call()") + else: + assert_never(type_) + return "".join(segments) + + __str__ = _format_segments + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(){self._format_segments()}" + def provided() -> ProvidedInstance: """Return provided instance modifier.""" @@ -910,7 +1035,7 @@ def provided() -> ProvidedInstance: ] -if TYPE_CHECKING: +if TYPE_CHECKING: # noqa class _Marker(Protocol): __IS_MARKER__: bool @@ -918,6 +1043,7 @@ class _Marker(Protocol): def __call__(self) -> Self: ... def __getattr__(self, item: str) -> Self: ... def __getitem__(self, item: Any) -> Any: ... + def __repr__(self) -> str: ... Provide: _Marker Provider: _Marker @@ -946,6 +1072,12 @@ def __class_getitem__(cls, item: MarkerItem) -> Self: def __call__(self) -> Self: return self + def __repr__(self) -> str: + cls_name = self.__class__.__name__ + if self.modifier: + return f"{cls_name}[{self.provider!r}, {self.modifier!r}]" + return f"{cls_name}[{self.provider!r}]" + class Provide(_Marker): ... class Provider(_Marker): ... @@ -1012,7 +1144,7 @@ class ExtensionFileLoader(importlib.machinery.ExtensionFileLoader): ... sys.path_hooks.insert(0, self._path_hook) sys.path_importer_cache.clear() - importlib.invalidate_caches() + invalidate_import_caches() def uninstall(self) -> None: if not self.installed: @@ -1020,7 +1152,7 @@ def uninstall(self) -> None: sys.path_hooks.remove(self._path_hook) sys.path_importer_cache.clear() - importlib.invalidate_caches() + invalidate_import_caches() def register_loader_containers(*containers: Container) -> None: @@ -1058,7 +1190,7 @@ def is_loader_installed() -> bool: # Wiring uses the following Python wrapper because there is # no possibility to compile a first-type citizen coroutine in Cython. def _get_async_patched(fn: F, patched: PatchedCallable) -> F: - @functools.wraps(fn) + @wraps(fn) async def _patched(*args: Any, **raw_kwargs: Any) -> Any: resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing) @@ -1069,7 +1201,7 @@ async def _patched(*args: Any, **raw_kwargs: Any) -> Any: def _get_async_gen_patched(fn: F, patched: PatchedCallable) -> F: - @functools.wraps(fn) + @wraps(fn) async def _patched(*args: Any, **raw_kwargs: Any) -> AsyncIterator[Any]: resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing) @@ -1081,7 +1213,7 @@ async def _patched(*args: Any, **raw_kwargs: Any) -> AsyncIterator[Any]: def _get_sync_patched(fn: F, patched: PatchedCallable) -> F: - @functools.wraps(fn) + @wraps(fn) def _patched(*args: Any, **raw_kwargs: Any) -> Any: resolver = DependencyResolver(raw_kwargs, patched.injections, patched.closing) @@ -1091,26 +1223,24 @@ def _patched(*args: Any, **raw_kwargs: Any) -> Any: return cast(F, _patched) -if sys.version_info >= (3, 10): - - def _get_annotations(obj: Any) -> Dict[str, Any]: - return inspect.get_annotations(obj) - -else: - - def _get_annotations(obj: Any) -> Dict[str, Any]: +try: + from inspect import get_annotations +except ImportError: + def get_annotations(obj: Any) -> Dict[str, Any]: return getattr(obj, "__annotations__", {}) def _get_members_and_annotated(obj: Any) -> Iterable[Tuple[str, Any]]: - members = inspect.getmembers(obj) - annotations = _get_annotations(obj) + members = getmembers(obj) + annotations = get_annotations(obj) for annotation_name, annotation in annotations.items(): if get_origin(annotation) is Annotated: args = get_args(annotation) - if len(args) > 1: - member = args[1] - members.append((annotation_name, member)) + # Search through all metadata items (args[1:]) for a DI marker + for arg in args[1:]: + if _is_marker(arg): + members.append((annotation_name, arg)) + break return members diff --git a/tests/typing/aggregate.py b/tests/typing/aggregate.py index 1f134cb0..6518257a 100644 --- a/tests/typing/aggregate.py +++ b/tests/typing/aggregate.py @@ -1,4 +1,5 @@ from dependency_injector import providers +from typing_extensions import assert_type, Any class Animal: ... @@ -8,29 +9,35 @@ class Cat(Animal): ... # Test 1: to check Aggregate provider -provider1: providers.Aggregate[str] = providers.Aggregate( +provider1 = providers.Aggregate( a=providers.Object("str1"), b=providers.Object("str2"), ) -provider_a_1: providers.Provider[str] = provider1.a +provider_a_1 = provider1.a provider_b_1: providers.Provider[str] = provider1.b -val1: str = provider1("a") +val1 = provider1("a") +assert_type(provider1, providers.Aggregate[str]) +assert_type(provider_a_1, providers.Provider[str]) +assert_type(provider_b_1, providers.Provider[str]) +assert_type(val1, str) -provider1_set_non_string_keys: providers.Aggregate[str] = providers.Aggregate() +provider1_set_non_string_keys = providers.Aggregate[str]() provider1_set_non_string_keys.set_providers({Cat: providers.Object("str")}) -provider_set_non_string_1: providers.Provider[str] = ( - provider1_set_non_string_keys.providers[Cat] -) +provider_set_non_string_1 = provider1_set_non_string_keys.providers[Cat] +assert_type(provider_set_non_string_1, providers.Provider[str]) + -provider1_new_non_string_keys: providers.Aggregate[str] = providers.Aggregate( +provider1_new_non_string_keys = providers.Aggregate( {Cat: providers.Object("str")}, ) -factory_new_non_string_1: providers.Provider[str] = ( - provider1_new_non_string_keys.providers[Cat] -) +factory_new_non_string_1 = provider1_new_non_string_keys.providers[Cat] +assert_type(provider1_new_non_string_keys, providers.Aggregate[str]) +assert_type(factory_new_non_string_1, providers.Provider[str]) + provider1_no_explicit_typing = providers.Aggregate(a=providers.Object("str")) -provider1_no_explicit_typing_factory: providers.Provider[str] = ( - provider1_no_explicit_typing.providers["a"] -) -provider1_no_explicit_typing_object: str = provider1_no_explicit_typing("a") +provider1_no_explicit_typing_factory = provider1_no_explicit_typing.providers["a"] +provider1_no_explicit_typing_object = provider1_no_explicit_typing("a") + +assert_type(provider1_no_explicit_typing_factory, providers.Provider[str]) +assert_type(provider1_no_explicit_typing_object, str) diff --git a/tests/typing/callable.py b/tests/typing/callable.py index 8d345d3b..3ee02ac4 100644 --- a/tests/typing/callable.py +++ b/tests/typing/callable.py @@ -1,4 +1,5 @@ -from typing import Any, Callable, Dict, Optional, Tuple, Type +from typing import Any, Callable, Dict, Optional, Tuple +from typing_extensions import assert_type from dependency_injector import providers @@ -7,7 +8,6 @@ class Animal: ... class Cat(Animal): - @classmethod def create(cls) -> Animal: return cls() @@ -15,11 +15,13 @@ def create(cls) -> Animal: # Test 1: to check the return type (class) provider1 = providers.Callable(Cat) -animal1: Animal = provider1(1, 2, 3, b="1", c=2, e=0.0) +cat1 = provider1(1, 2, 3, b="1", c=2, e=0.0) +assert_type(cat1, Cat) # Test 2: to check the return type (class factory method) provider2 = providers.Callable(Cat.create) -animal2: Animal = provider2() +animal2 = provider2() +assert_type(animal2, Animal) # Test 3: to check the .override() method provider3 = providers.Callable(Animal) @@ -28,24 +30,32 @@ def create(cls) -> Animal: # Test 4: to check the .args & .kwargs attributes provider4 = providers.Callable(Animal) -args4: Tuple[Any] = provider4.args -kwargs4: Dict[str, Any] = provider4.kwargs +args4 = provider4.args +kwargs4 = provider4.kwargs +assert_type(args4, Tuple[Any]) +assert_type(kwargs4, Dict[str, Any]) # Test 5: to check the provided instance interface provider5 = providers.Callable(Animal) -provided5: Animal = provider5.provided() -attr_getter5: providers.AttributeGetter = provider5.provided.attr -item_getter5: providers.ItemGetter = provider5.provided["item"] -method_caller: providers.MethodCaller = provider5.provided.method.call(123, arg=324) +provided_val5 = provider5.provided() +attr_getter5 = provider5.provided.attr +item_getter5 = provider5.provided["item"] +method_caller5 = provider5.provided.method.call(123, arg=324) +assert_type(provided_val5, Any) +assert_type(attr_getter5, providers.AttributeGetter) +assert_type(item_getter5, providers.ItemGetter) +assert_type(method_caller5, providers.MethodCaller) # Test 6: to check the DelegatedCallable provider6 = providers.DelegatedCallable(Cat) -animal6: Animal = provider6(1, 2, 3, b="1", c=2, e=0.0) +cat6 = provider6(1, 2, 3, b="1", c=2, e=0.0) +assert_type(cat6, Cat) # Test 7: to check the AbstractCallable provider7 = providers.AbstractCallable(Animal) provider7.override(providers.Callable(Cat)) -animal7: Animal = provider7(1, 2, 3, b="1", c=2, e=0.0) +animal7 = provider7(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal7, Animal) # Test 8: to check the CallableDelegate __init__ provider8 = providers.CallableDelegate(providers.Callable(lambda: None)) @@ -55,20 +65,22 @@ def create(cls) -> Animal: async def _async9() -> None: - animal1: Animal = await provider9(1, 2, 3, b="1", c=2, e=0.0) # type: ignore - animal2: Animal = await provider9.async_(1, 2, 3, b="1", c=2, e=0.0) + await provider9(1, 2, 3, b="1", c=2, e=0.0) # type: ignore[misc] + cat9 = await provider9.async_(1, 2, 3, b="1", c=2, e=0.0) + assert_type(cat9, Cat) # Test 10: to check the .provides provider10 = providers.Callable(Cat) -provides10: Optional[Callable[..., Cat]] = provider10.provides -assert provides10 is Cat +provides10 = provider10.provides +assert_type(provides10, Optional[Callable[..., Cat]]) # Test 11: to check the .provides for explicit typevar provider11 = providers.Callable[Animal](Cat) -provides11: Optional[Callable[..., Animal]] = provider11.provides -assert provides11 is Cat +provides11 = provider11.provides +assert_type(provides11, Optional[Callable[..., Animal]]) + # Test 12: to check string imports -provider12: providers.Callable[Dict[Any, Any]] = providers.Callable("builtins.dict") +provider12 = providers.Callable("builtins.dict") provider12.set_provides("builtins.dict") diff --git a/tests/typing/configuration.py b/tests/typing/configuration.py index fd9a1d18..cea97f76 100644 --- a/tests/typing/configuration.py +++ b/tests/typing/configuration.py @@ -1,13 +1,15 @@ from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Type +from typing_extensions import assert_type from pydantic_settings import BaseSettings as PydanticSettings from dependency_injector import providers # Test 1: to check the getattr -config1: providers.Configuration = providers.Configuration() -provider1: providers.Provider[Dict[str, Any]] = providers.Factory(dict, a=config1.a) +config1 = providers.Configuration() +provider1 = providers.Factory(dict[str, Any], a=config1.a) +assert_type(provider1, providers.Factory[Dict[str, Any]]) # Test 2: to check the from_*() method config2 = providers.Configuration() @@ -31,15 +33,23 @@ config2.from_pydantic(PydanticSettings()) +config2.from_pydantic(PydanticSettings) + # Test 3: to check as_*() methods config3 = providers.Configuration() -int3: providers.Callable[int] = config3.option.as_int() -float3: providers.Callable[float] = config3.option.as_float() -int3_custom: providers.Callable[int] = config3.option.as_(int) +int3 = config3.option.as_int() +float3 = config3.option.as_float() +int3_custom = config3.option.as_(int) + +assert_type(int3, providers.TypedConfigurationOption[int]) +assert_type(float3, providers.TypedConfigurationOption[float]) +assert_type(int3_custom, providers.TypedConfigurationOption[int]) # Test 4: to check required() method config4 = providers.Configuration() -option4: providers.ConfigurationOption = config4.option.required() +option4 = config4.option.required() +assert_type(option4, providers.ConfigurationOption) + # Test 5: to check get/set config files' methods and init arguments # Test 5: ini @@ -47,7 +57,8 @@ ini_files=["config.ini", Path("config.ini")], ) config5_ini.set_ini_files(["config.ini", Path("config.ini")]) -config5_ini_files: list[str | Path] = config5_ini.get_ini_files() +config5_ini_files = config5_ini.get_ini_files() +assert_type(config5_ini_files, list[str | Path]) # Test 5: yaml config5_yaml = providers.Configuration( @@ -55,22 +66,25 @@ ) config5_yaml.set_yaml_files(["config.yml", Path("config.yml")]) config5_yaml_files: list[str | Path] = config5_yaml.get_yaml_files() +assert_type(config5_yaml_files, list[str | Path]) # Test 5: json config5_json = providers.Configuration( json_files=["config.json", Path("config.json")], ) config5_json.set_json_files(["config.json", Path("config.json")]) -config5_json_files: list[str | Path] = config5_json.get_json_files() +config5_json_files = config5_json.get_json_files() +assert_type(config5_json_files, list[str | Path]) # Test 5: pydantic config5_pydantic = providers.Configuration( pydantic_settings=[PydanticSettings()], ) config5_pydantic.set_pydantic_settings([PydanticSettings()]) -config5_pydantic_settings: list[PydanticSettings] = ( - config5_pydantic.get_pydantic_settings() -) + +config5_pydantic_settings = config5_pydantic.get_pydantic_settings() + +assert_type(config5_pydantic_settings, list[PydanticSettings | Type[PydanticSettings]]) # Test 6: to check init arguments config6 = providers.Configuration( @@ -78,3 +92,8 @@ strict=True, default={}, ) + +# Test 7: pydantic class +config7_pydantic_class = providers.Configuration( + pydantic_settings=[PydanticSettings] +) diff --git a/tests/typing/container.py b/tests/typing/container.py index 1a170b55..99cd6575 100644 --- a/tests/typing/container.py +++ b/tests/typing/container.py @@ -1,4 +1,5 @@ from typing import Any +from typing_extensions import assert_type from dependency_injector import providers @@ -8,8 +9,11 @@ class Container: ... # Test 1: to check the return type provider1 = providers.Container(Container) -var1: Container = provider1() +var1 = provider1() +assert_type(var1, Container) + # Test 2: to check the getattr provider2 = providers.Container(Container) -attr: providers.Provider[Any] = provider2.attr +attr = provider2.attr +assert_type(attr, providers.Provider[Any]) diff --git a/tests/typing/coroutine.py b/tests/typing/coroutine.py index e6234077..190d771a 100644 --- a/tests/typing/coroutine.py +++ b/tests/typing/coroutine.py @@ -1,4 +1,5 @@ -from typing import Awaitable, Coroutine +from typing import Awaitable, Coroutine, Any +from typing_extensions import assert_type from dependency_injector import providers @@ -8,8 +9,10 @@ async def _coro() -> None: ... # Test 1: to check the return type provider1 = providers.Coroutine(_coro) -var1: Awaitable[None] = provider1() +var1 = provider1() +assert_type(var1, Coroutine[Any, Any, None]) # type: ignore[unused-coroutine] # Test 2: to check string imports -provider2: providers.Coroutine[None] = providers.Coroutine("_coro") +provider2 = providers.Coroutine("_coro") provider2.set_provides("_coro") +assert_type(provider2, providers.Coroutine[Any]) diff --git a/tests/typing/declarative_container.py b/tests/typing/declarative_container.py index 5cb4c780..53ef5921 100644 --- a/tests/typing/declarative_container.py +++ b/tests/typing/declarative_container.py @@ -1,4 +1,5 @@ from typing import Any, Dict +from typing_extensions import assert_type from dependency_injector import containers, providers @@ -8,10 +9,12 @@ class Container1(containers.DeclarativeContainer): provider = providers.Factory(int) +# NOTE: Using assignment to check base class instead of exact type container1 = Container1() container1_type: containers.Container = Container1() provider1: providers.Provider[int] = container1.provider -val1: int = container1.provider(3) +val1 = container1.provider(3) +assert_type(val1, int) # Test 2: to check @override decorator @@ -46,7 +49,8 @@ class Container5(containers.DeclarativeContainer): provider = providers.Factory(int) -dependencies: Dict[str, providers.Provider[Any]] = Container5.dependencies +dependencies = Container5.dependencies +assert_type(dependencies, Dict[str, providers.Provider[Any]]) # Test 6: to check base class diff --git a/tests/typing/delegate.py b/tests/typing/delegate.py index 01b0525e..6294a0ed 100644 --- a/tests/typing/delegate.py +++ b/tests/typing/delegate.py @@ -1,20 +1,24 @@ from typing import Any, Optional +from typing_extensions import assert_type from dependency_injector import providers # Test 1: to check the return type provider1 = providers.Delegate(providers.Provider()) -var1: providers.Provider[Any] = provider1() +var1 = provider1() +assert_type(var1, providers.Provider[Any]) # Test 2: to check the return type with await provider2 = providers.Delegate(providers.Provider()) async def _async2() -> None: - var1: providers.Provider[Any] = await provider2() # type: ignore - var2: providers.Provider[Any] = await provider2.async_() + var1 = await provider2() # type: ignore + var2 = await provider2.async_() + assert_type(var2, providers.Provider[Any]) # Test 3: to check class type from provider provider3 = providers.Delegate(providers.Provider()) -provided_provides: Optional[providers.Provider[Any]] = provider3.provides +provided_provides = provider3.provides +assert_type(provided_provides, Optional[providers.Provider[Any]]) diff --git a/tests/typing/dependencies_container.py b/tests/typing/dependencies_container.py index 614c445e..a2184903 100644 --- a/tests/typing/dependencies_container.py +++ b/tests/typing/dependencies_container.py @@ -1,4 +1,5 @@ from typing import Any +from typing_extensions import assert_type from dependency_injector import providers @@ -7,6 +8,10 @@ a=providers.Provider(), b=providers.Provider(), ) -a1: providers.Provider[Any] = provider1.a -b1: providers.Provider[Any] = provider1.b -c1: providers.ProvidedInstance = provider1.c.provided +a1 = provider1.a +b1 = provider1.b +c1 = provider1.c.provided + +assert_type(a1, providers.Provider[Any]) +assert_type(b1, providers.Provider[Any]) +assert_type(c1, providers.ProvidedInstance) diff --git a/tests/typing/dependency.py b/tests/typing/dependency.py index 10711f93..963df8f0 100644 --- a/tests/typing/dependency.py +++ b/tests/typing/dependency.py @@ -1,4 +1,5 @@ from typing import Any, Type +from typing_extensions import assert_type from dependency_injector import providers @@ -14,16 +15,19 @@ def __init__(self, *a: Any, **kw: Any) -> None: ... # Test 1: to check the return type provider1 = providers.Dependency(instance_of=Animal) provider1.override(providers.Factory(Cat)) -var1: Animal = provider1() +var1 = provider1() +assert_type(var1, Animal) # Test 2: to check the return type provider2 = providers.Dependency(instance_of=Animal) -var2: Type[Animal] = provider2.instance_of +var2 = provider2.instance_of +assert_type(var2, Type[Animal]) # Test 3: to check the return type with await provider3 = providers.Dependency(instance_of=Animal) async def _async3() -> None: - var1: Animal = await provider3() # type: ignore - var2: Animal = await provider3.async_() + var1 = await provider3() # type: ignore + var2 = await provider3.async_() + assert_type(var2, Animal) diff --git a/tests/typing/dict.py b/tests/typing/dict.py index 2a6b0044..10054283 100644 --- a/tests/typing/dict.py +++ b/tests/typing/dict.py @@ -1,4 +1,5 @@ from typing import Any, Dict +from typing_extensions import assert_type from dependency_injector import providers @@ -7,19 +8,22 @@ a1=providers.Factory(object), a2=providers.Factory(object), ) -var1: Dict[Any, Any] = provider1() +var1 = provider1() +assert_type(var1, Dict[Any, Any]) # Test 2: to check init with non-string keys provider2 = providers.Dict({object(): providers.Factory(object)}) -var2: Dict[Any, Any] = provider2() +var2 = provider2() +assert_type(var2, Dict[Any, Any]) # Test 3: to check init with non-string keys provider3 = providers.Dict( {object(): providers.Factory(object)}, a2=providers.Factory(object) ) -var3: Dict[Any, Any] = provider3() +var3 = provider3() +assert_type(var3, Dict[Any, Any]) # Test 4: to check the .args attributes @@ -27,7 +31,8 @@ a1=providers.Factory(object), a2=providers.Factory(object), ) -args4: Dict[Any, Any] = provider4.kwargs +args = provider4.kwargs +assert_type(args, Dict[Any, Any]) # Test 5: to check the provided instance interface @@ -35,7 +40,8 @@ a1=providers.Factory(object), a2=providers.Factory(object), ) -provided5: dict[Any, Any] = provider5.provided() +provided5 = provider5.provided() +assert_type(provided5, Any) # Test 6: to check the return type with await @@ -46,5 +52,6 @@ async def _async3() -> None: - var1: Dict[Any, Any] = await provider6() # type: ignore - var2: Dict[Any, Any] = await provider6.async_() + var1 = await provider6() # type: ignore + var2 = await provider6.async_() + assert_type(var2, Dict[Any, Any]) diff --git a/tests/typing/dynamic_container.py b/tests/typing/dynamic_container.py index b88da6bd..9febded3 100644 --- a/tests/typing/dynamic_container.py +++ b/tests/typing/dynamic_container.py @@ -1,4 +1,5 @@ from typing import Any, Dict +from typing_extensions import assert_type from dependency_injector import containers, providers @@ -22,7 +23,9 @@ # Test 5: to check .dependencies attribute container5 = containers.DynamicContainer() -dependencies: Dict[str, providers.Provider[Any]] = container5.dependencies +dependencies = container5.dependencies +assert_type(dependencies, Dict[str, providers.Provider[Any]]) # Test 6: to check base class +# NOTE: Using assignment to check base class instead of exact type container6: containers.Container = containers.DynamicContainer() diff --git a/tests/typing/factory.py b/tests/typing/factory.py index 29b34823..314f267b 100644 --- a/tests/typing/factory.py +++ b/tests/typing/factory.py @@ -1,4 +1,5 @@ from typing import Any, Callable, Dict, Optional, Tuple, Type +from typing_extensions import assert_type from dependency_injector import providers @@ -17,103 +18,126 @@ def create(cls) -> Animal: # Test 1: to check the return type (class) provider1 = providers.Factory(Cat) -animal1: Animal = provider1(1, 2, 3, b="1", c=2, e=0.0) +animal1 = provider1(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal1, Cat) # Test 2: to check the return type (class factory method) provider2 = providers.Factory(Cat.create) -animal2: Animal = provider2() +animal2 = provider2() +assert_type(animal2, Animal) # Test 3: to check the .override() method provider3 = providers.Factory(Animal) with provider3.override(providers.Factory(Cat)): - provider3() + animal3 = provider3() + assert_type(animal3, Animal) # Test 4: to check the .args, .kwargs, .attributes attributes provider4 = providers.Factory(Animal) -args4: Tuple[Any] = provider4.args -kwargs4: Dict[str, Any] = provider4.kwargs -attributes4: Dict[str, Any] = provider4.attributes +args4 = provider4.args +kwargs4 = provider4.kwargs +attributes4 = provider4.attributes +assert_type(args4, Tuple[Any]) +assert_type(kwargs4, Dict[str, Any]) +assert_type(attributes4, Dict[str, Any]) # Test 5: to check the provided instance interface provider5 = providers.Factory(Animal) -provided5: Animal = provider5.provided() -attr_getter5: providers.AttributeGetter = provider5.provided.attr -item_getter5: providers.ItemGetter = provider5.provided["item"] -method_caller5: providers.MethodCaller = provider5.provided.method.call(123, arg=324) +provided5 = provider5.provided +provided_val5 = provided5() +attr_getter5 = provider5.provided.attr +item_getter5 = provider5.provided["item"] +method_caller5 = provider5.provided.method.call(123, arg=324) +assert_type(provided5, providers.ProvidedInstance) +assert_type(provided_val5, Any) +assert_type(attr_getter5, providers.AttributeGetter) +assert_type(item_getter5, providers.ItemGetter) +assert_type(method_caller5, providers.MethodCaller) # Test 6: to check the DelegatedFactory provider6 = providers.DelegatedFactory(Cat) -animal6: Animal = provider6(1, 2, 3, b="1", c=2, e=0.0) +animal6 = provider6(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal6, Cat) # Test 7: to check the AbstractFactory provider7 = providers.AbstractFactory(Animal) provider7.override(providers.Factory(Cat)) -animal7: Animal = provider7(1, 2, 3, b="1", c=2, e=0.0) +animal7 = provider7(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal7, Animal) # Test 8: to check the FactoryDelegate __init__ provider8 = providers.FactoryDelegate(providers.Factory(object)) # Test 9: to check FactoryAggregate provider -provider9: providers.FactoryAggregate[str] = providers.FactoryAggregate( +provider9 = providers.FactoryAggregate( a=providers.Factory(str, "str1"), b=providers.Factory(str, "str2"), ) -factory_a_9: providers.Factory[str] = provider9.a -factory_b_9: providers.Factory[str] = provider9.b -val9: str = provider9("a") - -provider9_set_non_string_keys: providers.FactoryAggregate[str] = ( - providers.FactoryAggregate() -) +factory_a_9 = provider9.a +factory_b_9 = provider9.b +val9 = provider9("a") +assert_type(provider9, providers.FactoryAggregate[str]) +assert_type(factory_a_9, providers.Factory[str]) +assert_type(factory_b_9, providers.Factory[str]) +assert_type(val9, str) + +provider9_set_non_string_keys = providers.FactoryAggregate[str]() provider9_set_non_string_keys.set_factories({Cat: providers.Factory(str, "str")}) -factory_set_non_string_9: providers.Factory[str] = ( - provider9_set_non_string_keys.factories[Cat] -) +factory_set_non_string_9 = provider9_set_non_string_keys.factories[Cat] +assert_type(provider9_set_non_string_keys, providers.FactoryAggregate[str]) +assert_type(factory_set_non_string_9, providers.Factory[str]) -provider9_new_non_string_keys: providers.FactoryAggregate[str] = ( - providers.FactoryAggregate( - {Cat: providers.Factory(str, "str")}, - ) -) -factory_new_non_string_9: providers.Factory[str] = ( - provider9_new_non_string_keys.factories[Cat] +provider9_new_non_string_keys = providers.FactoryAggregate( + {Cat: providers.Factory(str, "str")}, ) +factory_new_non_string_9 = provider9_new_non_string_keys.factories[Cat] +assert_type(provider9_new_non_string_keys, providers.FactoryAggregate[str]) +assert_type(factory_new_non_string_9, providers.Factory[str]) provider9_no_explicit_typing = providers.FactoryAggregate( a=providers.Factory(str, "str") ) -provider9_no_explicit_typing_factory: providers.Factory[str] = ( - provider9_no_explicit_typing.factories["a"] -) -provider9_no_explicit_typing_object: str = provider9_no_explicit_typing("a") +provider9_no_explicit_typing_factory = provider9_no_explicit_typing.factories["a"] +provider9_no_explicit_typing_object = provider9_no_explicit_typing("a") +assert_type(provider9_no_explicit_typing, providers.FactoryAggregate[str]) +assert_type(provider9_no_explicit_typing_factory, providers.Factory[str]) +assert_type(provider9_no_explicit_typing_object, str) # Test 10: to check the explicit typing -factory10: providers.Provider[Animal] = providers.Factory(Cat) -animal10: Animal = factory10() +factory10 = providers.Factory[Animal](Cat) +animal10 = factory10() +assert_type(factory10, providers.Factory[Animal]) +assert_type(animal10, Animal) # Test 11: to check the return type with await provider11 = providers.Factory(Cat) async def _async11() -> None: - animal1: Animal = await provider11(1, 2, 3, b="1", c=2, e=0.0) # type: ignore - animal2: Animal = await provider11.async_(1, 2, 3, b="1", c=2, e=0.0) + animal1 = await provider11(1, 2, 3, b="1", c=2, e=0.0) # type: ignore + animal2 = await provider11.async_(1, 2, 3, b="1", c=2, e=0.0) + assert_type(animal2, Cat) # Test 12: to check class type from .provides provider12 = providers.Factory(Cat) -provided_cls12: Type[Animal] = provider12.cls +provided_cls12 = provider12.cls assert issubclass(provided_cls12, Animal) -provided_provides12: Optional[Callable[..., Animal]] = provider12.provides +provided_provides12 = provider12.provides assert provided_provides12 is not None and provided_provides12() == Cat() +assert_type(provided_cls12, Type[Cat]) +assert_type(provided_provides12, Callable[..., Cat]) + # Test 13: to check class from .provides with explicit typevar provider13 = providers.Factory[Animal](Cat) -provided_cls13: Type[Animal] = provider13.cls +provided_cls13 = provider13.cls assert issubclass(provided_cls13, Animal) -provided_provides13: Optional[Callable[..., Animal]] = provider13.provides +provided_provides13 = provider13.provides assert provided_provides13 is not None and provided_provides13() == Cat() +assert_type(provided_cls13, Type[Animal]) +assert_type(provided_provides13, Callable[..., Animal]) # Test 14: to check string imports -provider14: providers.Factory[Dict[Any, Any]] = providers.Factory("builtins.dict") +provider14 = providers.Factory[Any]("builtins.dict") provider14.set_provides("builtins.dict") diff --git a/tests/typing/list.py b/tests/typing/list.py index 65e65e78..44d66c8e 100644 --- a/tests/typing/list.py +++ b/tests/typing/list.py @@ -1,4 +1,5 @@ from typing import Any, List, Tuple +from typing_extensions import assert_type from dependency_injector import providers @@ -7,7 +8,8 @@ providers.Factory(object), providers.Factory(object), ) -var1: List[Any] = provider1() +var1 = provider1() +assert_type(var1, List[Any]) # Test 2: to check the .args attributes @@ -15,17 +17,24 @@ providers.Factory(object), providers.Factory(object), ) -args2: Tuple[Any] = provider2.args +args2 = provider2.args +assert_type(args2, Tuple[Any]) # Test 3: to check the provided instance interface provider3 = providers.List( providers.Factory(object), providers.Factory(object), ) -provided3: List[Any] = provider3.provided() -attr_getter3: providers.AttributeGetter = provider3.provided.attr -item_getter3: providers.ItemGetter = provider3.provided["item"] -method_caller3: providers.MethodCaller = provider3.provided.method.call(123, arg=324) +provided3 = provider3.provided +provided_val3 = provided3() +attr_getter3 = provider3.provided.attr +item_getter3 = provider3.provided["item"] +method_caller3 = provider3.provided.method.call(123, arg=324) +assert_type(provided3, providers.ProvidedInstance) +assert_type(provided_val3, Any) +assert_type(attr_getter3, providers.AttributeGetter) +assert_type(item_getter3, providers.ItemGetter) +assert_type(method_caller3, providers.MethodCaller) # Test 4: to check the return type with await provider4 = providers.List( @@ -35,5 +44,6 @@ async def _async4() -> None: - var1: List[Any] = await provider4() # type: ignore - var2: List[Any] = await provider4.async_() + var1 = await provider4() # type: ignore + var2 = await provider4.async_() + assert_type(var2, List[Any]) diff --git a/tests/typing/object.py b/tests/typing/object.py index 739b6238..b149679e 100644 --- a/tests/typing/object.py +++ b/tests/typing/object.py @@ -1,27 +1,38 @@ -from typing import Optional, Type +from typing import Optional, Any +from typing_extensions import assert_type from dependency_injector import providers # Test 1: to check the return type provider1 = providers.Object(int(3)) -var1: int = provider1() +var1 = provider1() +assert_type(var1, int) # Test 2: to check the provided instance interface provider2 = providers.Object(int) -provided2: Type[int] = provider2.provided() -attr_getter2: providers.AttributeGetter = provider2.provided.attr -item_getter2: providers.ItemGetter = provider2.provided["item"] -method_caller2: providers.MethodCaller = provider2.provided.method.call(123, arg=324) +provided2 = provider2.provided +provided_val2 = provided2() +attr_getter2 = provider2.provided.attr +item_getter2 = provider2.provided["item"] +method_caller2 = provider2.provided.method.call(123, arg=324) +assert_type(provided2, providers.ProvidedInstance) +assert_type(provided_val2, Any) +assert_type(attr_getter2, providers.AttributeGetter) +assert_type(item_getter2, providers.ItemGetter) +assert_type(method_caller2, providers.MethodCaller) + # Test 3: to check the return type with await provider3 = providers.Object(int(3)) async def _async3() -> None: - var1: int = await provider3() # type: ignore - var2: int = await provider3.async_() + var1 = await provider3() # type: ignore + var2 = await provider3.async_() + assert_type(var2, int) # Test 4: to check class type from provider provider4 = providers.Object(int("1")) -provided_provides: Optional[int] = provider4.provides +provided_provides4 = provider4.provides +assert_type(provided_provides4, Optional[int]) diff --git a/tests/typing/provider.py b/tests/typing/provider.py index cb832b77..85906430 100644 --- a/tests/typing/provider.py +++ b/tests/typing/provider.py @@ -1,17 +1,26 @@ from typing import Any +from typing_extensions import assert_type from dependency_injector import providers # Test 1: to check .provided attribute provider1: providers.Provider[int] = providers.Object(1) -provided: int = provider1.provided() -provider1_delegate: providers.Provider[int] = provider1.provider +provided1 = provider1.provided +provided_val1 = provided1() +provider1_delegate = provider1.provider +assert_type(provider1, providers.Provider[int]) +assert_type(provided1, providers.ProvidedInstance) +assert_type(provided_val1, Any) +assert_type(provider1_delegate, providers.Provider[int]) # Test 2: to check async mode API -provider2: providers.Provider[Any] = providers.Provider() +provider2 = providers.Provider[Any]() provider2.enable_async_mode() provider2.disable_async_mode() provider2.reset_async_mode() -r1: bool = provider2.is_async_mode_enabled() -r2: bool = provider2.is_async_mode_disabled() -r3: bool = provider2.is_async_mode_undefined() +r1 = provider2.is_async_mode_enabled() +r2 = provider2.is_async_mode_disabled() +r3 = provider2.is_async_mode_undefined() +assert_type(r1, bool) +assert_type(r2, bool) +assert_type(r3, bool) diff --git a/tests/typing/resource.py b/tests/typing/resource.py index d01a5106..5813b94a 100644 --- a/tests/typing/resource.py +++ b/tests/typing/resource.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager, asynccontextmanager from typing import ( Any, AsyncGenerator, @@ -7,7 +8,9 @@ Iterator, List, Optional, + Self, ) +from typing_extensions import assert_type from dependency_injector import providers, resources @@ -18,7 +21,8 @@ def init1() -> List[int]: provider1 = providers.Resource(init1) -var1: List[int] = provider1() +var1 = provider1() +assert_type(var1, List[int]) # Test 2: to check the return type with iterator @@ -27,7 +31,8 @@ def init2() -> Iterator[List[int]]: provider2 = providers.Resource(init2) -var2: List[int] = provider2() +var2 = provider2() +assert_type(var2, List[int]) # Test 3: to check the return type with generator @@ -36,7 +41,8 @@ def init3() -> Generator[List[int], None, None]: provider3 = providers.Resource(init3) -var3: List[int] = provider3() +var3 = provider3() +assert_type(var3, List[int]) # Test 4: to check the return type with resource subclass @@ -48,7 +54,8 @@ def shutdown(self, resource: Optional[List[int]]) -> None: ... provider4 = providers.Resource(MyResource4) -var4: List[int] = provider4() +var4 = provider4() +assert_type(var4, List[int]) # Test 5: to check the return type with async function @@ -60,8 +67,9 @@ async def init5() -> List[int]: async def _provide5() -> None: - var1: List[int] = await provider5() # type: ignore - var2: List[int] = await provider5.async_() + var1 = await provider5() # type: ignore + var2 = await provider5.async_() + assert_type(var2, List[int]) # Test 6: to check the return type with async iterator @@ -73,8 +81,9 @@ async def init6() -> AsyncIterator[List[int]]: async def _provide6() -> None: - var1: List[int] = await provider6() # type: ignore - var2: List[int] = await provider6.async_() + var1 = await provider6() # type: ignore + var2 = await provider6.async_() + assert_type(var2, List[int]) # Test 7: to check the return type with async generator @@ -86,8 +95,9 @@ async def init7() -> AsyncGenerator[List[int], None]: async def _provide7() -> None: - var1: List[int] = await provider7() # type: ignore - var2: List[int] = await provider7.async_() + var1 = await provider7() # type: ignore + var2 = await provider7.async_() + assert_type(var2, List[int]) # Test 8: to check the return type with async resource subclass @@ -102,10 +112,75 @@ async def shutdown(self, resource: Optional[List[int]]) -> None: ... async def _provide8() -> None: - var1: List[int] = await provider8() # type: ignore - var2: List[int] = await provider8.async_() + var1 = await provider8() # type: ignore + var2 = await provider8.async_() + assert_type(var2, List[int]) # Test 9: to check string imports provider9: providers.Resource[Dict[Any, Any]] = providers.Resource("builtins.dict") provider9.set_provides("builtins.dict") + + +# Test 10: to check the return type with classes implementing AbstractContextManager protocol +class MyResource10: + def __init__(self) -> None: + pass + + def __enter__(self) -> Self: + return self + + def __exit__(self, *args: Any, **kwargs: Any) -> None: + return None + + +provider10 = providers.Resource(MyResource10) +var10 = provider10() +assert_type(var10, MyResource10) + + +# Test 11: to check the return type with functions decorated with contextlib.contextmanager +@contextmanager +def init11() -> Iterator[int]: + yield 1 + + +provider11 = providers.Resource(init11) +var11 = provider11() +assert_type(var11, int) + + +# Test 12: to check the return type with classes implementing AbstractAsyncContextManager protocol +class MyResource12: + def __init__(self) -> None: + pass + + async def __aenter__(self) -> Self: + return self + + async def __aexit__(self, *args: Any, **kwargs: Any) -> None: + return None + + +provider12 = providers.Resource(MyResource12) + + +async def _provide12() -> None: + var1 = await provider12() # type: ignore + var2 = await provider12.async_() + assert_type(var2, MyResource12) + + +# Test 13: to check the return type with functions decorated with contextlib.asynccontextmanager +@asynccontextmanager +async def init13() -> AsyncIterator[int]: + yield 1 + + +provider13 = providers.Resource(init13) + + +async def _provide13() -> None: + var1 = await provider13() # type: ignore + var2 = await provider13.async_() + assert_type(var2, int) diff --git a/tests/typing/selector.py b/tests/typing/selector.py index 5d89ec66..66cc5bce 100644 --- a/tests/typing/selector.py +++ b/tests/typing/selector.py @@ -1,4 +1,5 @@ -from typing import Any +from typing import Any, Callable, Optional, Dict +from typing_extensions import assert_type from dependency_injector import providers @@ -8,7 +9,8 @@ a=providers.Factory(object), b=providers.Factory(object), ) -var1: Any = provider1() +var1 = provider1() +assert_type(var1, Any) # Test 2: to check the provided instance interface provider2 = providers.Selector( @@ -16,10 +18,17 @@ a=providers.Factory(object), b=providers.Factory(object), ) -provided2: providers.ProvidedInstance = provider2.provided -attr_getter2: providers.AttributeGetter = provider2.provided.attr -item_getter2: providers.ItemGetter = provider2.provided["item"] -method_caller2: providers.MethodCaller = provider2.provided.method.call(123, arg=324) +provided2 = provider2.provided +provided_val2 = provided2() +attr_getter2 = provider2.provided.attr +item_getter2 = provider2.provided["item"] +method_caller2 = provider2.provided.method.call(123, arg=324) +assert_type(provider2, providers.Selector[Any]) +assert_type(provided2, providers.ProvidedInstance) +assert_type(provided_val2, Any) +assert_type(attr_getter2, providers.AttributeGetter) +assert_type(item_getter2, providers.ItemGetter) +assert_type(method_caller2, providers.MethodCaller) # Test3 to check the getattr provider3 = providers.Selector( @@ -27,7 +36,8 @@ a=providers.Factory(object), b=providers.Factory(object), ) -attr3: providers.Provider[Any] = provider3.a +attr3 = provider3.a +assert_type(attr3, providers.Provider[Any]) # Test 4: to check the return type with await provider4 = providers.Selector( @@ -38,5 +48,51 @@ async def _async4() -> None: - var1: Any = await provider4() - var2: Any = await provider4.async_() + var1 = await provider4() + var2 = await provider4.async_() + assert_type(var1, Any) + assert_type(var2, Any) + + +# Test 5: to check selector getter and setter +provider5 = providers.Selector( + lambda: "a", + a=providers.Factory(object), + b=providers.Factory(object), +) +selector5 = provider5.selector +provider5_after_set_selector = provider5.set_selector(lambda: "a") +assert_type(selector5, Optional[Callable[..., Any]]) +assert_type(provider5_after_set_selector, providers.Selector[Any]) + +# Test 6: to check providers getter and setter +provider6 = providers.Selector( + lambda: "a", + a=providers.Factory(object), + b=providers.Factory(object), +) +providers6 = provider6.providers +provider6_after_set_providers = provider6.set_providers(c=providers.Factory(object)) +assert_type(providers6, Dict[str, providers.Provider[Any]]) +assert_type(provider6_after_set_providers, providers.Selector[Any]) + +# Test 7: to check explicit typing: return type, getattr, getter/setter of providers and selectors +provider7 = providers.Selector[bool]( + lambda: "a", a=providers.Factory(bool), b=providers.Factory(int) +) +var7 = provider7() +attr7 = provider7.a +assert_type(var7, bool) +assert_type(attr7, providers.Provider[bool]) + +selector7 = provider7.selector +provider7_after_set_selector = provider7.set_selector(lambda: "a") +assert_type(selector7, Optional[Callable[..., Any]]) +assert_type(provider7_after_set_selector, providers.Selector[bool]) + +providers7 = provider7.providers +provider7_after_set_providers = provider7.set_providers( + c=providers.Factory(str) +) # We don't require Provider of subclass of bool yet since Provider is invariant +assert_type(providers7, Dict[str, providers.Provider[bool]]) +assert_type(provider7_after_set_providers, providers.Selector[bool]) diff --git a/tests/typing/singleton.py b/tests/typing/singleton.py index 740da8b9..08837e38 100644 --- a/tests/typing/singleton.py +++ b/tests/typing/singleton.py @@ -1,4 +1,5 @@ from typing import Any, Callable, Dict, Optional, Tuple, Type +from typing_extensions import assert_type from dependency_injector import providers @@ -17,54 +18,72 @@ def create(cls) -> Animal: # Test 1: to check the return type (class) provider1 = providers.Singleton(Cat) -animal1: Animal = provider1(1, 2, 3, b="1", c=2, e=0.0) +animal1 = provider1(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal1, Cat) # Test 2: to check the return type (class factory method) provider2 = providers.Singleton(Cat.create) -animal2: Animal = provider2() +animal2 = provider2() +assert_type(animal2, Animal) # Test 3: to check the .override() method provider3 = providers.Singleton(Animal) with provider3.override(providers.Singleton(Cat)): - provider3() + animal3 = provider3() + assert_type(animal3, Animal) # Test 4: to check the .args, .kwargs, .attributes attributes provider4 = providers.Singleton(Animal) -args4: Tuple[Any] = provider4.args -kwargs4: Dict[str, Any] = provider4.kwargs -attributes4: Dict[str, Any] = provider4.attributes +args4 = provider4.args +kwargs4 = provider4.kwargs +attributes4 = provider4.attributes +assert_type(args4, Tuple[Any]) +assert_type(kwargs4, Dict[str, Any]) +assert_type(attributes4, Dict[str, Any]) # Test 5: to check the provided instance interface provider5 = providers.Singleton(Animal) -provided5: Animal = provider5.provided() -attr_getter5: providers.AttributeGetter = provider5.provided.attr -item_getter5: providers.ItemGetter = provider5.provided["item"] -method_caller5: providers.MethodCaller = provider5.provided.method.call(123, arg=324) +provided5 = provider5.provided +provided_val5 = provided5() +attr_getter5 = provider5.provided.attr +item_getter5 = provider5.provided["item"] +method_caller5 = provider5.provided.method.call(123, arg=324) +assert_type(provided5, providers.ProvidedInstance) +assert_type(provided_val5, Any) +assert_type(attr_getter5, providers.AttributeGetter) +assert_type(item_getter5, providers.ItemGetter) +assert_type(method_caller5, providers.MethodCaller) # Test 6: to check the DelegatedSingleton provider6 = providers.DelegatedSingleton(Cat) -animal6: Animal = provider6(1, 2, 3, b="1", c=2, e=0.0) +animal6 = provider6(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal6, Cat) # Test 7: to check the ThreadSafeSingleton -provider7: providers.BaseSingleton[Animal] = providers.ThreadSafeSingleton(Cat) -animal7: Animal = provider7() +provider7 = providers.ThreadSafeSingleton(Cat) +animal7 = provider7() +assert_type(animal7, Cat) # Test 8: to check the DelegatedThreadSafeSingleton provider8 = providers.DelegatedThreadSafeSingleton(Cat) -animal8: Animal = provider8(1, 2, 3, b="1", c=2, e=0.0) +animal8 = provider8(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal8, Cat) # Test 9: to check the ThreadLocalSingleton provider9 = providers.ThreadLocalSingleton(Cat) -animal9: Animal = provider9(1, 2, 3, b="1", c=2, e=0.0) +animal9 = provider9(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal9, Cat) # Test 10: to check the DelegatedThreadLocalSingleton provider10 = providers.DelegatedThreadLocalSingleton(Cat) -animal10: Animal = provider10(1, 2, 3, b="1", c=2, e=0.0) +animal10 = provider10(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal10, Cat) # Test 11: to check the AbstractSingleton provider11 = providers.AbstractSingleton(Animal) provider11.override(providers.Singleton(Cat)) -animal11: Animal = provider11(1, 2, 3, b="1", c=2, e=0.0) +animal11 = provider11(1, 2, 3, b="1", c=2, e=0.0) +assert_type(animal11, Animal) # Test 12: to check the SingletonDelegate __init__ provider12 = providers.SingletonDelegate(providers.Singleton(object)) @@ -74,16 +93,19 @@ def create(cls) -> Animal: async def _async13() -> None: - animal1: Animal = await provider13(1, 2, 3, b="1", c=2, e=0.0) # type: ignore - animal2: Animal = await provider13.async_(1, 2, 3, b="1", c=2, e=0.0) + animal1 = await provider13(1, 2, 3, b="1", c=2, e=0.0) # type: ignore + animal2 = await provider13.async_(1, 2, 3, b="1", c=2, e=0.0) + assert_type(animal2, Cat) # Test 14: to check class from .provides provider14 = providers.Singleton(Cat) -provided_cls14: Type[Cat] = provider14.cls +provided_cls14 = provider14.cls assert issubclass(provided_cls14, Cat) -provided_provides14: Optional[Callable[..., Cat]] = provider14.provides +provided_provides14 = provider14.provides assert provided_provides14 is not None and provided_provides14() == Cat() +assert_type(provided_cls14, Type[Cat]) +assert_type(provided_provides14, Callable[..., Cat]) # Test 15: to check class from .provides with explicit typevar provider15 = providers.Singleton[Animal](Cat) @@ -91,7 +113,9 @@ async def _async13() -> None: assert issubclass(provided_cls15, Animal) provided_provides15: Optional[Callable[..., Animal]] = provider15.provides assert provided_provides15 is not None and provided_provides15() == Cat() +assert_type(provided_cls15, Type[Animal]) +assert_type(provided_provides15, Callable[..., Animal]) # Test 16: to check string imports -provider16: providers.Singleton[Dict[Any, Any]] = providers.Singleton("builtins.dict") +provider16 = providers.Singleton[Any]("builtins.dict") provider16.set_provides("builtins.dict") diff --git a/tests/unit/providers/configuration/test_from_pydantic_py36.py b/tests/unit/providers/configuration/test_from_pydantic_py36.py index ee2a1176..f47e458c 100644 --- a/tests/unit/providers/configuration/test_from_pydantic_py36.py +++ b/tests/unit/providers/configuration/test_from_pydantic_py36.py @@ -168,21 +168,23 @@ def test_option_not_instance_of_settings(config): def test_subclass_instead_of_instance(config): - with raises(errors.Error) as error: - config.from_pydantic(Settings1) - assert error.value.args[0] == ( - "Got settings class, but expect instance: " - "instead \"Settings1\" use \"Settings1()\"" - ) + config.from_pydantic(Settings1) + + assert config() == {"section1": {"value1": 1}, "section2": {"value2": 2}} + assert config.section1() == {"value1": 1} + assert config.section1.value1() == 1 + assert config.section2() == {"value2": 2} + assert config.section2.value2() == 2 def test_option_subclass_instead_of_instance(config): - with raises(errors.Error) as error: - config.option.from_pydantic(Settings1) - assert error.value.args[0] == ( - "Got settings class, but expect instance: " - "instead \"Settings1\" use \"Settings1()\"" - ) + config.option.from_pydantic(Settings1) + + assert config.option() == {"section1": {"value1": 1}, "section2": {"value2": 2}} + assert config.option.section1() == {"value1": 1} + assert config.option.section1.value1() == 1 + assert config.option.section2() == {"value2": 2} + assert config.option.section2.value2() == 2 @mark.usefixtures("no_pydantic_module_installed") diff --git a/tests/unit/providers/resource/test_async_resource_py35.py b/tests/unit/providers/resource/test_async_resource_py35.py index 6458584d..eff10ca4 100644 --- a/tests/unit/providers/resource/test_async_resource_py35.py +++ b/tests/unit/providers/resource/test_async_resource_py35.py @@ -2,7 +2,7 @@ import asyncio import inspect -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, contextmanager from typing import Any from pytest import mark, raises @@ -346,3 +346,22 @@ async def _init(): assert result2 is resource assert _init.counter == 1 + + +@mark.asyncio +async def test_sync_resource_with_async_deps(): + @asynccontextmanager + async def resource_async(v): + await asyncio.sleep(0) + yield v + + @contextmanager + def resource_sync(_async): + yield _async + 1 + + _async = providers.Resource(resource_async, 1) + _sync = providers.Resource(resource_sync, _async) + + assert (await _sync()) == 2 + await _async.shutdown() + await _sync.shutdown() diff --git a/tests/unit/providers/resource/test_context_local_resource_py38.py b/tests/unit/providers/resource/test_context_local_resource_py38.py new file mode 100644 index 00000000..3a0452b9 --- /dev/null +++ b/tests/unit/providers/resource/test_context_local_resource_py38.py @@ -0,0 +1,492 @@ +"""Resource provider tests.""" + +import asyncio +import decimal +import sys +from contextlib import contextmanager + +from pytest import mark, raises + +from dependency_injector import containers, errors, providers, resources + + +def init_fn(*args, **kwargs): + return args, kwargs + + +def test_is_provider(): + assert providers.is_provider(providers.ContextLocalResource(init_fn)) is True + + +def test_init_optional_provides(): + provider = providers.ContextLocalResource() + provider.set_provides(init_fn) + assert provider.provides is init_fn + assert provider() == (tuple(), dict()) + + +def test_set_provides_returns_(): + provider = providers.ContextLocalResource() + assert provider.set_provides(init_fn) is provider + + +@mark.parametrize( + "str_name,cls", + [ + ("dependency_injector.providers.Factory", providers.Factory), + ("decimal.Decimal", decimal.Decimal), + ("list", list), + (".test_context_local_resource_py38.test_is_provider", test_is_provider), + ("test_is_provider", test_is_provider), + ], +) +def test_set_provides_string_imports(str_name, cls): + assert providers.ContextLocalResource(str_name).provides is cls + + +def test_provided_instance_provider(): + provider = providers.ContextLocalResource(init_fn) + assert isinstance(provider.provided, providers.ProvidedInstance) + + +def test_injection(): + resource = object() + + def _init(): + _init.counter += 1 + return resource + + _init.counter = 0 + + class Container(containers.DeclarativeContainer): + context_local_resource = providers.ContextLocalResource(_init) + dependency1 = providers.List(context_local_resource) + dependency2 = providers.List(context_local_resource) + + container = Container() + list1 = container.dependency1() + list2 = container.dependency2() + + assert list1 == [resource] + assert list1[0] is resource + + assert list2 == [resource] + assert list2[0] is resource + + assert _init.counter == 1 + + +@mark.asyncio +async def test_injection_in_different_context(): + def _init(): + return object() + + async def _async_init(): + return object() + + class Container(containers.DeclarativeContainer): + context_local_resource = providers.ContextLocalResource(_init) + async_context_local_resource = providers.ContextLocalResource(_async_init) + + async def run_in_context(): + obj = await container.async_context_local_resource() + return obj + + container = Container() + + obj1, obj2 = await asyncio.gather(run_in_context(), run_in_context()) + assert obj1 != obj2 + + obj3 = await container.async_context_local_resource() + obj4 = await container.async_context_local_resource() + assert obj3 == obj4 + + obj5, obj6 = await asyncio.gather(run_in_context(), run_in_context()) + assert obj5 == obj6 # as context is copied from the current one where async_context_local_resource was initialized + + obj7 = container.context_local_resource() + obj8 = container.context_local_resource() + + assert obj7 == obj8 + + +def test_init_function(): + def _init(): + _init.counter += 1 + + _init.counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider() + assert result1 is None + assert _init.counter == 1 + + result2 = provider() + assert result2 is None + assert _init.counter == 1 + + provider.shutdown() + + +def test_init_generator_in_one_context(): + def _init(): + _init.init_counter += 1 + yield object() + _init.shutdown_counter += 1 + + _init.init_counter = 0 + _init.shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider() + result2 = provider() + + assert result1 == result2 + + assert _init.init_counter == 1 + assert _init.shutdown_counter == 0 + + provider.shutdown() + assert _init.init_counter == 1 + assert _init.shutdown_counter == 1 + + provider.shutdown() + assert _init.init_counter == 1 + assert _init.shutdown_counter == 1 + + +def test_init_context_manager_in_one_context() -> None: + init_counter, shutdown_counter = 0, 0 + + @contextmanager + def _init(): + nonlocal init_counter, shutdown_counter + + init_counter += 1 + yield object() + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider() + result2 = provider() + assert result1 == result2 + + assert init_counter == 1 + assert shutdown_counter == 0 + + provider.shutdown() + + assert init_counter == 1 + assert shutdown_counter == 1 + + provider.shutdown() + assert init_counter == 1 + assert shutdown_counter == 1 + + +@mark.asyncio +async def test_async_init_context_manager_in_different_contexts() -> None: + init_counter, shutdown_counter = 0, 0 + + async def _init(): + nonlocal init_counter, shutdown_counter + init_counter += 1 + yield object() + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + async def run_in_context(): + resource = await provider() + await provider.shutdown() + return resource + + result1, result2 = await asyncio.gather(run_in_context(), run_in_context()) + + assert result1 != result2 + assert init_counter == 2 + assert shutdown_counter == 2 + + +@mark.asyncio +async def test_async_init_context_manager_in_one_context() -> None: + init_counter, shutdown_counter = 0, 0 + + async def _init(): + nonlocal init_counter, shutdown_counter + init_counter += 1 + yield object() + shutdown_counter += 1 + + init_counter = 0 + shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + async def run_in_context(): + resource_1 = await provider() + resource_2 = await provider() + await provider.shutdown() + return resource_1, resource_2 + + result1, result2 = await run_in_context() + + assert result1 == result2 + assert init_counter == 1 + assert shutdown_counter == 1 + + +def test_init_class(): + class TestResource(resources.Resource): + init_counter = 0 + shutdown_counter = 0 + + def init(self): + self.__class__.init_counter += 1 + + def shutdown(self, _): + self.__class__.shutdown_counter += 1 + + provider = providers.ContextLocalResource(TestResource) + + result1 = provider() + assert result1 is None + assert TestResource.init_counter == 1 + assert TestResource.shutdown_counter == 0 + + provider.shutdown() + assert TestResource.init_counter == 1 + assert TestResource.shutdown_counter == 1 + + result2 = provider() + assert result2 is None + assert TestResource.init_counter == 2 + assert TestResource.shutdown_counter == 1 + + provider.shutdown() + assert TestResource.init_counter == 2 + assert TestResource.shutdown_counter == 2 + + +def test_init_not_callable(): + provider = providers.ContextLocalResource(1) + with raises(TypeError, match=r"object is not callable"): + provider.init() + + +def test_init_and_shutdown(): + def _init(): + _init.init_counter += 1 + yield + _init.shutdown_counter += 1 + + _init.init_counter = 0 + _init.shutdown_counter = 0 + + provider = providers.ContextLocalResource(_init) + + result1 = provider.init() + assert result1 is None + assert _init.init_counter == 1 + assert _init.shutdown_counter == 0 + + provider.shutdown() + assert _init.init_counter == 1 + assert _init.shutdown_counter == 1 + + result2 = provider.init() + assert result2 is None + assert _init.init_counter == 2 + assert _init.shutdown_counter == 1 + + provider.shutdown() + assert _init.init_counter == 2 + assert _init.shutdown_counter == 2 + + +def test_shutdown_of_not_initialized(): + def _init(): + yield + + provider = providers.ContextLocalResource(_init) + + result = provider.shutdown() + assert result is None + + +def test_initialized(): + provider = providers.ContextLocalResource(init_fn) + assert provider.initialized is False + + provider.init() + assert provider.initialized is True + + provider.shutdown() + assert provider.initialized is False + + +def test_call_with_context_args(): + provider = providers.ContextLocalResource(init_fn, "i1", "i2") + assert provider("i3", i4=4) == (("i1", "i2", "i3"), {"i4": 4}) + + +def test_fluent_interface(): + provider = providers.ContextLocalResource(init_fn).add_args(1, 2).add_kwargs(a3=3, a4=4) + assert provider() == ((1, 2), {"a3": 3, "a4": 4}) + + +def test_set_args(): + provider = providers.ContextLocalResource(init_fn).add_args(1, 2).set_args(3, 4) + assert provider.args == (3, 4) + + +def test_clear_args(): + provider = providers.ContextLocalResource(init_fn).add_args(1, 2).clear_args() + assert provider.args == tuple() + + +def test_set_kwargs(): + provider = providers.ContextLocalResource(init_fn).add_kwargs(a1="i1", a2="i2").set_kwargs(a3="i3", a4="i4") + assert provider.kwargs == {"a3": "i3", "a4": "i4"} + + +def test_clear_kwargs(): + provider = providers.ContextLocalResource(init_fn).add_kwargs(a1="i1", a2="i2").clear_kwargs() + assert provider.kwargs == {} + + +def test_call_overridden(): + provider = providers.ContextLocalResource(init_fn, 1) + overriding_provider1 = providers.ContextLocalResource(init_fn, 2) + overriding_provider2 = providers.ContextLocalResource(init_fn, 3) + + provider.override(overriding_provider1) + provider.override(overriding_provider2) + + instance1 = provider() + instance2 = provider() + + assert instance1 is instance2 + assert instance1 == ((3,), {}) + assert instance2 == ((3,), {}) + + +def test_deepcopy(): + provider = providers.ContextLocalResource(init_fn, 1, 2, a3=3, a4=4) + + provider_copy = providers.deepcopy(provider) + + assert provider is not provider_copy + assert provider.args == provider_copy.args + assert provider.kwargs == provider_copy.kwargs + assert isinstance(provider, providers.ContextLocalResource) + + +def test_deepcopy_initialized(): + provider = providers.ContextLocalResource(init_fn) + provider.init() + + with raises(errors.Error): + providers.deepcopy(provider) + + +def test_deepcopy_from_memo(): + provider = providers.ContextLocalResource(init_fn) + provider_copy_memo = providers.ContextLocalResource(init_fn) + + provider_copy = providers.deepcopy( + provider, + memo={id(provider): provider_copy_memo}, + ) + + assert provider_copy is provider_copy_memo + + +def test_deepcopy_args(): + provider = providers.ContextLocalResource(init_fn) + dependent_provider1 = providers.Factory(list) + dependent_provider2 = providers.Factory(dict) + + provider.add_args(dependent_provider1, dependent_provider2) + + provider_copy = providers.deepcopy(provider) + dependent_provider_copy1 = provider_copy.args[0] + dependent_provider_copy2 = provider_copy.args[1] + + assert provider.args != provider_copy.args + + assert dependent_provider1.cls is dependent_provider_copy1.cls + assert dependent_provider1 is not dependent_provider_copy1 + + assert dependent_provider2.cls is dependent_provider_copy2.cls + assert dependent_provider2 is not dependent_provider_copy2 + + +def test_deepcopy_kwargs(): + provider = providers.ContextLocalResource(init_fn) + dependent_provider1 = providers.Factory(list) + dependent_provider2 = providers.Factory(dict) + + provider.add_kwargs(d1=dependent_provider1, d2=dependent_provider2) + + provider_copy = providers.deepcopy(provider) + dependent_provider_copy1 = provider_copy.kwargs["d1"] + dependent_provider_copy2 = provider_copy.kwargs["d2"] + + assert provider.kwargs != provider_copy.kwargs + + assert dependent_provider1.cls is dependent_provider_copy1.cls + assert dependent_provider1 is not dependent_provider_copy1 + + assert dependent_provider2.cls is dependent_provider_copy2.cls + assert dependent_provider2 is not dependent_provider_copy2 + + +def test_deepcopy_overridden(): + provider = providers.ContextLocalResource(init_fn) + object_provider = providers.Object(object()) + + provider.override(object_provider) + + provider_copy = providers.deepcopy(provider) + object_provider_copy = provider_copy.overridden[0] + + assert provider is not provider_copy + assert provider.args == provider_copy.args + assert isinstance(provider, providers.ContextLocalResource) + + assert object_provider is not object_provider_copy + assert isinstance(object_provider_copy, providers.Object) + + +def test_deepcopy_with_sys_streams(): + provider = providers.ContextLocalResource(init_fn) + provider.add_args(sys.stdin, sys.stdout, sys.stderr) + + provider_copy = providers.deepcopy(provider) + + assert provider is not provider_copy + assert isinstance(provider_copy, providers.ContextLocalResource) + assert provider.args[0] is sys.stdin + assert provider.args[1] is sys.stdout + assert provider.args[2] is sys.stderr + + +def test_repr(): + provider = providers.ContextLocalResource(init_fn) + + assert repr(provider) == ( + "".format( + repr(init_fn), + hex(id(provider)), + ) + ) diff --git a/tests/unit/samples/wiring/asyncinjections.py b/tests/unit/samples/wiring/asyncinjections.py index e0861017..befd59b0 100644 --- a/tests/unit/samples/wiring/asyncinjections.py +++ b/tests/unit/samples/wiring/asyncinjections.py @@ -18,6 +18,7 @@ def reset_counters(self): resource1 = TestResource() resource2 = TestResource() +resource3 = TestResource() async def async_resource(resource): @@ -34,6 +35,8 @@ class Container(containers.DeclarativeContainer): resource1 = providers.Resource(async_resource, providers.Object(resource1)) resource2 = providers.Resource(async_resource, providers.Object(resource2)) + context_local_resource = providers.ContextLocalResource(async_resource, providers.Object(resource3)) + context_local_resource_with_factory_object = providers.ContextLocalResource(async_resource, providers.Factory(TestResource)) @inject @@ -57,5 +60,13 @@ async def async_generator_injection( async def async_injection_with_closing( resource1: object = Closing[Provide[Container.resource1]], resource2: object = Closing[Provide[Container.resource2]], + context_local_resource: object = Closing[Provide[Container.context_local_resource]], ): - return resource1, resource2 + return resource1, resource2, context_local_resource + + +@inject +async def async_injection_with_closing_context_local_resources( + context_local_resource1: object = Closing[Provide[Container.context_local_resource_with_factory_object]], +): + return context_local_resource1 diff --git a/tests/unit/samples/wiring/container.py b/tests/unit/samples/wiring/container.py index 95681dbf..69eb7115 100644 --- a/tests/unit/samples/wiring/container.py +++ b/tests/unit/samples/wiring/container.py @@ -1,6 +1,6 @@ from dependency_injector import containers, providers -from .service import Service +from .service import Service, ServiceWithCallable class SubContainer(containers.DeclarativeContainer): @@ -14,4 +14,6 @@ class Container(containers.DeclarativeContainer): service = providers.Factory(Service) + service_with_callable = providers.Factory(ServiceWithCallable) + sub = providers.Container(SubContainer) diff --git a/tests/unit/samples/wiring/module.py b/tests/unit/samples/wiring/module.py index 3925b295..dbec1207 100644 --- a/tests/unit/samples/wiring/module.py +++ b/tests/unit/samples/wiring/module.py @@ -100,6 +100,27 @@ def test_provided_instance(some_value: int = Provide[Container.service.provided. return some_value +@inject +def test_provided_instance_call_with_args( + some_value: int = Provide[Container.service_with_callable.provided.method_with_args.call(1, 2)] +): + return some_value + + +@inject +def test_provided_instance_call_with_kwargs( + some_value: dict = Provide[Container.service_with_callable.provided.method_with_kwargs.call(a=1, b=2)] +): + return some_value + + +@inject +def test_provided_instance_call_with_args_and_kwargs( + some_value: dict = Provide[Container.service_with_callable.provided.foo.process.call(1, 2, key="value")] +): + return some_value + + @inject def test_subcontainer_provider(some_value: int = Provide[Container.sub.int_object]): return some_value diff --git a/tests/unit/samples/wiring/module_annotated.py b/tests/unit/samples/wiring/module_annotated.py index f954d0cb..3af049f2 100644 --- a/tests/unit/samples/wiring/module_annotated.py +++ b/tests/unit/samples/wiring/module_annotated.py @@ -124,3 +124,10 @@ def test_class_decorator(service: Annotated[Service, Provide[Container.service]] def test_container(container: Annotated[Container, Provide[Container]]): return container.service() + + +@inject +def test_annotated_with_non_di_metadata_first( + service: Annotated[Service, "some other annotated value", Provide[Container.service]], +): + return service diff --git a/tests/unit/samples/wiring/service.py b/tests/unit/samples/wiring/service.py index 4151b94f..944b9921 100644 --- a/tests/unit/samples/wiring/service.py +++ b/tests/unit/samples/wiring/service.py @@ -1,2 +1,21 @@ class Service: service_attr: int + + +class ServiceWithCallable: + def __init__(self): + self.foo = CallableDict({"bar": lambda: 10}) + + def method_with_args(self, x, y): + return x + y + + def method_with_kwargs(self, **kwargs): + return kwargs + + +class CallableDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def process(self, *args, **kwargs): + return {"args": args, "kwargs": kwargs} diff --git a/tests/unit/samples/wiringcython/__init__.py b/tests/unit/samples/wiringcython/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/samples/wiringcython/container.py b/tests/unit/samples/wiringcython/container.py new file mode 100644 index 00000000..ba414bc9 --- /dev/null +++ b/tests/unit/samples/wiringcython/container.py @@ -0,0 +1,20 @@ +"""DI container used by the Cython-compiled wiring fixture.""" + +from dependency_injector import containers, providers + + +class Service: + """Simple service injected into the Cython-compiled fixture handlers.""" + + def __init__(self, value: str = "default") -> None: + self.value = value + + async def aget(self) -> str: + return self.value + + def get(self) -> str: + return self.value + + +class Container(containers.DeclarativeContainer): + service = providers.Factory(Service, value="injected") diff --git a/tests/unit/samples/wiringcython/cythonmodule.pyx b/tests/unit/samples/wiringcython/cythonmodule.pyx new file mode 100644 index 00000000..4642dd49 --- /dev/null +++ b/tests/unit/samples/wiringcython/cythonmodule.pyx @@ -0,0 +1,23 @@ +# cython: language_level=3, binding=True, embedsignature=True, annotation_typing=False + +from dependency_injector.wiring import Provide + +from samples.wiringcython.container import Container, Service + + +def sync_handler(svc: Service = Provide[Container.service]) -> str: + return svc.get() + + +async def async_handler(svc: Service = Provide[Container.service]) -> str: + return await svc.aget() + + +async def async_gen_handler(svc: Service = Provide[Container.service]): + yield svc.get() + yield svc.get() + "_2" + + +class HandlerClass: + async def __call__(self, svc: Service = Provide[Container.service]) -> str: + return svc.get() diff --git a/tests/unit/samples/wiringstringids/asyncinjections.py b/tests/unit/samples/wiringstringids/asyncinjections.py index 41529379..514b455a 100644 --- a/tests/unit/samples/wiringstringids/asyncinjections.py +++ b/tests/unit/samples/wiringstringids/asyncinjections.py @@ -16,6 +16,7 @@ def reset_counters(self): resource1 = TestResource() resource2 = TestResource() +resource3 = TestResource() async def async_resource(resource): @@ -32,6 +33,8 @@ class Container(containers.DeclarativeContainer): resource1 = providers.Resource(async_resource, providers.Object(resource1)) resource2 = providers.Resource(async_resource, providers.Object(resource2)) + context_local_resource = providers.ContextLocalResource(async_resource, providers.Object(resource3)) + context_local_resource_with_factory_object = providers.ContextLocalResource(async_resource, providers.Factory(TestResource)) @inject @@ -46,5 +49,13 @@ async def async_injection( async def async_injection_with_closing( resource1: object = Closing[Provide["resource1"]], resource2: object = Closing[Provide["resource2"]], + context_local_resource: object = Closing[Provide["context_local_resource"]], ): - return resource1, resource2 + return resource1, resource2, context_local_resource + + +@inject +async def async_injection_with_closing_context_local_resources( + context_local_resource1: object = Closing[Provide["context_local_resource_with_factory_object"]] +): + return context_local_resource1 diff --git a/tests/unit/samples/wiringstringids/container.py b/tests/unit/samples/wiringstringids/container.py index 95681dbf..69eb7115 100644 --- a/tests/unit/samples/wiringstringids/container.py +++ b/tests/unit/samples/wiringstringids/container.py @@ -1,6 +1,6 @@ from dependency_injector import containers, providers -from .service import Service +from .service import Service, ServiceWithCallable class SubContainer(containers.DeclarativeContainer): @@ -14,4 +14,6 @@ class Container(containers.DeclarativeContainer): service = providers.Factory(Service) + service_with_callable = providers.Factory(ServiceWithCallable) + sub = providers.Container(SubContainer) diff --git a/tests/unit/samples/wiringstringids/missing.py b/tests/unit/samples/wiringstringids/missing.py new file mode 100644 index 00000000..b8bafae5 --- /dev/null +++ b/tests/unit/samples/wiringstringids/missing.py @@ -0,0 +1,15 @@ +from dependency_injector.wiring import Provide, inject + +missing_obj: object = Provide["missing"] + + +class TestMissingClass: + obj: object = Provide["missing"] + + def method(self, obj: object = Provide["missing"]) -> object: + return obj + + +@inject +def test_missing_function(obj: object = Provide["missing"]): + return obj diff --git a/tests/unit/samples/wiringstringids/module.py b/tests/unit/samples/wiringstringids/module.py index aac85aa8..8e6662d2 100644 --- a/tests/unit/samples/wiringstringids/module.py +++ b/tests/unit/samples/wiringstringids/module.py @@ -4,21 +4,20 @@ from typing import Callable from dependency_injector.wiring import ( - inject, Provide, Provider, - as_int, - as_float, as_, - required, + as_float, + as_int, + inject, invariant, provided, + required, ) from .container import Container from .service import Service - service: Service = Provide["service"] service_provider: Callable[..., Service] = Provider["service"] undefined: Callable = Provide["undefined"] @@ -55,22 +54,24 @@ def test_function(service: Service = Provide["service"]): @inject -def test_function_provider(service_provider: Callable[..., Service] = Provider["service"]): +def test_function_provider( + service_provider: Callable[..., Service] = Provider["service"], +): service = service_provider() return service @inject def test_config_value( - value_int: int = Provide["config.a.b.c", as_int()], - value_float: float = Provide["config.a.b.c", as_float()], - value_str: str = Provide["config.a.b.c", as_(str)], - value_decimal: Decimal = Provide["config.a.b.c", as_(Decimal)], - value_required: str = Provide["config.a.b.c", required()], - value_required_int: int = Provide["config.a.b.c", required().as_int()], - value_required_float: float = Provide["config.a.b.c", required().as_float()], - value_required_str: str = Provide["config.a.b.c", required().as_(str)], - value_required_decimal: str = Provide["config.a.b.c", required().as_(Decimal)], + value_int: int = Provide["config.a.b.c", as_int()], + value_float: float = Provide["config.a.b.c", as_float()], + value_str: str = Provide["config.a.b.c", as_(str)], + value_decimal: Decimal = Provide["config.a.b.c", as_(Decimal)], + value_required: str = Provide["config.a.b.c", required()], + value_required_int: int = Provide["config.a.b.c", required().as_int()], + value_required_float: float = Provide["config.a.b.c", required().as_float()], + value_required_str: str = Provide["config.a.b.c", required().as_(str)], + value_required_decimal: str = Provide["config.a.b.c", required().as_(Decimal)], ): return ( value_int, @@ -87,25 +88,60 @@ def test_config_value( @inject def test_config_value_required_undefined( - value_required: int = Provide["config.a.b.c", required()], + value_required: int = Provide["config.a.b.c", required()], ): return value_required @inject -def test_provide_provider(service_provider: Callable[..., Service] = Provide["service.provider"]): +def test_provide_provider( + service_provider: Callable[..., Service] = Provide["service.provider"], +): service = service_provider() return service @inject -def test_provider_provider(service_provider: Callable[..., Service] = Provider["service.provider"]): +def test_provider_provider( + service_provider: Callable[..., Service] = Provider["service.provider"], +): service = service_provider() return service @inject -def test_provided_instance(some_value: int = Provide["service", provided().foo["bar"].call()]): +def test_provided_instance( + some_value: int = Provide["service", provided().foo["bar"].call()] +): + return some_value + + +@inject +def test_provided_instance_call_with_args( + some_value: int = Provide[ + "service_with_callable", + provided().method_with_args.call(1, 2), + ], +): + return some_value + + +@inject +def test_provided_instance_call_with_kwargs( + some_value: dict = Provide[ + "service_with_callable", + provided().method_with_kwargs.call(a=1, b=2), + ], +): + return some_value + + +@inject +def test_provided_instance_call_with_args_and_kwargs( + some_value: dict = Provide[ + "service_with_callable", provided().foo.process.call(1, 2, key="value") + ] +): return some_value @@ -115,14 +151,16 @@ def test_subcontainer_provider(some_value: int = Provide["sub.int_object"]): @inject -def test_config_invariant(some_value: int = Provide["config.option", invariant("config.switch")]): +def test_config_invariant( + some_value: int = Provide["config.option", invariant("config.switch")] +): return some_value @inject def test_provide_from_different_containers( - service: Service = Provide["service"], - some_value: int = Provide["int_object"], + service: Service = Provide["service"], + some_value: int = Provide["int_object"], ): return service, some_value diff --git a/tests/unit/samples/wiringstringids/service.py b/tests/unit/samples/wiringstringids/service.py index 4151b94f..944b9921 100644 --- a/tests/unit/samples/wiringstringids/service.py +++ b/tests/unit/samples/wiringstringids/service.py @@ -1,2 +1,21 @@ class Service: service_attr: int + + +class ServiceWithCallable: + def __init__(self): + self.foo = CallableDict({"bar": lambda: 10}) + + def method_with_args(self, x, y): + return x + y + + def method_with_kwargs(self, **kwargs): + return kwargs + + +class CallableDict(dict): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def process(self, *args, **kwargs): + return {"args": args, "kwargs": kwargs} diff --git a/tests/unit/wiring/provider_ids/test_async_injections_py36.py b/tests/unit/wiring/provider_ids/test_async_injections_py36.py index 70f9eb17..4c5ec12f 100644 --- a/tests/unit/wiring/provider_ids/test_async_injections_py36.py +++ b/tests/unit/wiring/provider_ids/test_async_injections_py36.py @@ -1,7 +1,8 @@ """Async injection tests.""" -from pytest import fixture, mark +import asyncio +from pytest import fixture, mark from samples.wiring import asyncinjections @@ -51,7 +52,7 @@ async def test_async_generator_injections() -> None: @mark.asyncio async def test_async_injections_with_closing(): - resource1, resource2 = await asyncinjections.async_injection_with_closing() + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 1 @@ -61,7 +62,11 @@ async def test_async_injections_with_closing(): assert asyncinjections.resource2.init_counter == 1 assert asyncinjections.resource2.shutdown_counter == 1 - resource1, resource2 = await asyncinjections.async_injection_with_closing() + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 1 + assert asyncinjections.resource3.shutdown_counter == 1 + + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 2 @@ -70,3 +75,19 @@ async def test_async_injections_with_closing(): assert resource2 is asyncinjections.resource2 assert asyncinjections.resource2.init_counter == 2 assert asyncinjections.resource2.shutdown_counter == 2 + + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 2 + assert asyncinjections.resource3.shutdown_counter == 2 + + +@mark.asyncio +async def test_async_injections_with_closing_concurrently(): + resource1, resource2 = await asyncio.gather(asyncinjections.async_injection_with_closing_context_local_resources(), + asyncinjections.async_injection_with_closing_context_local_resources()) + assert resource1 != resource2 + + resource1 = await asyncinjections.Container.context_local_resource_with_factory_object() + resource2 = await asyncinjections.Container.context_local_resource_with_factory_object() + + assert resource1 == resource2 diff --git a/tests/unit/wiring/provider_ids/test_main_annotated_py36.py b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py index 34d1d747..5e289cd0 100644 --- a/tests/unit/wiring/provider_ids/test_main_annotated_py36.py +++ b/tests/unit/wiring/provider_ids/test_main_annotated_py36.py @@ -174,3 +174,14 @@ def test_class_decorator(): def test_container(): service = module.test_container() assert isinstance(service, Service) + + +def test_annotated_with_non_di_metadata_first(): + """Test that Annotated works when DI marker is not the first metadata item. + + This tests the case where Annotated has other metadata (like docstrings or + other annotations) before the Provide marker, e.g.: + Annotated[Service, "some doc", Provide[Container.service]] + """ + service = module.test_annotated_with_non_di_metadata_first() + assert isinstance(service, Service) diff --git a/tests/unit/wiring/provider_ids/test_main_py36.py b/tests/unit/wiring/provider_ids/test_main_py36.py index 15ac31c0..21b16881 100644 --- a/tests/unit/wiring/provider_ids/test_main_py36.py +++ b/tests/unit/wiring/provider_ids/test_main_py36.py @@ -1,5 +1,6 @@ """Main wiring tests.""" +import re from decimal import Decimal from dependency_injector import errors @@ -67,7 +68,7 @@ def test_module_attributes_wiring(): def test_module_attribute_wiring_with_invalid_marker(container: Container): from samples.wiring import module_invalid_attr_injection - with raises(Exception, match="Unknown type of marker {0}".format(module_invalid_attr_injection.service)): + with raises(Exception, match=re.escape("Unknown type of marker {0}".format(module_invalid_attr_injection.service))): container.wire(modules=[module_invalid_attr_injection]) @@ -187,6 +188,21 @@ class TestService: assert some_value == 10 +def test_provided_instance_call_with_args(): + some_value = module.test_provided_instance_call_with_args() + assert some_value == 3 + + +def test_provided_instance_call_with_kwargs(): + some_value = module.test_provided_instance_call_with_kwargs() + assert some_value == {"a": 1, "b": 2} + + +def test_provided_instance_call_with_args_and_kwargs(): + some_value = module.test_provided_instance_call_with_args_and_kwargs() + assert some_value == {"args": (1, 2), "kwargs": {"key": "value"}} + + def test_subcontainer(): some_value = module.test_subcontainer_provider() assert some_value == 1 diff --git a/tests/unit/wiring/string_ids/test_async_injections_py36.py b/tests/unit/wiring/string_ids/test_async_injections_py36.py index cff13ce5..bdf6a2ab 100644 --- a/tests/unit/wiring/string_ids/test_async_injections_py36.py +++ b/tests/unit/wiring/string_ids/test_async_injections_py36.py @@ -1,7 +1,8 @@ """Async injection tests.""" -from pytest import fixture, mark +import asyncio +from pytest import fixture, mark from samples.wiringstringids import asyncinjections @@ -34,7 +35,7 @@ async def test_async_injections(): @mark.asyncio async def test_async_injections_with_closing(): - resource1, resource2 = await asyncinjections.async_injection_with_closing() + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 1 @@ -44,7 +45,11 @@ async def test_async_injections_with_closing(): assert asyncinjections.resource2.init_counter == 1 assert asyncinjections.resource2.shutdown_counter == 1 - resource1, resource2 = await asyncinjections.async_injection_with_closing() + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 1 + assert asyncinjections.resource3.shutdown_counter == 1 + + resource1, resource2, context_local_resource = await asyncinjections.async_injection_with_closing() assert resource1 is asyncinjections.resource1 assert asyncinjections.resource1.init_counter == 2 @@ -53,3 +58,19 @@ async def test_async_injections_with_closing(): assert resource2 is asyncinjections.resource2 assert asyncinjections.resource2.init_counter == 2 assert asyncinjections.resource2.shutdown_counter == 2 + + assert context_local_resource is asyncinjections.resource3 + assert asyncinjections.resource3.init_counter == 2 + assert asyncinjections.resource3.shutdown_counter == 2 + + +@mark.asyncio +async def test_async_injections_with_closing_concurrently(): + resource1, resource2 = await asyncio.gather(asyncinjections.async_injection_with_closing_context_local_resources(), + asyncinjections.async_injection_with_closing_context_local_resources()) + assert resource1 != resource2 + + resource1 = await asyncinjections.Container.context_local_resource_with_factory_object() + resource2 = await asyncinjections.Container.context_local_resource_with_factory_object() + + assert resource1 == resource2 diff --git a/tests/unit/wiring/string_ids/test_main_py36.py b/tests/unit/wiring/string_ids/test_main_py36.py index 8125481a..4731e876 100644 --- a/tests/unit/wiring/string_ids/test_main_py36.py +++ b/tests/unit/wiring/string_ids/test_main_py36.py @@ -1,14 +1,21 @@ """Main wiring tests.""" +import re from decimal import Decimal -from pytest import fixture, mark, raises +from pytest import fixture, mark, raises, warns from samples.wiringstringids import module, package, resourceclosing from samples.wiringstringids.container import Container, SubContainer from samples.wiringstringids.service import Service from dependency_injector import errors -from dependency_injector.wiring import Closing, Provide, Provider, wire +from dependency_injector.wiring import ( + Closing, + Provide, + Provider, + UnresolvedMarkerWarning, + wire, +) @fixture(autouse=True) @@ -44,18 +51,21 @@ def resourceclosing_container(request): def test_package_lookup(): from samples.wiringstringids.package import test_package_function + service = test_package_function() assert isinstance(service, Service) def test_package_subpackage_lookup(): from samples.wiringstringids.package.subpackage import test_package_function + service = test_package_function() assert isinstance(service, Service) def test_package_submodule_lookup(): from samples.wiringstringids.package.subpackage.submodule import test_function + service = test_function() assert isinstance(service, Service) @@ -68,10 +78,26 @@ def test_module_attributes_wiring(): def test_module_attribute_wiring_with_invalid_marker(container: Container): from samples.wiringstringids import module_invalid_attr_injection - with raises(Exception, match="Unknown type of marker {0}".format(module_invalid_attr_injection.service)): + + with raises( + Exception, + match=re.escape( + "Unknown type of marker {0}".format(module_invalid_attr_injection.service) + ), + ): container.wire(modules=[module_invalid_attr_injection]) +def test_warn_unresolved_marker(container: Container): + from samples.wiringstringids import missing + + with warns( + UnresolvedMarkerWarning, + match=r"^Unresolved marker .+ in .+$", + ): + container.wire(modules=[missing], warn_unresolved=True) + + def test_class_wiring(): test_class_object = module.TestClass() assert isinstance(test_class_object.service, Service) @@ -165,7 +191,7 @@ def test_configuration_option(): def test_configuration_option_required_undefined(container: Container): container.config.reset_override() - with raises(errors.Error, match="Undefined configuration option \"config.a.b.c\""): + with raises(errors.Error, match='Undefined configuration option "config.a.b.c"'): module.test_config_value_required_undefined() @@ -188,6 +214,21 @@ class TestService: assert some_value == 10 +def test_provided_instance_call_with_args(): + some_value = module.test_provided_instance_call_with_args() + assert some_value == 3 + + +def test_provided_instance_call_with_kwargs(): + some_value = module.test_provided_instance_call_with_kwargs() + assert some_value == {"a": 1, "b": 2} + + +def test_provided_instance_call_with_args_and_kwargs(): + some_value = module.test_provided_instance_call_with_args_and_kwargs() + assert some_value == {"args": (1, 2), "kwargs": {"key": "value"}} + + def test_subcontainer(): some_value = module.test_subcontainer_provider() assert some_value == 1 @@ -243,11 +284,13 @@ def test_unwire_class_method(container: Container): def test_unwire_package_function(container: Container): container.unwire() from samples.wiringstringids.package.subpackage.submodule import test_function + assert isinstance(test_function(), Provide) def test_unwire_package_function_by_reference(container: Container): from samples.wiringstringids.package.subpackage import submodule + container.unwire() assert isinstance(submodule.test_function(), Provide) diff --git a/tests/unit/wiring/test_cython.py b/tests/unit/wiring/test_cython.py new file mode 100644 index 00000000..80a9a42a --- /dev/null +++ b/tests/unit/wiring/test_cython.py @@ -0,0 +1,102 @@ +"""Wiring discovery against Cython-compiled user modules.""" + +import pytest + +pytest.importorskip("Cython") + +import pyximport # noqa: E402 + +pyximport.install(language_level=3) + +cythonmodule = pytest.importorskip( + "samples.wiringcython.cythonmodule", + reason="Cython fixture not built (Cython / C toolchain missing)", +) + +from samples.wiringcython.container import Container, Service # noqa: E402 + +from dependency_injector import providers # noqa: E402 +from dependency_injector.wiring import ( # noqa: E402 + _is_cyfunction, + _is_function_like, + _patched_registry, +) + + +@pytest.fixture +def container(): + c = Container() + c.wire(modules=[cythonmodule]) + yield c + c.unwire() + + +def _pure_python_fn(): + pass + + +@pytest.mark.parametrize( + "obj,is_cy,is_func_like", + [ + pytest.param(lambda: cythonmodule.sync_handler, True, True, id="cython-sync"), + pytest.param(lambda: cythonmodule.async_handler, True, True, id="cython-async"), + pytest.param( + lambda: cythonmodule.async_gen_handler, True, True, id="cython-async-gen" + ), + pytest.param( + lambda: cythonmodule.HandlerClass.__call__, + True, + True, + id="cython-class-call", + ), + pytest.param(lambda: _pure_python_fn, False, True, id="pure-python"), + ], +) +def test_function_like_predicate(obj, is_cy, is_func_like): + target = obj() + assert _is_cyfunction(target) is is_cy + assert _is_function_like(target) is is_func_like + + +def test_sync_handler_wired(container): + assert cythonmodule.sync_handler() == "injected" + + +@pytest.mark.asyncio +async def test_async_handler_wired(container): + assert await cythonmodule.async_handler() == "injected" + + +@pytest.mark.asyncio +async def test_async_gen_handler_wired(container): + results = [v async for v in cythonmodule.async_gen_handler()] + assert results == ["injected", "injected_2"] + + +@pytest.mark.asyncio +async def test_class_method_wired(container): + handler = cythonmodule.HandlerClass() + assert await handler() == "injected" + + +def test_sync_handler_respects_provider_override(container): + with container.service.override(providers.Object(Service(value="overridden"))): + assert cythonmodule.sync_handler() == "overridden" + assert cythonmodule.sync_handler() == "injected" + + +def test_unwire_clears_injection_bindings_on_compiled_module(): + c = Container() + c.wire(modules=[cythonmodule]) + + wrapper = cythonmodule.sync_handler + patched = _patched_registry.get_callable(wrapper) + + assert patched is not None + assert patched.reference_injections + assert patched.injections + + c.unwire() + + assert patched.injections == {} + assert patched.reference_injections diff --git a/tests/unit/wiring/test_reprs.py b/tests/unit/wiring/test_reprs.py new file mode 100644 index 00000000..24da7424 --- /dev/null +++ b/tests/unit/wiring/test_reprs.py @@ -0,0 +1,42 @@ +from dependency_injector.wiring import ( + Closing, + InvariantModifier, + Provide, + ProvidedInstance, + RequiredModifier, + TypeModifier, +) + + +def test_type_modifier_repr() -> None: + assert repr(TypeModifier(int)) == f"TypeModifier({int!r})" + + +def test_required_modifier_repr() -> None: + assert repr(RequiredModifier()) == "RequiredModifier()" + + +def test_required_modifier_with_type_repr() -> None: + type_modifier = TypeModifier(int) + required_modifier = RequiredModifier(type_modifier) + assert repr(required_modifier) == f"RequiredModifier({type_modifier!r})" + + +def test_invariant_modifier_repr() -> None: + assert repr(InvariantModifier("test")) == "InvariantModifier('test')" + + +def test_provided_instance_repr() -> None: + provided_instance = ProvidedInstance().test["attr"].call() + + assert repr(provided_instance) == "ProvidedInstance().test['attr'].call()" + + +def test_marker_repr() -> None: + assert repr(Closing[Provide["test"]]) == "Closing[Provide['test']]" + + +def test_marker_with_modifier_repr() -> None: + marker = Provide["test", RequiredModifier()] + + assert repr(marker) == "Provide['test', RequiredModifier()]" diff --git a/tox.ini b/tox.ini index cadccd84..cc1c02f4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] parallel_show_output = true envlist= - coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10, pypy3.11 + coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14, pypy3.9, pypy3.10, pypy3.11 [testenv] deps= @@ -18,6 +18,9 @@ deps= pydantic-settings werkzeug fast-depends + # Cython is required to build tests/unit/samples/wiringcython/ + cython>=3,<4 + setuptools extras= yaml commands = pytest @@ -50,7 +53,7 @@ commands = pytest -m pydantic [testenv:coveralls] passenv = GITHUB_*, COVERALLS_*, DEPENDENCY_INJECTOR_* -basepython=python3.12 # TODO: Upgrade to version 3.13 is blocked by coveralls 4.0.1 not supporting Python 3.13 +basepython=python3.14 deps= {[testenv]deps} cython>=3,<4