Skip to content

Commit

Permalink
Merge pull request #3 from DanCardin/dc/pydantic-v1
Browse files Browse the repository at this point in the history
fix: Support pydantic v1.
  • Loading branch information
DanCardin authored Apr 24, 2024
2 parents dcc7806 + b4cb8e7 commit e1f9db4
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 26 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
pydantic-version: ["1.0", "2.0"]

steps:
- uses: actions/checkout@v3
Expand All @@ -43,7 +44,9 @@ jobs:
${{ runner.os }}-poetry-
- name: Install dependencies
run: make install
run: |
make install
pip install 'pydantic~=${{ matrix.pydantic-version }}'
- name: Run Linters
run: poetry run make lint
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
[PEP-681](https://peps.python.org/pep-0681/)-compliant dataclass-like object,
including but not limited to:

- [Pydantic models](https://pydantic-docs.helpmanual.io/) (v2+),
- [Pydantic models](https://pydantic-docs.helpmanual.io/) (v1/v2),
- [dataclasses](https://docs.python.org/3/library/dataclasses.html)
- [attrs classes](https://www.attrs.org/en/stable/).

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "dataclass-settings"
version = "0.2.3"
version = "0.3.0"
description = "Declarative dataclass settings."

repository = "https://github.com/dancardin/dataclass-settings"
Expand Down
43 changes: 37 additions & 6 deletions src/dataclass_settings/class_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any, Callable, Type

import typing_inspect
from typing_extensions import Self, get_args, get_origin
from typing_extensions import Annotated, Self, get_args, get_origin, get_type_hints

from dataclass_settings.loaders import Loader

Expand All @@ -25,6 +25,7 @@ def detect(cls: type) -> bool:
class ClassTypes(Enum):
dataclass = "dataclass"
pydantic = "pydantic"
pydantic_v1 = "pydantic_v1"
pydantic_dataclass = "pydantic_dataclass"
attrs = "attrs"

Expand All @@ -37,16 +38,20 @@ def from_cls(cls, obj: type) -> ClassTypes:
return cls.dataclass

try:
from pydantic import BaseModel
import pydantic
except ImportError: # pragma: no cover
pass
else:
try:
is_base_model = issubclass(obj, BaseModel)
is_base_model = isinstance(obj, type) and issubclass(
obj, pydantic.BaseModel
)
except TypeError:
is_base_model = False

if is_base_model:
if pydantic.__version__.startswith("1."):
return cls.pydantic_v1
return cls.pydantic

if hasattr(obj, "__attrs_attrs__"):
Expand All @@ -69,11 +74,17 @@ class Field:
def from_dataclass(cls, typ: Type) -> list[Self]:
fields = []
for f in typ.__dataclass_fields__.values():
type_ = get_origin(f.type) or f.type
args = get_args(f.type) or ()
if type_ is Annotated:
type_, *_args = args
args = tuple(_args)

field = cls(
name=f.name,
type=get_origin(f.type) or f.type,
annotations=get_args(f.type) or (),
mapper=f.type,
type=type_,
annotations=args,
mapper=type_,
)
fields.append(field)
return fields
Expand All @@ -94,6 +105,23 @@ def from_pydantic(cls, typ: Type) -> list[Self]:
fields.append(field)
return fields

@classmethod
def from_pydantic_v1(cls, typ: Type) -> list[Self]:
fields = []
type_hints = get_type_hints(typ, include_extras=True)
for name, f in typ.__fields__.items():
annotation = get_type(type_hints[name])
mapper = annotation if detect(annotation) else None

field = cls(
name=name,
type=f.annotation,
annotations=get_args(annotation) or (),
mapper=mapper,
)
fields.append(field)
return fields

@classmethod
def from_pydantic_dataclass(cls, typ: Type) -> list[Self]:
fields = []
Expand Down Expand Up @@ -167,6 +195,9 @@ def fields(cls: type):
if class_type == ClassTypes.pydantic:
return Field.from_pydantic(cls)

if class_type == ClassTypes.pydantic_v1:
return Field.from_pydantic_v1(cls)

if class_type == ClassTypes.pydantic_dataclass:
return Field.from_pydantic_dataclass(cls)

Expand Down
27 changes: 16 additions & 11 deletions tests/class_types/test_pydantic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from decimal import Decimal

import pytest
Expand All @@ -8,12 +10,13 @@
from tests.utils import env_setup


def test_missing_required():
class Config(BaseModel):
foo: Annotated[str, Env("FOO")]
class MissingRequiredConfig(BaseModel):
foo: Annotated[str, Env("FOO")]


def test_missing_required():
with env_setup({}), pytest.raises(ValidationError):
load_settings(Config)
load_settings(MissingRequiredConfig)


def test_has_required_required():
Expand All @@ -27,17 +30,19 @@ class Config(BaseModel):
assert config == Config(foo="1", ignoreme="asdf")


def test_nested():
class Sub(BaseModel):
foo: Annotated[str, Env("FOO")]
class NestedSub(BaseModel):
foo: Annotated[str, Env("FOO")]

class Config(BaseModel):
sub: Sub

class NestedConfig(BaseModel):
sub: NestedSub


def test_nested():
with env_setup({"FOO": "3"}):
config = load_settings(Config)
config = load_settings(NestedConfig)

assert config == Config(sub=Sub(foo="3"))
assert config == NestedConfig(sub=NestedSub(foo="3"))


def test_map_int():
Expand Down
13 changes: 7 additions & 6 deletions tests/class_types/test_pydantic_dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from tests.utils import env_setup


def test_missing_required():
@dataclass
class Config:
foo: Annotated[str, Env("FOO")]
@dataclass
class MissingRequiredConfig:
foo: Annotated[str, Env("FOO")]


with env_setup({}), pytest.raises(ValidationError):
load_settings(Config)
def test_missing_required():
with env_setup({}), pytest.raises((ValidationError, TypeError)):
load_settings(MissingRequiredConfig)


def test_has_required_required():
Expand Down

0 comments on commit e1f9db4

Please sign in to comment.