Skip to content

Commit

Permalink
feat: support Config.extra
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Dec 13, 2021
1 parent 6914c04 commit 30b58f3
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 10 deletions.
5 changes: 4 additions & 1 deletion changes/2557-PrettyWood.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Refactor the whole _pydantic_ `dataclass` decorator to really act like its standard lib equivalent.
It hence keeps `__eq__`, `__hash__`, ... and makes comparison with its non-validated version possible.
It also fixes usage of `frozen` dataclasses in fields and usage of `default_factory` in nested dataclasses.
The support of `Config.extra` has been added.
Finally, config customization directly via a `dict` is now possible.
<br/><br/>
**BREAKING CHANGE** The `compiled` boolean (whether _pydantic_ is compiled with cython) has been moved from `main.py` to `version.py`
**BREAKING CHANGES**
- The `compiled` boolean (whether _pydantic_ is compiled with cython) has been moved from `main.py` to `version.py`
- Now that `Config.extra` is supported, `dataclass` ignores by default extra arguments (like `BaseModel`)
16 changes: 10 additions & 6 deletions pydantic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,18 +144,22 @@ def prepare_field(cls, field: 'ModelField') -> None:


def get_config(config: Union[ConfigDict, Type[BaseConfig], None]) -> Type[BaseConfig]:
if isinstance(config, dict):
if config is None:
return BaseConfig

else:
config_dict = (
config
if isinstance(config, dict)
else {k: getattr(config, k) for k in dir(config) if not k.startswith('__')}
)

class Config(BaseConfig):
...

for k, v in config.items():
for k, v in config_dict.items():
setattr(Config, k, v)
return Config
elif config is None:
return BaseConfig
else:
return config


def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType', **namespace: Any) -> 'ConfigType':
Expand Down
23 changes: 21 additions & 2 deletions pydantic/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class M:
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, Optional, Type, TypeVar, Union, overload

from .class_validators import gather_all_validators
from .config import BaseConfig, ConfigDict, get_config
from .config import BaseConfig, ConfigDict, Extra, get_config
from .error_wrappers import ValidationError
from .errors import DataclassTypeError
from .fields import Field, FieldInfo, Required, Undefined
Expand Down Expand Up @@ -222,9 +222,28 @@ def new_post_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None:

@wraps(init)
def new_init(self: 'Dataclass', *args: Any, **kwargs: Any) -> None:
init(self, *args, **kwargs)
if config.extra == Extra.ignore: # default behaviour

def ignore_extra_init(self: 'Dataclass', *a: Any, **kw: Any) -> None:
init(self, *a, **{k: v for k, v in kw.items() if k in self.__dataclass_fields__})

ignore_extra_init(self, *args, **kwargs)

elif config.extra == Extra.allow:

def allow_extra_init(self: 'Dataclass', *a: Any, **kw: Any) -> None:
self.__dict__ = kw
init(self, *a, **{k: v for k, v in kw.items() if k in self.__dataclass_fields__})

allow_extra_init(self, *args, **kwargs)

else: # Extra.forbid

init(self, *args, **kwargs)

if self.__class__.__pydantic_run_validation__:
self.__pydantic_validate_values__()

if hasattr(self, '__post_init_post_parse__'):
# We need to find again the initvars. To do that we use `__dataclass_fields__` instead of
# public method `dataclasses.fields`
Expand Down
56 changes: 55 additions & 1 deletion tests/test_dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import dataclasses
import pickle
import re
from collections.abc import Hashable
from datetime import datetime
from pathlib import Path
Expand All @@ -8,7 +9,7 @@
import pytest

import pydantic
from pydantic import BaseModel, ValidationError, validator
from pydantic import BaseModel, Extra, ValidationError, validator


def test_simple():
Expand Down Expand Up @@ -1228,3 +1229,56 @@ def __new__(cls, *args, **kwargs):
instance = cls(a=test_string)
assert instance._special_property == 1
assert instance.a == test_string


def test_ignore_extra():
@pydantic.dataclasses.dataclass(config=dict(extra=Extra.ignore))
class Foo:
x: int

foo = Foo(**{'x': '1', 'y': '2'})
assert foo.__dict__ == {'x': 1, '__pydantic_initialised__': True}


def test_ignore_extra_subclass():
@pydantic.dataclasses.dataclass(config=dict(extra=Extra.ignore))
class Foo:
x: int

@pydantic.dataclasses.dataclass(config=dict(extra=Extra.ignore))
class Bar(Foo):
y: int

bar = Bar(**{'x': '1', 'y': '2', 'z': '3'})
assert bar.__dict__ == {'x': 1, 'y': 2, '__pydantic_initialised__': True}


def test_allow_extra():
@pydantic.dataclasses.dataclass(config=dict(extra=Extra.allow))
class Foo:
x: int

foo = Foo(**{'x': '1', 'y': '2'})
assert foo.__dict__ == {'x': 1, 'y': '2', '__pydantic_initialised__': True}


def test_allow_extra_subclass():
@pydantic.dataclasses.dataclass(config=dict(extra=Extra.allow))
class Foo:
x: int

@pydantic.dataclasses.dataclass(config=dict(extra=Extra.allow))
class Bar(Foo):
y: int

bar = Bar(**{'x': '1', 'y': '2', 'z': '3'})
assert bar.__dict__ == {'x': 1, 'y': 2, 'z': '3', '__pydantic_initialised__': True}


def test_forbid_extra():
@pydantic.dataclasses.dataclass(config=dict(extra=Extra.forbid))
class Foo:
x: int

with pytest.raises(TypeError, match=re.escape("__init__() got an unexpected keyword argument 'y'")):
Foo(**{'x': '1', 'y': '2'})

0 comments on commit 30b58f3

Please sign in to comment.