Skip to content

Commit 3810c7b

Browse files
authored
Add a with_config decorator to comply with typing spec (pydantic#8611)
1 parent 2792775 commit 3810c7b

File tree

10 files changed

+132
-17
lines changed

10 files changed

+132
-17
lines changed

docs/api/config.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
group_by_category: false
44
members:
55
- ConfigDict
6+
- with_config
67
- ExtraValues
78
- BaseConfig
89

docs/concepts/config.md

+26-14
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ from pydantic.dataclasses import dataclass
5757
config = ConfigDict(str_max_length=10, validate_assignment=True)
5858

5959

60-
@dataclass(config=config) # (1)!
60+
@dataclass(config=config)
6161
class User:
6262
id: int
6363
name: str = 'John Doe'
@@ -76,26 +76,38 @@ except ValidationError as e:
7676
"""
7777
```
7878

79+
## Configuration with `dataclass` from the standard library or `TypedDict`
7980

80-
1. If using the `dataclass` from the standard library or `TypedDict`, you should use `__pydantic_config__` instead.
81-
See:
81+
If using the `dataclass` from the standard library or `TypedDict`, you should use `__pydantic_config__` instead.
8282

83-
```py
84-
from dataclasses import dataclass
85-
from datetime import datetime
83+
```py
84+
from dataclasses import dataclass
85+
from datetime import datetime
86+
87+
from pydantic import ConfigDict
88+
89+
90+
@dataclass
91+
class User:
92+
__pydantic_config__ = ConfigDict(strict=True)
93+
94+
id: int
95+
name: str = 'John Doe'
96+
signup_ts: datetime = None
97+
```
8698

87-
from pydantic import ConfigDict
99+
Alternatively, the [`with_config`][pydantic.config.with_config] decorator can be used to comply with type checkers.
88100

101+
```py
102+
from typing_extensions import TypedDict
89103

90-
@dataclass
91-
class User:
92-
__pydantic_config__ = ConfigDict(strict=True)
104+
from pydantic import ConfigDict, with_config
93105

94-
id: int
95-
name: str = 'John Doe'
96-
signup_ts: datetime = None
97-
```
98106

107+
@with_config(ConfigDict(str_to_lower=True))
108+
class Model(TypedDict):
109+
x: str
110+
```
99111

100112
## Change behaviour globally
101113

pydantic/__init__.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ._internal._generate_schema import GenerateSchema as GenerateSchema
2020
from .aliases import AliasChoices, AliasGenerator, AliasPath
2121
from .annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler
22-
from .config import ConfigDict
22+
from .config import ConfigDict, with_config
2323
from .errors import *
2424
from .fields import Field, PrivateAttr, computed_field
2525
from .functional_serializers import (
@@ -80,6 +80,7 @@
8080
'WrapSerializer',
8181
# config
8282
'ConfigDict',
83+
'with_config',
8384
# deprecated V1 config, these are imported via `__getattr__` below
8485
'BaseConfig',
8586
'Extra',
@@ -234,6 +235,7 @@
234235
'WrapSerializer': (__package__, '.functional_serializers'),
235236
# config
236237
'ConfigDict': (__package__, '.config'),
238+
'with_config': (__package__, '.config'),
237239
# validate call
238240
'validate_call': (__package__, '.validate_call_decorator'),
239241
# errors

pydantic/config.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Configuration for Pydantic models."""
22
from __future__ import annotations as _annotations
33

4-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Type, Union
4+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Type, TypeVar, Union
55

66
from typing_extensions import Literal, TypeAlias, TypedDict
77

@@ -11,7 +11,7 @@
1111
if TYPE_CHECKING:
1212
from ._internal._generate_schema import GenerateSchema as _GenerateSchema
1313

14-
__all__ = ('ConfigDict',)
14+
__all__ = ('ConfigDict', 'with_config')
1515

1616

1717
JsonValue: TypeAlias = Union[int, float, str, bool, None, List['JsonValue'], 'JsonDict']
@@ -945,4 +945,40 @@ class Model(BaseModel):
945945
'''
946946

947947

948+
_TypeT = TypeVar('_TypeT', bound=type)
949+
950+
951+
def with_config(config: ConfigDict) -> Callable[[_TypeT], _TypeT]:
952+
"""Usage docs: https://docs.pydantic.dev/2.6/concepts/config/#configuration-with-dataclass-from-the-standard-library-or-typeddict
953+
954+
A convenience decorator to set a [Pydantic configuration](config.md) on a `TypedDict` or a `dataclass` from the standard library.
955+
956+
Although the configuration can be set using the `__pydantic_config__` attribute, it does not play well with type checkers,
957+
especially with `TypedDict`.
958+
959+
!!! example "Usage"
960+
961+
```py
962+
from typing_extensions import TypedDict
963+
964+
from pydantic import ConfigDict, TypeAdapter, with_config
965+
966+
@with_config(ConfigDict(str_to_lower=True))
967+
class Model(TypedDict):
968+
x: str
969+
970+
ta = TypeAdapter(Model)
971+
972+
print(ta.validate_python({'x': 'ABC'}))
973+
#> {'x': 'abc'}
974+
```
975+
"""
976+
977+
def inner(TypedDictClass: _TypeT, /) -> _TypeT:
978+
TypedDictClass.__pydantic_config__ = config
979+
return TypedDictClass
980+
981+
return inner
982+
983+
948984
__getattr__ = getattr_migration(__name__)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import TypedDict
2+
3+
from pydantic import ConfigDict, with_config
4+
5+
6+
@with_config(ConfigDict(str_to_lower=True))
7+
class Model(TypedDict):
8+
a: str
9+
10+
11+
model = Model(a='ABC')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import TypedDict
2+
3+
from pydantic import ConfigDict, with_config
4+
5+
6+
@with_config(ConfigDict(str_to_lower=True))
7+
class Model(TypedDict):
8+
a: str
9+
10+
11+
model = Model(a='ABC')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from typing import TypedDict
2+
3+
from pydantic import ConfigDict, with_config
4+
5+
6+
@with_config(ConfigDict(str_to_lower=True))
7+
class Model(TypedDict):
8+
a: str
9+
10+
11+
model = Model(a='ABC')

tests/mypy/test_mypy.py

+1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def build(self) -> List[Union[Tuple[str, str], Any]]:
110110
('mypy-plugin-strict-no-any.ini', 'dataclass_no_any.py'),
111111
('mypy-plugin-very-strict.ini', 'metaclass_args.py'),
112112
('pyproject-default.toml', 'computed_fields.py'),
113+
('pyproject-default.toml', 'with_config_decorator.py'),
113114
]
114115
)
115116

tests/test_dataclasses.py

+19
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
field_serializer,
3333
field_validator,
3434
model_validator,
35+
with_config,
3536
)
3637
from pydantic._internal._mock_val_ser import MockValSer
3738
from pydantic.dataclasses import is_pydantic_dataclass, rebuild_dataclass
@@ -2406,6 +2407,24 @@ class Model:
24062407
assert ta.validate_python({'x': 'ABC '}).x == 'ABC '
24072408

24082409

2410+
def test_dataclasses_with_config_decorator():
2411+
@dataclasses.dataclass
2412+
@with_config(ConfigDict(str_to_lower=True))
2413+
class Model1:
2414+
x: str
2415+
2416+
ta = TypeAdapter(Model1)
2417+
assert ta.validate_python({'x': 'ABC'}).x == 'abc'
2418+
2419+
@with_config(ConfigDict(str_to_lower=True))
2420+
@dataclasses.dataclass
2421+
class Model2:
2422+
x: str
2423+
2424+
ta = TypeAdapter(Model2)
2425+
assert ta.validate_python({'x': 'ABC'}).x == 'abc'
2426+
2427+
24092428
def test_pydantic_field_annotation():
24102429
@pydantic.dataclasses.dataclass
24112430
class Model:

tests/test_types_typeddict.py

+11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
PositiveInt,
2121
PydanticUserError,
2222
ValidationError,
23+
with_config,
2324
)
2425
from pydantic._internal._decorators import get_attribute_from_bases
2526
from pydantic.functional_serializers import field_serializer, model_serializer
@@ -919,3 +920,13 @@ class C(B):
919920
pass
920921

921922
assert get_attribute_from_bases(C, 'x') == 2
923+
924+
925+
def test_typeddict_with_config_decorator():
926+
@with_config(ConfigDict(str_to_lower=True))
927+
class Model(TypedDict):
928+
x: str
929+
930+
ta = TypeAdapter(Model)
931+
932+
assert ta.validate_python({'x': 'ABC'}) == {'x': 'abc'}

0 commit comments

Comments
 (0)