Skip to content

Commit e1f9db4

Browse files
authored
Merge pull request #3 from DanCardin/dc/pydantic-v1
fix: Support pydantic v1.
2 parents dcc7806 + b4cb8e7 commit e1f9db4

File tree

6 files changed

+66
-26
lines changed

6 files changed

+66
-26
lines changed

.github/workflows/test.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
fail-fast: false
2020
matrix:
2121
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
22+
pydantic-version: ["1.0", "2.0"]
2223

2324
steps:
2425
- uses: actions/checkout@v3
@@ -43,7 +44,9 @@ jobs:
4344
${{ runner.os }}-poetry-
4445
4546
- name: Install dependencies
46-
run: make install
47+
run: |
48+
make install
49+
pip install 'pydantic~=${{ matrix.pydantic-version }}'
4750
4851
- name: Run Linters
4952
run: poetry run make lint

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
[PEP-681](https://peps.python.org/pep-0681/)-compliant dataclass-like object,
1212
including but not limited to:
1313

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

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "dataclass-settings"
3-
version = "0.2.3"
3+
version = "0.3.0"
44
description = "Declarative dataclass settings."
55

66
repository = "https://github.com/dancardin/dataclass-settings"

src/dataclass_settings/class_inspect.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import Any, Callable, Type
66

77
import typing_inspect
8-
from typing_extensions import Self, get_args, get_origin
8+
from typing_extensions import Annotated, Self, get_args, get_origin, get_type_hints
99

1010
from dataclass_settings.loaders import Loader
1111

@@ -25,6 +25,7 @@ def detect(cls: type) -> bool:
2525
class ClassTypes(Enum):
2626
dataclass = "dataclass"
2727
pydantic = "pydantic"
28+
pydantic_v1 = "pydantic_v1"
2829
pydantic_dataclass = "pydantic_dataclass"
2930
attrs = "attrs"
3031

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

3940
try:
40-
from pydantic import BaseModel
41+
import pydantic
4142
except ImportError: # pragma: no cover
4243
pass
4344
else:
4445
try:
45-
is_base_model = issubclass(obj, BaseModel)
46+
is_base_model = isinstance(obj, type) and issubclass(
47+
obj, pydantic.BaseModel
48+
)
4649
except TypeError:
4750
is_base_model = False
4851

4952
if is_base_model:
53+
if pydantic.__version__.startswith("1."):
54+
return cls.pydantic_v1
5055
return cls.pydantic
5156

5257
if hasattr(obj, "__attrs_attrs__"):
@@ -69,11 +74,17 @@ class Field:
6974
def from_dataclass(cls, typ: Type) -> list[Self]:
7075
fields = []
7176
for f in typ.__dataclass_fields__.values():
77+
type_ = get_origin(f.type) or f.type
78+
args = get_args(f.type) or ()
79+
if type_ is Annotated:
80+
type_, *_args = args
81+
args = tuple(_args)
82+
7283
field = cls(
7384
name=f.name,
74-
type=get_origin(f.type) or f.type,
75-
annotations=get_args(f.type) or (),
76-
mapper=f.type,
85+
type=type_,
86+
annotations=args,
87+
mapper=type_,
7788
)
7889
fields.append(field)
7990
return fields
@@ -94,6 +105,23 @@ def from_pydantic(cls, typ: Type) -> list[Self]:
94105
fields.append(field)
95106
return fields
96107

108+
@classmethod
109+
def from_pydantic_v1(cls, typ: Type) -> list[Self]:
110+
fields = []
111+
type_hints = get_type_hints(typ, include_extras=True)
112+
for name, f in typ.__fields__.items():
113+
annotation = get_type(type_hints[name])
114+
mapper = annotation if detect(annotation) else None
115+
116+
field = cls(
117+
name=name,
118+
type=f.annotation,
119+
annotations=get_args(annotation) or (),
120+
mapper=mapper,
121+
)
122+
fields.append(field)
123+
return fields
124+
97125
@classmethod
98126
def from_pydantic_dataclass(cls, typ: Type) -> list[Self]:
99127
fields = []
@@ -167,6 +195,9 @@ def fields(cls: type):
167195
if class_type == ClassTypes.pydantic:
168196
return Field.from_pydantic(cls)
169197

198+
if class_type == ClassTypes.pydantic_v1:
199+
return Field.from_pydantic_v1(cls)
200+
170201
if class_type == ClassTypes.pydantic_dataclass:
171202
return Field.from_pydantic_dataclass(cls)
172203

tests/class_types/test_pydantic.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from decimal import Decimal
24

35
import pytest
@@ -8,12 +10,13 @@
810
from tests.utils import env_setup
911

1012

11-
def test_missing_required():
12-
class Config(BaseModel):
13-
foo: Annotated[str, Env("FOO")]
13+
class MissingRequiredConfig(BaseModel):
14+
foo: Annotated[str, Env("FOO")]
1415

16+
17+
def test_missing_required():
1518
with env_setup({}), pytest.raises(ValidationError):
16-
load_settings(Config)
19+
load_settings(MissingRequiredConfig)
1720

1821

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

2932

30-
def test_nested():
31-
class Sub(BaseModel):
32-
foo: Annotated[str, Env("FOO")]
33+
class NestedSub(BaseModel):
34+
foo: Annotated[str, Env("FOO")]
3335

34-
class Config(BaseModel):
35-
sub: Sub
3636

37+
class NestedConfig(BaseModel):
38+
sub: NestedSub
39+
40+
41+
def test_nested():
3742
with env_setup({"FOO": "3"}):
38-
config = load_settings(Config)
43+
config = load_settings(NestedConfig)
3944

40-
assert config == Config(sub=Sub(foo="3"))
45+
assert config == NestedConfig(sub=NestedSub(foo="3"))
4146

4247

4348
def test_map_int():

tests/class_types/test_pydantic_dataclass.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
from tests.utils import env_setup
1010

1111

12-
def test_missing_required():
13-
@dataclass
14-
class Config:
15-
foo: Annotated[str, Env("FOO")]
12+
@dataclass
13+
class MissingRequiredConfig:
14+
foo: Annotated[str, Env("FOO")]
15+
1616

17-
with env_setup({}), pytest.raises(ValidationError):
18-
load_settings(Config)
17+
def test_missing_required():
18+
with env_setup({}), pytest.raises((ValidationError, TypeError)):
19+
load_settings(MissingRequiredConfig)
1920

2021

2122
def test_has_required_required():

0 commit comments

Comments
 (0)