Skip to content

Commit

Permalink
Add a with_config decorator to comply with typing spec (#8611)
Browse files Browse the repository at this point in the history
  • Loading branch information
Viicos committed Jan 26, 2024
1 parent 2792775 commit 3810c7b
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 17 deletions.
1 change: 1 addition & 0 deletions docs/api/config.md
Expand Up @@ -3,6 +3,7 @@
group_by_category: false
members:
- ConfigDict
- with_config
- ExtraValues
- BaseConfig

Expand Down
40 changes: 26 additions & 14 deletions docs/concepts/config.md
Expand Up @@ -57,7 +57,7 @@ from pydantic.dataclasses import dataclass
config = ConfigDict(str_max_length=10, validate_assignment=True)


@dataclass(config=config) # (1)!
@dataclass(config=config)
class User:
id: int
name: str = 'John Doe'
Expand All @@ -76,26 +76,38 @@ except ValidationError as e:
"""
```

## Configuration with `dataclass` from the standard library or `TypedDict`

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

```py
from dataclasses import dataclass
from datetime import datetime
```py
from dataclasses import dataclass
from datetime import datetime

from pydantic import ConfigDict


@dataclass
class User:
__pydantic_config__ = ConfigDict(strict=True)

id: int
name: str = 'John Doe'
signup_ts: datetime = None
```

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

```py
from typing_extensions import TypedDict

@dataclass
class User:
__pydantic_config__ = ConfigDict(strict=True)
from pydantic import ConfigDict, with_config

id: int
name: str = 'John Doe'
signup_ts: datetime = None
```

@with_config(ConfigDict(str_to_lower=True))
class Model(TypedDict):
x: str
```

## Change behaviour globally

Expand Down
4 changes: 3 additions & 1 deletion pydantic/__init__.py
Expand Up @@ -19,7 +19,7 @@
from ._internal._generate_schema import GenerateSchema as GenerateSchema
from .aliases import AliasChoices, AliasGenerator, AliasPath
from .annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler
from .config import ConfigDict
from .config import ConfigDict, with_config
from .errors import *
from .fields import Field, PrivateAttr, computed_field
from .functional_serializers import (
Expand Down Expand Up @@ -80,6 +80,7 @@
'WrapSerializer',
# config
'ConfigDict',
'with_config',
# deprecated V1 config, these are imported via `__getattr__` below
'BaseConfig',
'Extra',
Expand Down Expand Up @@ -234,6 +235,7 @@
'WrapSerializer': (__package__, '.functional_serializers'),
# config
'ConfigDict': (__package__, '.config'),
'with_config': (__package__, '.config'),
# validate call
'validate_call': (__package__, '.validate_call_decorator'),
# errors
Expand Down
40 changes: 38 additions & 2 deletions pydantic/config.py
@@ -1,7 +1,7 @@
"""Configuration for Pydantic models."""
from __future__ import annotations as _annotations

from typing import TYPE_CHECKING, Any, Callable, Dict, List, Type, Union
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Type, TypeVar, Union

from typing_extensions import Literal, TypeAlias, TypedDict

Expand All @@ -11,7 +11,7 @@
if TYPE_CHECKING:
from ._internal._generate_schema import GenerateSchema as _GenerateSchema

__all__ = ('ConfigDict',)
__all__ = ('ConfigDict', 'with_config')


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


_TypeT = TypeVar('_TypeT', bound=type)


def with_config(config: ConfigDict) -> Callable[[_TypeT], _TypeT]:
"""Usage docs: https://docs.pydantic.dev/2.6/concepts/config/#configuration-with-dataclass-from-the-standard-library-or-typeddict
A convenience decorator to set a [Pydantic configuration](config.md) on a `TypedDict` or a `dataclass` from the standard library.
Although the configuration can be set using the `__pydantic_config__` attribute, it does not play well with type checkers,
especially with `TypedDict`.
!!! example "Usage"
```py
from typing_extensions import TypedDict
from pydantic import ConfigDict, TypeAdapter, with_config
@with_config(ConfigDict(str_to_lower=True))
class Model(TypedDict):
x: str
ta = TypeAdapter(Model)
print(ta.validate_python({'x': 'ABC'}))
#> {'x': 'abc'}
```
"""

def inner(TypedDictClass: _TypeT, /) -> _TypeT:
TypedDictClass.__pydantic_config__ = config
return TypedDictClass

return inner


__getattr__ = getattr_migration(__name__)
11 changes: 11 additions & 0 deletions tests/mypy/modules/with_config_decorator.py
@@ -0,0 +1,11 @@
from typing import TypedDict

from pydantic import ConfigDict, with_config


@with_config(ConfigDict(str_to_lower=True))
class Model(TypedDict):
a: str


model = Model(a='ABC')
@@ -0,0 +1,11 @@
from typing import TypedDict

from pydantic import ConfigDict, with_config


@with_config(ConfigDict(str_to_lower=True))
class Model(TypedDict):
a: str


model = Model(a='ABC')
@@ -0,0 +1,11 @@
from typing import TypedDict

from pydantic import ConfigDict, with_config


@with_config(ConfigDict(str_to_lower=True))
class Model(TypedDict):
a: str


model = Model(a='ABC')
1 change: 1 addition & 0 deletions tests/mypy/test_mypy.py
Expand Up @@ -110,6 +110,7 @@ def build(self) -> List[Union[Tuple[str, str], Any]]:
('mypy-plugin-strict-no-any.ini', 'dataclass_no_any.py'),
('mypy-plugin-very-strict.ini', 'metaclass_args.py'),
('pyproject-default.toml', 'computed_fields.py'),
('pyproject-default.toml', 'with_config_decorator.py'),
]
)

Expand Down
19 changes: 19 additions & 0 deletions tests/test_dataclasses.py
Expand Up @@ -32,6 +32,7 @@
field_serializer,
field_validator,
model_validator,
with_config,
)
from pydantic._internal._mock_val_ser import MockValSer
from pydantic.dataclasses import is_pydantic_dataclass, rebuild_dataclass
Expand Down Expand Up @@ -2406,6 +2407,24 @@ class Model:
assert ta.validate_python({'x': 'ABC '}).x == 'ABC '


def test_dataclasses_with_config_decorator():
@dataclasses.dataclass
@with_config(ConfigDict(str_to_lower=True))
class Model1:
x: str

ta = TypeAdapter(Model1)
assert ta.validate_python({'x': 'ABC'}).x == 'abc'

@with_config(ConfigDict(str_to_lower=True))
@dataclasses.dataclass
class Model2:
x: str

ta = TypeAdapter(Model2)
assert ta.validate_python({'x': 'ABC'}).x == 'abc'


def test_pydantic_field_annotation():
@pydantic.dataclasses.dataclass
class Model:
Expand Down
11 changes: 11 additions & 0 deletions tests/test_types_typeddict.py
Expand Up @@ -20,6 +20,7 @@
PositiveInt,
PydanticUserError,
ValidationError,
with_config,
)
from pydantic._internal._decorators import get_attribute_from_bases
from pydantic.functional_serializers import field_serializer, model_serializer
Expand Down Expand Up @@ -919,3 +920,13 @@ class C(B):
pass

assert get_attribute_from_bases(C, 'x') == 2


def test_typeddict_with_config_decorator():
@with_config(ConfigDict(str_to_lower=True))
class Model(TypedDict):
x: str

ta = TypeAdapter(Model)

assert ta.validate_python({'x': 'ABC'}) == {'x': 'abc'}

0 comments on commit 3810c7b

Please sign in to comment.