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

Add a with_config decorator to comply with typing spec #8611

Merged
merged 4 commits into from Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
Viicos marked this conversation as resolved.
Show resolved Hide resolved

```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 @@ -909,4 +909,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'}