diff --git a/.github/workflows/tests-and-linters.yml b/.github/workflows/tests-and-linters.yml index a924c5e7..f43a2db8 100644 --- a/.github/workflows/tests-and-linters.yml +++ b/.github/workflows/tests-and-linters.yml @@ -4,28 +4,12 @@ on: [push, pull_request, workflow_dispatch] jobs: - tests-on-legacy-versions: - name: Run tests on legacy versions - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [3.7] - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - run: pip install tox - - run: tox - env: - TOXENV: ${{ matrix.python-version }} - test-on-different-versions: name: Run tests 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"] steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 diff --git a/Makefile b/Makefile index 84d0ef86..29e4086f 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ uninstall: test: # Unit tests with coverage report coverage erase - coverage run -m pytest -c tests/.configs/pytest.ini + coverage run -m pytest coverage report coverage html diff --git a/pyproject.toml b/pyproject.toml index eba17764..ec5daadd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ maintainers = [ description = "Dependency injection framework for Python" readme = {file = "README.rst", content-type = "text/x-rst"} license = {file = "LICENSE.rst", content-type = "text/x-rst"} -requires-python = ">=3.7" +requires-python = ">=3.8" keywords = [ "Dependency injection", "DI", @@ -31,7 +31,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -99,3 +98,17 @@ ignore = ["tests"] [tool.pylint.design] min-public-methods = 0 max-public-methods = 30 + +[tool.pytest.ini_options] +testpaths = ["tests/unit/"] +asyncio_mode = "auto" +markers = [ + "pydantic: Tests with Pydantic as a dependency", +] +filterwarnings = [ + "ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\\.0\\.0:DeprecationWarning", + "ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\\.0\\.0:DeprecationWarning", + "ignore:Please use \\`.*?\\` from the \\`scipy.*?\\`(.*?)namespace is deprecated\\.:DeprecationWarning", + "ignore:Please import \\`.*?\\` from the \\`scipy(.*?)\\` namespace(.*):DeprecationWarning", + "ignore:\\`scipy(.*?)\\` is deprecated(.*):DeprecationWarning", +] diff --git a/requirements-dev.txt b/requirements-dev.txt index 0d759d4e..e0def494 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,4 @@ -cython==3.0.11 +cython==3.1.0 setuptools pytest pytest-asyncio diff --git a/setup.py b/setup.py index 429e672c..877d9472 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,9 @@ from setuptools import Extension, setup debug = os.environ.get("DEPENDENCY_INJECTOR_DEBUG_MODE") == "1" +limited_api = os.environ.get("DEPENDENCY_INJECTOR_LIMITED_API") == "1" defined_macros = [] +options = {} compiler_directives = { "language_level": 3, "profile": debug, @@ -17,6 +19,7 @@ # Adding debug options: if debug: + limited_api = False # line tracing is not part of the Limited API defined_macros.extend( [ ("CYTHON_TRACE", "1"), @@ -25,14 +28,20 @@ ] ) +if limited_api: + options.setdefault("bdist_wheel", {}) + options["bdist_wheel"]["py_limited_api"] = "cp38" + defined_macros.append(("Py_LIMITED_API", 0x03080000)) setup( + options=options, ext_modules=cythonize( [ Extension( "*", ["src/**/*.pyx"], define_macros=defined_macros, + py_limited_api=limited_api, ), ], annotate=debug, diff --git a/src/dependency_injector/providers.pyx b/src/dependency_injector/providers.pyx index 73c5cbe1..d276903b 100644 --- a/src/dependency_injector/providers.pyx +++ b/src/dependency_injector/providers.pyx @@ -4,7 +4,6 @@ from __future__ import absolute_import import asyncio import builtins -import contextvars import copy import errno import functools @@ -17,6 +16,7 @@ import sys import threading import warnings from configparser import ConfigParser as IniConfigParser +from contextvars import ContextVar try: from inspect import _is_coroutine_mark as _is_coroutine_marker @@ -1592,8 +1592,7 @@ cdef class ConfigurationOption(Provider): segment() if is_provider(segment) else segment for segment in self._name ) - @property - def root(self): + def _get_root(self): return self._root def get_name(self): @@ -3224,15 +3223,10 @@ cdef class ContextLocalSingleton(BaseSingleton): :param provides: Provided type. :type provides: type """ - if not contextvars: - raise RuntimeError( - "Contextvars library not found. This provider " - "requires Python 3.7 or a backport of contextvars. " - "To install a backport run \"pip install contextvars\"." - ) + super(ContextLocalSingleton, self).__init__(provides, *args, **kwargs) - self._storage = contextvars.ContextVar("_storage", default=self._none) + self._storage = ContextVar("_storage", default=self._none) def reset(self): """Reset cached instance, if any. diff --git a/src/dependency_injector/wiring.py b/src/dependency_injector/wiring.py index 9de6f823..1effd16f 100644 --- a/src/dependency_injector/wiring.py +++ b/src/dependency_injector/wiring.py @@ -314,7 +314,7 @@ def _resolve_config_option( original: providers.ConfigurationOption, as_: Any = None, ) -> Optional[providers.Provider]: - original_root = original.root + original_root = original._get_root() new = self._resolve_provider(original_root) if new is None: return None @@ -523,6 +523,10 @@ def _patch_method( _bind_injections(fn, providers_map) + if fn is method: + # Hotfix, see: https://github.com/ets-labs/python-dependency-injector/issues/884 + return + if isinstance(method, (classmethod, staticmethod)): fn = type(method)(fn) @@ -628,21 +632,6 @@ def _fetch_reference_injections( # noqa: C901 return injections, closing -def _locate_dependent_closing_args( - provider: providers.Provider, closing_deps: Dict[str, providers.Provider] -) -> Dict[str, providers.Provider]: - for arg in [ - *getattr(provider, "args", []), - *getattr(provider, "kwargs", {}).values(), - ]: - if not isinstance(arg, providers.Provider): - continue - if isinstance(arg, providers.Resource): - closing_deps[str(id(arg))] = arg - - _locate_dependent_closing_args(arg, closing_deps) - - def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> None: patched_callable = _patched_registry.get_callable(fn) if patched_callable is None: @@ -664,10 +653,9 @@ def _bind_injections(fn: Callable[..., Any], providers_map: ProvidersMap) -> Non if injection in patched_callable.reference_closing: patched_callable.add_closing(injection, provider) - deps = {} - _locate_dependent_closing_args(provider, deps) - for key, dep in deps.items(): - patched_callable.add_closing(key, dep) + + for resource in provider.traverse(types=[providers.Resource]): + patched_callable.add_closing(str(id(resource)), resource) def _unbind_injections(fn: Callable[..., Any]) -> None: diff --git a/tests/.configs/pytest.ini b/tests/.configs/pytest.ini deleted file mode 100644 index ea92be96..00000000 --- a/tests/.configs/pytest.ini +++ /dev/null @@ -1,13 +0,0 @@ -[pytest] -testpaths = tests/unit/ -python_files = test_*_py3*.py -asyncio_mode = auto -markers = - pydantic: Tests with Pydantic as a dependency -filterwarnings = - ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning - ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning - ignore:Please use \`.*?\` from the \`scipy.*?\`(.*?)namespace is deprecated\.:DeprecationWarning - ignore:Please import \`.*?\` from the \`scipy(.*?)\` namespace(.*):DeprecationWarning - ignore:\`scipy(.*?)\` is deprecated(.*):DeprecationWarning - ignore:ssl\.PROTOCOL_TLS is deprecated:DeprecationWarning:botocore.* diff --git a/tests/unit/ext/test_flask_py2_py3.py b/tests/unit/ext/test_flask_py2_py3.py index e64de165..3e79e067 100644 --- a/tests/unit/ext/test_flask_py2_py3.py +++ b/tests/unit/ext/test_flask_py2_py3.py @@ -11,7 +11,7 @@ def index(): return "Hello World!" -def test(): +def _test(): return "Test!" @@ -25,7 +25,7 @@ class ApplicationContainer(containers.DeclarativeContainer): app = flask.Application(Flask, __name__) index_view = flask.View(index) - test_view = flask.View(test) + test_view = flask.View(_test) test_class_view = flask.ClassBasedView(Test) diff --git a/tests/unit/providers/resource/test_async_resource_py35.py b/tests/unit/providers/resource/test_async_resource_py35.py index ba983d60..1ca950a8 100644 --- a/tests/unit/providers/resource/test_async_resource_py35.py +++ b/tests/unit/providers/resource/test_async_resource_py35.py @@ -34,7 +34,6 @@ async def _init(): @mark.asyncio -@mark.skipif(sys.version_info < (3, 6), reason="requires Python 3.6+") async def test_init_async_generator(): resource = object() diff --git a/tests/unit/samples/wiringstringids/resourceclosing.py b/tests/unit/samples/wiringstringids/resourceclosing.py index c4d1f20f..5a3d2ba4 100644 --- a/tests/unit/samples/wiringstringids/resourceclosing.py +++ b/tests/unit/samples/wiringstringids/resourceclosing.py @@ -59,12 +59,13 @@ def init_service(counter: Counter, _list: List[int], _dict: Dict[str, int]): class Container(containers.DeclarativeContainer): + config = providers.Configuration(default={"a": 1, "b": 4}) counter = providers.Singleton(Counter) _list = providers.List( - providers.Callable(lambda a: a, a=1), providers.Callable(lambda b: b, 2) + providers.Callable(lambda a: a, a=config.a), providers.Callable(lambda b: b, 2) ) _dict = providers.Dict( - a=providers.Callable(lambda a: a, a=3), b=providers.Callable(lambda b: b, 4) + a=providers.Callable(lambda a: a, a=3), b=providers.Callable(lambda b: b, config.b) ) service = providers.Resource(init_service, counter, _list, _dict=_dict) service2 = providers.Resource(init_service, counter, _list, _dict=_dict) diff --git a/tests/unit/wiring/test_no_interference.py b/tests/unit/wiring/test_no_interference.py new file mode 100644 index 00000000..21f8b0e9 --- /dev/null +++ b/tests/unit/wiring/test_no_interference.py @@ -0,0 +1,40 @@ +from typing import Any, Iterator + +from pytest import fixture + +from dependency_injector.containers import DeclarativeContainer +from dependency_injector.providers import Object +from dependency_injector.wiring import Provide, inject + + +class A: + @inject + def foo(self, value: str = Provide["value"]) -> str: + return "A" + value + + +class B(A): ... + + +class C(A): + def foo(self, *args: Any, **kwargs: Any) -> str: + return "C" + super().foo() + + +class D(B, C): ... + + +class Container(DeclarativeContainer): + value = Object("X") + + +@fixture +def container() -> Iterator[Container]: + c = Container() + c.wire(modules=[__name__]) + yield c + c.unwire() + + +def test_preserve_mro(container: Container) -> None: + assert D().foo() == "CAX" diff --git a/tox.ini b/tox.ini index 54f99e57..29bc5a4f 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.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10 + coveralls, pylint, flake8, pydocstyle, pydantic-v1, pydantic-v2, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10 [testenv] deps= @@ -19,7 +19,7 @@ deps= werkzeug extras= yaml -commands = pytest -c tests/.configs/pytest.ini +commands = pytest python_files = test_*_py3*.py setenv = COVERAGE_RCFILE = pyproject.toml @@ -45,7 +45,7 @@ deps = boto3 mypy_boto3_s3 werkzeug -commands = pytest -c tests/.configs/pytest.ini -m pydantic +commands = pytest -m pydantic [testenv:coveralls] passenv = GITHUB_*, COVERALLS_*, DEPENDENCY_INJECTOR_* @@ -57,7 +57,7 @@ deps= coveralls>=4 commands= coverage erase - coverage run -m pytest -c tests/.configs/pytest.ini + coverage run -m pytest coverage report coveralls @@ -74,7 +74,7 @@ deps= mypy_boto3_s3 extras= yaml -commands = pytest -c tests/.configs/pytest-py35.ini +commands = pytest [testenv:pylint]