mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-03-09 22:38:03 +03:00
Yet another Pydantic 2 support (#832)
* Add support for Pydantic v2 settings * Configure pipeline to run tests against different pydantic versions * Update Pydantic docs and examples for v2 * Fix compatibility with httpx v0.27.0
This commit is contained in:
parent
cab75cb9c7
commit
c61fc16b8d
11
.github/workflows/tests-and-linters.yml
vendored
11
.github/workflows/tests-and-linters.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -58,6 +58,7 @@ dependencies = ["six"]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
yaml = ["pyyaml"]
|
yaml = ["pyyaml"]
|
||||||
pydantic = ["pydantic"]
|
pydantic = ["pydantic"]
|
||||||
|
pydantic2 = ["pydantic-settings"]
|
||||||
flask = ["flask"]
|
flask = ["flask"]
|
||||||
aiohttp = ["aiohttp"]
|
aiohttp = ["aiohttp"]
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]\""
|
|
||||||
)
|
|
||||||
|
|
|
@ -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() == {}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
23
tox.ini
23
tox.ini
|
@ -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=
|
||||||
|
@ -29,6 +29,27 @@ setenv =
|
||||||
[testenv:.pkg]
|
[testenv:.pkg]
|
||||||
passenv = DEPENDENCY_INJECTOR_*
|
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_*
|
||||||
basepython=python3.12 # TODO: Upgrade to version 3.13 is blocked by coveralls 4.0.1 not supporting Python 3.13
|
basepython=python3.12 # TODO: Upgrade to version 3.13 is blocked by coveralls 4.0.1 not supporting Python 3.13
|
||||||
|
|
Loading…
Reference in New Issue
Block a user