Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pydantic 2 support #786

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,7 @@ src/dependency_injector/providers/*.so

# Workspace for samples
.workspace/


# pyenv
.python-version
14 changes: 7 additions & 7 deletions docs/providers/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,22 +183,22 @@ See also: :ref:`configuration-envs-interpolation`.
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`` Settings object using the
:py:meth:`Configuration.from_pydantic` method:

.. literalinclude:: ../../examples/providers/configuration/configuration_pydantic.py
:language: python
:lines: 3-
:emphasize-lines: 31

To get the data from pydantic settings ``Configuration`` provider calls ``Settings.dict()`` method.
To get the data from pydantic settings ``Configuration`` provider calls ``Settings.model_dump()`` method.
If you need to pass an argument to this call, use ``.from_pydantic()`` keyword arguments.

.. code-block:: python

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`` Settings object over the configuration provider argument. In that case,
the container will call ``config.from_pydantic()`` automatically:

.. code-block:: python
Expand All @@ -215,15 +215,15 @@ the container will call ``config.from_pydantic()`` automatically:

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

pip install dependency-injector[pydantic]
pip install dependency-injector[pydantic-settings]

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.*

Expand Down
9 changes: 6 additions & 3 deletions examples/providers/configuration/configuration_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import os

from typing import Annotated

from dependency_injector import containers, providers
from pydantic import BaseSettings, Field
from pydantic import Field
from pydantic_settings import BaseSettings

# Emulate environment variables
os.environ["AWS_ACCESS_KEY_ID"] = "KEY"
Expand All @@ -12,8 +15,8 @@

class AwsSettings(BaseSettings):

access_key_id: str = Field(env="aws_access_key_id")
secret_access_key: str = Field(env="aws_secret_access_key")
access_key_id: str = Field(alias="aws_access_key_id")
secret_access_key: str = Field(alias="aws_secret_access_key")


class Settings(BaseSettings):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import os

from dependency_injector import containers, providers
from pydantic import BaseSettings, Field
from pydantic import Field
from pydantic_settings import BaseSettings

# Emulate environment variables
os.environ["AWS_ACCESS_KEY_ID"] = "KEY"
Expand All @@ -12,8 +13,8 @@

class AwsSettings(BaseSettings):

access_key_id: str = Field(env="aws_access_key_id")
secret_access_key: str = Field(env="aws_secret_access_key")
access_key_id: str = Field(alias="aws_access_key_id")
secret_access_key: str = Field(alias="aws_secret_access_key")


class Settings(BaseSettings):
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ mypy
pyyaml
httpx
fastapi
pydantic
pydantic-settings
numpy
scipy
boto3
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ def _open(filename):
"yaml": [
"pyyaml",
],
"pydantic": [
"pydantic",
"pydantic-settings": [
"pydantic-settings",
],
"flask": [
"flask",
Expand Down
52 changes: 36 additions & 16 deletions src/dependency_injector/providers.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ try:
except ImportError:
yaml = None


try:
import pydantic_settings
except ImportError:
pydantic_settings = None


try:
import pydantic
except ImportError:
Expand All @@ -61,6 +68,17 @@ from .errors import (
cimport cython


if pydantic_settings:
pydantic_settings_pkg = pydantic_settings
using_pydantic_2 = True
elif pydantic and pydantic.version.VERSION.startswith("1"):
pydantic_settings_pkg = pydantic.settings
using_pydantic_2 = False
else:
pydantic_settings_pkg = None
using_pydantic_2 = None


if sys.version_info[0] == 3: # pragma: no cover
CLASS_TYPES = (type,)
else: # pragma: no cover
Expand Down Expand Up @@ -1796,26 +1814,27 @@ cdef class ConfigurationOption(Provider):

:rtype: None
"""
if pydantic is None:
if pydantic_settings_pkg 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]\""
"Unable to load pydantic-settings configuration - pydantic-settings is not installed. "
"Install pydantic-settings or install Dependency Injector with pydantic-settings extras: "
"\"pip install dependency-injector[pydantic-settings]\""
)

if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings):
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic_settings_pkg.BaseSettings):
raise Error(
"Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__)
)

if not isinstance(settings, pydantic.BaseSettings):
if not isinstance(settings, pydantic_settings_pkg.BaseSettings):
raise Error(
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"Unable to recognize settings instance, expect \"pydantic_settings.BaseSettings\", "
"got {0} instead".format(settings)
)

self.from_dict(settings.dict(**kwargs), required=required)
settings_dict = settings.model_dump(**kwargs) if using_pydantic_2 else settings.dict(**kwargs)
self.from_dict(settings_dict, required=required)

def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary.
Expand Down Expand Up @@ -2365,26 +2384,27 @@ cdef class Configuration(Object):

:rtype: None
"""
if pydantic is None:
if pydantic_settings_pkg 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]\""
"Unable to load pydantic-settings configuration - pydantic-settings is not installed. "
"Install pydantic-settings or install Dependency Injector with pydantic-settings extras: "
"\"pip install dependency-injector[pydantic-settings]\""
)

if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic.BaseSettings):
if isinstance(settings, CLASS_TYPES) and issubclass(settings, pydantic_settings_pkg.BaseSettings):
raise Error(
"Got settings class, but expect instance: "
"instead \"{0}\" use \"{0}()\"".format(settings.__name__)
)

if not isinstance(settings, pydantic.BaseSettings):
if not isinstance(settings, pydantic_settings_pkg.BaseSettings):
raise Error(
"Unable to recognize settings instance, expect \"pydantic.BaseSettings\", "
"Unable to recognize settings instance, expect \"pydantic_settings.BaseSettings\", "
"got {0} instead".format(settings)
)

self.from_dict(settings.dict(**kwargs), required=required)
settings_dict = settings.model_dump(**kwargs) if using_pydantic_2 else settings.dict(**kwargs)
self.from_dict(settings_dict, required=required)

def from_dict(self, options, required=UNDEFINED):
"""Load configuration from the dictionary.
Expand Down
2 changes: 1 addition & 1 deletion tests/typing/configuration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pathlib import Path

from dependency_injector import providers
from pydantic import BaseSettings as PydanticSettings
from pydantic_settings import BaseSettings as PydanticSettings


# Test 1: to check the getattr
Expand Down
5 changes: 3 additions & 2 deletions tests/unit/providers/configuration/test_from_pydantic_py36.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Configuration.from_pydantic() tests."""

import pydantic
import pydantic_settings
from dependency_injector import providers, errors
from pytest import fixture, mark, raises

Expand All @@ -13,7 +14,7 @@ class Section12(pydantic.BaseModel):
value2 = 2


class Settings1(pydantic.BaseSettings):
class Settings1(pydantic_settings.BaseSettings):
section1 = Section11()
section2 = Section12()

Expand All @@ -27,7 +28,7 @@ class Section3(pydantic.BaseModel):
value3 = 3


class Settings2(pydantic.BaseSettings):
class Settings2(pydantic_settings.BaseSettings):
section1 = Section21()
section3 = Section3()

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ deps=
mypy_boto3_s3
extras=
yaml
pydantic
pydantic-settings
flask
aiohttp
commands = pytest -c tests/.configs/pytest.ini
Expand Down