Merge branch 'release/4.44.0' into master

This commit is contained in:
Roman Mogylatov 2024-12-07 11:52:05 -05:00
commit 704e36a642
25 changed files with 401 additions and 269020 deletions

View File

@ -1,10 +0,0 @@
[run]
source = dependency_injector
omit = tests/unit
plugins = Cython.Coverage
[report]
show_missing = true
[html]
directory=reports/unittests/

View File

@ -36,6 +36,17 @@ jobs:
env: env:
TOXENV: ${{ matrix.python-version }} TOXENV: ${{ matrix.python-version }}
test-different-pydantic-versions:
name: Run tests with different pydantic versions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.12"
- run: pip install tox
- run: tox -e pydantic-v1,pydantic-v2
test-coverage: test-coverage:
name: Run tests with coverage name: Run tests with coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -50,7 +61,6 @@ jobs:
with: with:
python-version: 3.12 python-version: 3.12
- run: pip install tox 'cython>=3,<4' - run: pip install tox 'cython>=3,<4'
- run: make cythonize
- run: tox -vv - run: tox -vv
env: env:
TOXENV: coveralls TOXENV: coveralls

12
.gitignore vendored
View File

@ -63,13 +63,11 @@ venv*/
# Vim Rope # Vim Rope
.ropeproject/ .ropeproject/
# C extensions # Cython artifacts
src/dependency_injector/*.h src/**/*.c
src/dependency_injector/*.so src/**/*.h
src/dependency_injector/containers/*.h src/**/*.so
src/dependency_injector/containers/*.so src/**/*.html
src/dependency_injector/providers/*.h
src/dependency_injector/providers/*.so
# Workspace for samples # Workspace for samples
.workspace/ .workspace/

View File

@ -1,49 +0,0 @@
[MASTER]
# Add <file or directory> to the black list. It should be a base name, not a
# path. You may set this option multiple times.
ignore=utils,tests
[MESSAGES CONTROL]
# Disable the message(s) with the given id(s).
# disable-msg=
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=5
[TYPECHECK]
ignore-mixin-members=yes
# ignored-classes=
zope=no
# generated-members=providedBy,implementedBy,rawDataReceived
[DESIGN]
# Maximum number of arguments for function / method
max-args=10
# Maximum number of locals for function / method body
max-locals=20
# Maximum number of return / yield for function / method body
max-returns=10
# Maximum number of branch for function / method body
max-branchs=10
# Maximum number of statements in function / method body
max-statements=60
# Maximum number of parents for a class (see R0901).
max-parents=10
# Maximum number of attributes for a class (see R0902).
max-attributes=30
# Minimum number of public methods for a class (see R0903).
min-public-methods=0
# Maximum number of public methods for a class (see R0904).
max-public-methods=30

View File

@ -1,14 +1,6 @@
VERSION := $(shell python setup.py --version) VERSION := $(shell python setup.py --version)
CYTHON_SRC := $(shell find src/dependency_injector -name '*.pyx') export COVERAGE_RCFILE := pyproject.toml
CYTHON_DIRECTIVES = -Xlanguage_level=3
ifdef DEPENDENCY_INJECTOR_DEBUG_MODE
CYTHON_DIRECTIVES += -Xprofile=True
CYTHON_DIRECTIVES += -Xlinetrace=True
endif
clean: clean:
# Clean sources # Clean sources
@ -25,21 +17,17 @@ clean:
find examples -name '*.py[co]' -delete find examples -name '*.py[co]' -delete
find examples -name '__pycache__' -delete find examples -name '__pycache__' -delete
cythonize: build: clean
# Compile Cython to C # Compile C extensions
cython -a $(CYTHON_DIRECTIVES) $(CYTHON_SRC) python setup.py build_ext --inplace
# Move all Cython html reports # Move all Cython html reports
mkdir -p reports/cython/ mkdir -p reports/cython/
find src -name '*.html' -exec mv {} reports/cython/ \; find src -name '*.html' -exec mv {} reports/cython/ \;
build: clean cythonize
# Compile C extensions
python setup.py build_ext --inplace
docs-live: docs-live:
sphinx-autobuild docs docs/_build/html sphinx-autobuild docs docs/_build/html
install: uninstall clean cythonize install: uninstall clean build
pip install -ve . pip install -ve .
uninstall: uninstall:
@ -48,9 +36,9 @@ uninstall:
test: test:
# Unit tests with coverage report # Unit tests with coverage report
coverage erase coverage erase
coverage run --rcfile=./.coveragerc -m pytest -c tests/.configs/pytest.ini coverage run -m pytest -c tests/.configs/pytest.ini
coverage report --rcfile=./.coveragerc coverage report
coverage html --rcfile=./.coveragerc coverage html
check: check:
flake8 src/dependency_injector/ flake8 src/dependency_injector/
@ -61,9 +49,9 @@ check:
mypy tests/typing mypy tests/typing
test-publish: cythonize test-publish: build
# Create distributions # Create distributions
python setup.py sdist python -m build --sdist
# Upload distributions to PyPI # Upload distributions to PyPI
twine upload --repository testpypi dist/dependency-injector-$(VERSION)* twine upload --repository testpypi dist/dependency-injector-$(VERSION)*

View File

@ -7,6 +7,14 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
4.44.0
--------
- Implement support for Pydantic 2. PR: `#832 <https://github.com/ets-labs/python-dependency-injector/pull/832>`_.
- Implement `PEP-517 <https://peps.python.org/pep-0517/>`_, `PEP-518 <https://peps.python.org/pep-0518/>`_, and
`PEP-621 <https://peps.python.org/pep-0621/>`_. PR: `#829 <https://github.com/ets-labs/python-dependency-injector/pull/829>`_.
Many thanks to `ZipFile <https://github.com/ZipFile>`_ for both contributions.
4.43.0 4.43.0
-------- --------
- Add support for Python 3.13. - Add support for Python 3.13.

View File

@ -183,22 +183,22 @@ See also: :ref:`configuration-envs-interpolation`.
Loading from a Pydantic settings Loading from a Pydantic settings
-------------------------------- --------------------------------
``Configuration`` provider can load configuration from a ``pydantic`` settings object using the ``Configuration`` provider can load configuration from a ``pydantic_settings.BaseSettings`` object using the
:py:meth:`Configuration.from_pydantic` method: :py:meth:`Configuration.from_pydantic` method:
.. literalinclude:: ../../examples/providers/configuration/configuration_pydantic.py .. literalinclude:: ../../examples/providers/configuration/configuration_pydantic.py
:language: python :language: python
:lines: 3- :lines: 3-
:emphasize-lines: 31 :emphasize-lines: 32
To get the data from pydantic settings ``Configuration`` provider calls ``Settings.dict()`` method. To get the data from pydantic settings ``Configuration`` provider calls its ``model_dump()`` method.
If you need to pass an argument to this call, use ``.from_pydantic()`` keyword arguments. If you need to pass an argument to this call, use ``.from_pydantic()`` keyword arguments.
.. code-block:: python .. code-block:: python
container.config.from_pydantic(Settings(), exclude={"optional"}) container.config.from_pydantic(Settings(), exclude={"optional"})
Alternatively, you can provide a ``pydantic`` settings object over the configuration provider argument. In that case, Alternatively, you can provide a ``pydantic_settings.BaseSettings`` object over the configuration provider argument. In that case,
the container will call ``config.from_pydantic()`` automatically: the container will call ``config.from_pydantic()`` automatically:
.. code-block:: python .. code-block:: python
@ -215,18 +215,23 @@ the container will call ``config.from_pydantic()`` automatically:
.. note:: .. note::
``Dependency Injector`` doesn't install ``pydantic`` by default. ``Dependency Injector`` doesn't install ``pydantic-settings`` by default.
You can install the ``Dependency Injector`` with an extra dependency:: You can install the ``Dependency Injector`` with an extra dependency::
pip install dependency-injector[pydantic] pip install dependency-injector[pydantic2]
or install ``pydantic`` directly:: or install ``pydantic-settings`` directly::
pip install pydantic pip install pydantic-settings
*Don't forget to mirror the changes in the requirements file.* *Don't forget to mirror the changes in the requirements file.*
.. note::
For backward-compatibility, Pydantic v1 is still supported.
Passing ``pydantic.BaseSettings`` instances will work just as fine as ``pydantic_settings.BaseSettings``.
Loading from a dictionary Loading from a dictionary
------------------------- -------------------------

View File

@ -3,7 +3,7 @@
from unittest import mock from unittest import mock
import pytest import pytest
from httpx import AsyncClient from httpx import ASGITransport, AsyncClient
from .application import app, container from .application import app, container
from .services import Service from .services import Service
@ -11,7 +11,10 @@ from .services import Service
@pytest.fixture @pytest.fixture
def client(event_loop): def client(event_loop):
client = AsyncClient(app=app, base_url="http://test") client = AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
)
yield client yield client
event_loop.run_until_complete(client.aclose()) event_loop.run_until_complete(client.aclose())

View File

@ -1,14 +1,17 @@
from unittest import mock from unittest import mock
import pytest import pytest
from httpx import AsyncClient from httpx import ASGITransport, AsyncClient
from fastapi_di_example import app, container, Service from fastapi_di_example import app, container, Service
@pytest.fixture @pytest.fixture
async def client(event_loop): async def client(event_loop):
async with AsyncClient(app=app, base_url="http://test") as client: async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
yield client yield client

View File

@ -3,7 +3,7 @@
from unittest import mock from unittest import mock
import pytest import pytest
from httpx import AsyncClient from httpx import ASGITransport, AsyncClient
from giphynavigator.application import app from giphynavigator.application import app
from giphynavigator.giphy import GiphyClient from giphynavigator.giphy import GiphyClient
@ -11,7 +11,10 @@ from giphynavigator.giphy import GiphyClient
@pytest.fixture @pytest.fixture
async def client(): async def client():
async with AsyncClient(app=app, base_url="http://test") as client: async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
yield client yield client

View File

@ -3,7 +3,7 @@
import os import os
from dependency_injector import containers, providers from dependency_injector import containers, providers
from pydantic import BaseSettings, Field from pydantic_settings import BaseSettings, SettingsConfigDict
# Emulate environment variables # Emulate environment variables
os.environ["AWS_ACCESS_KEY_ID"] = "KEY" os.environ["AWS_ACCESS_KEY_ID"] = "KEY"
@ -11,15 +11,16 @@ os.environ["AWS_SECRET_ACCESS_KEY"] = "SECRET"
class AwsSettings(BaseSettings): class AwsSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="aws_")
access_key_id: str = Field(env="aws_access_key_id") access_key_id: str
secret_access_key: str = Field(env="aws_secret_access_key") secret_access_key: str
class Settings(BaseSettings): class Settings(BaseSettings):
aws: AwsSettings = AwsSettings() aws: AwsSettings = AwsSettings()
optional: str = Field(default="default_value") optional: str = "default_value"
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):

View File

@ -3,7 +3,7 @@
import os import os
from dependency_injector import containers, providers from dependency_injector import containers, providers
from pydantic import BaseSettings, Field from pydantic_settings import BaseSettings, SettingsConfigDict
# Emulate environment variables # Emulate environment variables
os.environ["AWS_ACCESS_KEY_ID"] = "KEY" os.environ["AWS_ACCESS_KEY_ID"] = "KEY"
@ -11,15 +11,16 @@ os.environ["AWS_SECRET_ACCESS_KEY"] = "SECRET"
class AwsSettings(BaseSettings): class AwsSettings(BaseSettings):
model_config = SettingsConfigDict(env_prefix="aws_")
access_key_id: str = Field(env="aws_access_key_id") access_key_id: str
secret_access_key: str = Field(env="aws_secret_access_key") secret_access_key: str
class Settings(BaseSettings): class Settings(BaseSettings):
aws: AwsSettings = AwsSettings() aws: AwsSettings = AwsSettings()
optional: str = Field(default="default_value") optional: str = "default_value"
class Container(containers.DeclarativeContainer): class Container(containers.DeclarativeContainer):

102
pyproject.toml Normal file
View File

@ -0,0 +1,102 @@
[build-system]
requires = ["setuptools", "Cython"]
build-backend = "setuptools.build_meta"
[project]
name = "dependency-injector"
authors = [
{name = "Roman Mogylatov", email = "rmogilatov@gmail.com"},
]
maintainers = [
{name = "Roman Mogylatov", email = "rmogilatov@gmail.com"},
]
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"
keywords = [
"Dependency injection",
"DI",
"Inversion of Control",
"IoC",
"Factory",
"Singleton",
"Design patterns",
"Flask",
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"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",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: AsyncIO",
"Framework :: Bottle",
"Framework :: Django",
"Framework :: Flask",
"Framework :: Pylons",
"Framework :: Pyramid",
"Framework :: Pytest",
"Framework :: TurboGears",
"Topic :: Software Development",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dynamic = ["version"]
dependencies = ["six"]
[project.optional-dependencies]
yaml = ["pyyaml"]
pydantic = ["pydantic"]
pydantic2 = ["pydantic-settings"]
flask = ["flask"]
aiohttp = ["aiohttp"]
[project.urls]
Homepage = "https://github.com/ets-labs/python-dependency-injector"
Documentation = "https://python-dependency-injector.ets-labs.org/"
Download = "https://pypi.python.org/pypi/dependency_injector"
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
dependency_injector = ["*.pxd", "*.pyi", "py.typed"]
[tool.setuptools.dynamic]
version = {attr = "dependency_injector.__version__"}
[tool.coverage.run]
branch = false
relative_files = true
source_pkgs = ["dependency_injector"]
plugins = ["Cython.Coverage"]
[tool.coverage.html]
directory = "reports/unittests/"
[tool.coverage.report]
show_missing = true
[tool.isort]
profile = "black"
[tool.pylint.main]
ignore = ["tests"]
[tool.pylint.design]
min-public-methods = 0
max-public-methods = 30

View File

@ -5,6 +5,7 @@ pytest-asyncio
tox tox
coverage coverage
flake8 flake8
flake8-pyproject
pydocstyle pydocstyle
sphinx_autobuild sphinx_autobuild
pip pip

152
setup.py
View File

@ -1,128 +1,42 @@
"""`Dependency injector` setup script.""" """`Dependency injector` setup script."""
import os import os
import re
import sys
from setuptools import setup, Extension from Cython.Build import cythonize
from Cython.Compiler import Options
from setuptools import Extension, setup
debug = os.environ.get("DEPENDENCY_INJECTOR_DEBUG_MODE") == "1"
def _open(filename): defined_macros = []
if sys.version_info[0] == 2: compiler_directives = {
return open(filename) "language_level": 3,
return open(filename, encoding="utf-8") "profile": debug,
"linetrace": debug,
}
# Defining setup variables: Options.annotate = debug
defined_macros = dict()
defined_macros["CYTHON_CLINE_IN_TRACEBACK"] = 0
# Getting description:
with _open("README.rst") as readme_file:
description = readme_file.read()
# Getting requirements:
with _open("requirements.txt") as requirements_file:
requirements = requirements_file.readlines()
# Getting version:
with _open("src/dependency_injector/__init__.py") as init_file:
version = re.search("__version__ = \"(.*?)\"", init_file.read()).group(1)
# Adding debug options: # Adding debug options:
if os.environ.get("DEPENDENCY_INJECTOR_DEBUG_MODE") == "1": if debug:
defined_macros["CYTHON_TRACE"] = 1 defined_macros.extend(
defined_macros["CYTHON_TRACE_NOGIL"] = 1 [
defined_macros["CYTHON_CLINE_IN_TRACEBACK"] = 1 ("CYTHON_TRACE", "1"),
("CYTHON_TRACE_NOGIL", "1"),
("CYTHON_CLINE_IN_TRACEBACK", "1"),
]
)
setup(name="dependency-injector", setup(
version=version, ext_modules=cythonize(
description="Dependency injection framework for Python", [
long_description=description, Extension(
author="Roman Mogylatov", "*",
author_email="rmogilatov@gmail.com", ["src/**/*.pyx"],
maintainer="Roman Mogylatov", define_macros=defined_macros,
maintainer_email="rmogilatov@gmail.com", ),
url="https://github.com/ets-labs/python-dependency-injector", ],
download_url="https://pypi.python.org/pypi/dependency_injector", annotate=debug,
packages=[ show_all_warnings=True,
"dependency_injector", compiler_directives=compiler_directives,
"dependency_injector.ext", ),
], )
package_dir={
"": "src",
},
package_data={
"dependency_injector": ["*.pxd", "*.pyi", "py.typed"],
},
ext_modules=[
Extension("dependency_injector.containers",
["src/dependency_injector/containers.c"],
define_macros=list(defined_macros.items()),
extra_compile_args=["-O2"]),
Extension("dependency_injector.providers",
["src/dependency_injector/providers.c"],
define_macros=list(defined_macros.items()),
extra_compile_args=["-O2"]),
Extension("dependency_injector._cwiring",
["src/dependency_injector/_cwiring.c"],
define_macros=list(defined_macros.items()),
extra_compile_args=["-O2"]),
],
install_requires=requirements,
extras_require={
"yaml": [
"pyyaml",
],
"pydantic": [
"pydantic",
],
"flask": [
"flask",
],
"aiohttp": [
"aiohttp",
],
},
zip_safe=True,
license="BSD New",
platforms=["any"],
keywords=[
"Dependency injection",
"DI",
"Inversion of Control",
"IoC",
"Factory",
"Singleton",
"Design patterns",
"Flask",
],
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"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",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Framework :: AsyncIO",
"Framework :: Bottle",
"Framework :: Django",
"Framework :: Flask",
"Framework :: Pylons",
"Framework :: Pyramid",
"Framework :: Pytest",
"Framework :: TurboGears",
"Topic :: Software Development",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Python Modules",
])

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -48,10 +48,25 @@ try:
except ImportError: except ImportError:
yaml = None yaml = None
has_pydantic_settings = True
cdef bint pydantic_v1 = False
cdef str pydantic_module = "pydantic_settings"
cdef str pydantic_extra = "pydantic2"
try: try:
import pydantic from pydantic_settings import BaseSettings as PydanticSettings
except ImportError: except ImportError:
pydantic = None 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"
except ImportError:
# if it is present, ofc
has_pydantic_settings = False
from .errors import ( from .errors import (
Error, Error,
@ -149,6 +164,31 @@ cdef int ASYNC_MODE_DISABLED = 2
cdef set __iscoroutine_typecache = set() cdef set __iscoroutine_typecache = set()
cdef tuple __COROUTINE_TYPES = asyncio.coroutines._COROUTINE_TYPES if asyncio else tuple() cdef tuple __COROUTINE_TYPES = asyncio.coroutines._COROUTINE_TYPES if asyncio else tuple()
cdef dict pydantic_settings_to_dict(settings, dict kwargs):
if not has_pydantic_settings:
raise Error(
f"Unable to load pydantic configuration - {pydantic_module} is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: "
f"\"pip install dependency-injector[{pydantic_extra}]\""
)
if isinstance(settings, CLASS_TYPES) and issubclass(settings, PydanticSettings):
raise Error(
"Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__)
)
if not isinstance(settings, PydanticSettings):
raise Error(
f"Unable to recognize settings instance, expect \"{pydantic_module}.BaseSettings\", "
f"got {settings} instead"
)
if pydantic_v1:
return settings.dict(**kwargs)
return settings.model_dump(mode="python", **kwargs)
cdef class Provider(object): cdef class Provider(object):
"""Base provider class. """Base provider class.
@ -1786,36 +1826,20 @@ cdef class ConfigurationOption(Provider):
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
:param settings: Pydantic settings instances. :param settings: Pydantic settings instances.
:type settings: :py:class:`pydantic.BaseSettings` :type settings: :py:class:`pydantic.BaseSettings` (pydantic v1) or
:py:class:`pydantic_settings.BaseSettings` (pydantic v2 and onwards)
:param required: When required is True, raise an exception if settings dict is empty. :param required: When required is True, raise an exception if settings dict is empty.
:type required: bool :type required: bool
:param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()`` call. :param kwargs: Keyword arguments forwarded to ``pydantic.BaseSettings.dict()`` or
``pydantic_settings.BaseSettings.model_dump()`` call (based on pydantic version).
:type kwargs: Dict[Any, Any] :type kwargs: Dict[Any, Any]
:rtype: None :rtype: None
""" """
if pydantic is None:
raise Error(
"Unable to load pydantic configuration - pydantic is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\""
)
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings): self.from_dict(pydantic_settings_to_dict(settings, kwargs), required=required)
raise Error(
"Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__)
)
if not isinstance(settings, pydantic.BaseSettings):
raise Error(
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"got {0} instead".format(settings)
)
self.from_dict(settings.dict(**kwargs), required=required)
def from_dict(self, options, required=UNDEFINED): def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary. """Load configuration from the dictionary.
@ -2355,7 +2379,8 @@ cdef class Configuration(Object):
Loaded configuration is merged recursively over existing configuration. Loaded configuration is merged recursively over existing configuration.
:param settings: Pydantic settings instances. :param settings: Pydantic settings instances.
:type settings: :py:class:`pydantic.BaseSettings` :type settings: :py:class:`pydantic.BaseSettings` (pydantic v1) or
:py:class:`pydantic_settings.BaseSettings` (pydantic v2 and onwards)
:param required: When required is True, raise an exception if settings dict is empty. :param required: When required is True, raise an exception if settings dict is empty.
:type required: bool :type required: bool
@ -2365,26 +2390,8 @@ cdef class Configuration(Object):
:rtype: None :rtype: None
""" """
if pydantic is None:
raise Error(
"Unable to load pydantic configuration - pydantic is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\""
)
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings): self.from_dict(pydantic_settings_to_dict(settings, kwargs), required=required)
raise Error(
"Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__)
)
if not isinstance(settings, pydantic.BaseSettings):
raise Error(
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"got {0} instead".format(settings)
)
self.from_dict(settings.dict(**kwargs), required=required)
def from_dict(self, options, required=UNDEFINED): def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary. """Load configuration from the dictionary.

View File

@ -2,6 +2,8 @@
testpaths = tests/unit/ testpaths = tests/unit/
python_files = test_*_py3*.py python_files = test_*_py3*.py
asyncio_mode = auto asyncio_mode = auto
markers =
pydantic: Tests with Pydantic as a dependency
filterwarnings = filterwarnings =
ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.aiohttp\" is deprecated since version 4\.0\.0:DeprecationWarning
ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning ignore:Module \"dependency_injector.ext.flask\" is deprecated since version 4\.0\.0:DeprecationWarning

View File

@ -1,41 +1,60 @@
"""Configuration.from_pydantic() tests.""" """Configuration.from_pydantic() tests."""
import pydantic from pydantic import BaseModel
from dependency_injector import providers, errors
try:
from pydantic_settings import (
BaseSettings, # type: ignore[import-not-found,unused-ignore]
)
except ImportError:
try:
from pydantic import BaseSettings # type: ignore[no-redef,unused-ignore]
except ImportError:
class BaseSettings: # type: ignore[no-redef]
"""No-op fallback"""
from pytest import fixture, mark, raises from pytest import fixture, mark, raises
from dependency_injector import errors, providers
class Section11(pydantic.BaseModel): pytestmark = mark.pydantic
value1 = 1
class Section12(pydantic.BaseModel): class Section11(BaseModel):
value2 = 2 value1: int = 1
class Settings1(pydantic.BaseSettings): class Section12(BaseModel):
section1 = Section11() value2: int = 2
section2 = Section12()
class Section21(pydantic.BaseModel): class Settings1(BaseSettings):
value1 = 11 section1: Section11 = Section11()
value11 = 11 section2: Section12 = Section12()
class Section3(pydantic.BaseModel): class Section21(BaseModel):
value3 = 3 value1: int = 11
value11: int = 11
class Settings2(pydantic.BaseSettings): class Section3(BaseModel):
section1 = Section21() value3: int = 3
section3 = Section3()
class Settings2(BaseSettings):
section1: Section21 = Section21()
section3: Section3 = Section3()
@fixture @fixture
def no_pydantic_module_installed(): def no_pydantic_module_installed():
providers.pydantic = None has_pydantic_settings = providers.has_pydantic_settings
providers.has_pydantic_settings = False
yield yield
providers.pydantic = pydantic providers.has_pydantic_settings = has_pydantic_settings
def test(config): def test(config):
@ -82,66 +101,70 @@ def test_merge(config):
def test_empty_settings(config): def test_empty_settings(config):
config.from_pydantic(pydantic.BaseSettings()) config.from_pydantic(BaseSettings())
assert config() == {} assert config() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_empty_settings_strict_mode(config): def test_empty_settings_strict_mode(config):
with raises(ValueError): with raises(ValueError):
config.from_pydantic(pydantic.BaseSettings()) config.from_pydantic(BaseSettings())
def test_option_empty_settings(config): def test_option_empty_settings(config):
config.option.from_pydantic(pydantic.BaseSettings()) config.option.from_pydantic(BaseSettings())
assert config.option() == {} assert config.option() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_option_empty_settings_strict_mode(config): def test_option_empty_settings_strict_mode(config):
with raises(ValueError): with raises(ValueError):
config.option.from_pydantic(pydantic.BaseSettings()) config.option.from_pydantic(BaseSettings())
def test_required_empty_settings(config): def test_required_empty_settings(config):
with raises(ValueError): with raises(ValueError):
config.from_pydantic(pydantic.BaseSettings(), required=True) config.from_pydantic(BaseSettings(), required=True)
def test_required_option_empty_settings(config): def test_required_option_empty_settings(config):
with raises(ValueError): with raises(ValueError):
config.option.from_pydantic(pydantic.BaseSettings(), required=True) config.option.from_pydantic(BaseSettings(), required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_not_required_empty_settings_strict_mode(config): def test_not_required_empty_settings_strict_mode(config):
config.from_pydantic(pydantic.BaseSettings(), required=False) config.from_pydantic(BaseSettings(), required=False)
assert config() == {} assert config() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_not_required_option_empty_settings_strict_mode(config): def test_not_required_option_empty_settings_strict_mode(config):
config.option.from_pydantic(pydantic.BaseSettings(), required=False) config.option.from_pydantic(BaseSettings(), required=False)
assert config.option() == {} assert config.option() == {}
assert config() == {"option": {}} assert config() == {"option": {}}
def test_not_instance_of_settings(config): def test_not_instance_of_settings(config):
with raises(errors.Error) as error: with raises(
errors.Error,
match=(
r"Unable to recognize settings instance, expect \"pydantic(?:_settings)?\.BaseSettings\", "
r"got {0} instead".format({})
),
):
config.from_pydantic({}) config.from_pydantic({})
assert error.value.args[0] == (
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"got {0} instead".format({})
)
def test_option_not_instance_of_settings(config): def test_option_not_instance_of_settings(config):
with raises(errors.Error) as error: with raises(
errors.Error,
match=(
r"Unable to recognize settings instance, expect \"pydantic(?:_settings)?\.BaseSettings\", "
"got {0} instead".format({})
),
):
config.option.from_pydantic({}) config.option.from_pydantic({})
assert error.value.args[0] == (
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"got {0} instead".format({})
)
def test_subclass_instead_of_instance(config): def test_subclass_instead_of_instance(config):
@ -164,21 +187,25 @@ def test_option_subclass_instead_of_instance(config):
@mark.usefixtures("no_pydantic_module_installed") @mark.usefixtures("no_pydantic_module_installed")
def test_no_pydantic_installed(config): def test_no_pydantic_installed(config):
with raises(errors.Error) as error: with raises(
errors.Error,
match=(
r"Unable to load pydantic configuration - pydantic(?:_settings)? is not installed\. "
r"Install pydantic or install Dependency Injector with pydantic extras: "
r"\"pip install dependency-injector\[pydantic2?\]\""
),
):
config.from_pydantic(Settings1()) config.from_pydantic(Settings1())
assert error.value.args[0] == (
"Unable to load pydantic configuration - pydantic is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\""
)
@mark.usefixtures("no_pydantic_module_installed") @mark.usefixtures("no_pydantic_module_installed")
def test_option_no_pydantic_installed(config): def test_option_no_pydantic_installed(config):
with raises(errors.Error) as error: with raises(
errors.Error,
match=(
r"Unable to load pydantic configuration - pydantic(?:_settings)? is not installed\. "
r"Install pydantic or install Dependency Injector with pydantic extras: "
r"\"pip install dependency-injector\[pydantic2?\]\""
),
):
config.option.from_pydantic(Settings1()) config.option.from_pydantic(Settings1())
assert error.value.args[0] == (
"Unable to load pydantic configuration - pydantic is not installed. "
"Install pydantic or install Dependency Injector with pydantic extras: "
"\"pip install dependency-injector[pydantic]\""
)

View File

@ -1,35 +1,52 @@
"""Configuration.from_pydantic() tests.""" """Configuration.from_pydantic() tests."""
import pydantic from pydantic import BaseModel
from dependency_injector import providers
try:
from pydantic_settings import (
BaseSettings, # type: ignore[import-not-found,unused-ignore]
)
except ImportError:
try:
from pydantic import BaseSettings # type: ignore[no-redef,unused-ignore]
except ImportError:
class BaseSettings: # type: ignore[no-redef]
"""No-op fallback"""
from pytest import fixture, mark, raises from pytest import fixture, mark, raises
from dependency_injector import providers
class Section11(pydantic.BaseModel): pytestmark = mark.pydantic
class Section11(BaseModel):
value1: int = 1 value1: int = 1
class Section12(pydantic.BaseModel): class Section12(BaseModel):
value2: int = 2 value2: int = 2
class Settings1(pydantic.BaseSettings): class Settings1(BaseSettings):
section1: Section11 = Section11() section1: Section11 = Section11()
section2: Section12 = Section12() section2: Section12 = Section12()
class Section21(pydantic.BaseModel): class Section21(BaseModel):
value1: int = 11 value1: int = 11
value11: int = 11 value11: int = 11
class Section3(pydantic.BaseModel): class Section3(BaseModel):
value3: int = 3 value3: int = 3
class Settings2(pydantic.BaseSettings): class Settings2(BaseSettings):
section1: Section21 = Section21() section1: Section21 = Section21()
section3: Section3= Section3() section3: Section3 = Section3()
@fixture @fixture
@ -86,10 +103,10 @@ def test_copy(config, pydantic_settings_1, pydantic_settings_2):
def test_set_pydantic_settings(config): def test_set_pydantic_settings(config):
class Settings3(pydantic.BaseSettings): class Settings3(BaseSettings):
... ...
class Settings4(pydantic.BaseSettings): class Settings4(BaseSettings):
... ...
settings_3 = Settings3() settings_3 = Settings3()
@ -100,27 +117,27 @@ def test_set_pydantic_settings(config):
def test_file_does_not_exist(config): def test_file_does_not_exist(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
config.load() config.load()
assert config() == {} assert config() == {}
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_file_does_not_exist_strict_mode(config): def test_file_does_not_exist_strict_mode(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
with raises(ValueError): with raises(ValueError):
config.load() config.load()
assert config() == {} assert config() == {}
def test_required_file_does_not_exist(config): def test_required_file_does_not_exist(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
with raises(ValueError): with raises(ValueError):
config.load(required=True) config.load(required=True)
@mark.parametrize("config_type", ["strict"]) @mark.parametrize("config_type", ["strict"])
def test_not_required_file_does_not_exist_strict_mode(config): def test_not_required_file_does_not_exist_strict_mode(config):
config.set_pydantic_settings([pydantic.BaseSettings()]) config.set_pydantic_settings([BaseSettings()])
config.load(required=False) config.load(required=False)
assert config() == {} assert config() == {}

View File

@ -1,4 +1,4 @@
from httpx import AsyncClient from httpx import ASGITransport, AsyncClient
from pytest import fixture, mark from pytest import fixture, mark
from pytest_asyncio import fixture as aio_fixture from pytest_asyncio import fixture as aio_fixture
@ -19,7 +19,7 @@ from wiringfastapi import web
@aio_fixture @aio_fixture
async def async_client(): async def async_client():
client = AsyncClient(app=web.app, base_url="http://test") client = AsyncClient(transport=ASGITransport(app=web.app), base_url="http://test")
yield client yield client
await client.aclose() await client.aclose()

34
tox.ini
View File

@ -1,7 +1,7 @@
[tox] [tox]
parallel_show_output = true parallel_show_output = true
envlist= envlist=
coveralls, pylint, flake8, pydocstyle, 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.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, pypy3.9, pypy3.10
[testenv] [testenv]
deps= deps=
@ -23,6 +23,32 @@ extras=
yaml yaml
commands = pytest -c tests/.configs/pytest.ini commands = pytest -c tests/.configs/pytest.ini
python_files = test_*_py3*.py python_files = test_*_py3*.py
setenv =
COVERAGE_RCFILE = pyproject.toml
[testenv:.pkg]
passenv = DEPENDENCY_INJECTOR_*
[testenv:pydantic-{v1,v2}]
description = run tests with different pydantic versions
base_python = python3.12
deps =
v1: pydantic<2
v2: pydantic-settings
pytest
pytest-asyncio
-rrequirements.txt
typing_extensions
httpx
fastapi
flask<2.2
aiohttp<=3.9.0b1
numpy
scipy
boto3
mypy_boto3_s3
werkzeug<=2.2.2
commands = pytest -c tests/.configs/pytest.ini -m pydantic
[testenv:coveralls] [testenv:coveralls]
passenv = GITHUB_*, COVERALLS_*, DEPENDENCY_INJECTOR_* passenv = GITHUB_*, COVERALLS_*, DEPENDENCY_INJECTOR_*
@ -34,8 +60,8 @@ deps=
coveralls>=4 coveralls>=4
commands= commands=
coverage erase coverage erase
coverage run --rcfile=./.coveragerc -m pytest -c tests/.configs/pytest.ini coverage run -m pytest -c tests/.configs/pytest.ini
coverage report --rcfile=./.coveragerc coverage report
coveralls coveralls
[testenv:pypy3.9] [testenv:pypy3.9]
@ -60,7 +86,7 @@ deps=
flask<2.2 flask<2.2
werkzeug<=2.2.2 werkzeug<=2.2.2
commands= commands=
- pylint -f colorized --rcfile=./.pylintrc src/dependency_injector - pylint -f colorized src/dependency_injector
[testenv:flake8] [testenv:flake8]
deps= deps=