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:
ZipFile 2024-12-07 18:38:08 +02:00 committed by GitHub
parent cab75cb9c7
commit c61fc16b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 234 additions and 132 deletions

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

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):

View File

@ -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"]

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()

23
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=
@ -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