From ce67660d2fa3003807718bdcb5459cfae97e6d82 Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+MrMrRobat@users.noreply.github.com> Date: Mon, 22 Feb 2021 20:10:04 +0300 Subject: [PATCH] Allow to configure models through class kwargs (#2356) * add support for class kwargs config * reformat tests * add changes file and docs * fix linting in 'inherit_config' * tweak docs Co-authored-by: Samuel Colvin --- changes/2356-MrMrRobat.md | 1 + docs/examples/model_config_class_kwargs.py | 11 ++++++++ docs/usage/model_config.md | 7 +++++- pydantic/main.py | 23 ++++++++++------- tests/test_main.py | 29 ++++++++++++++++++++++ 5 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 changes/2356-MrMrRobat.md create mode 100644 docs/examples/model_config_class_kwargs.py diff --git a/changes/2356-MrMrRobat.md b/changes/2356-MrMrRobat.md new file mode 100644 index 0000000000..d4adb3c2be --- /dev/null +++ b/changes/2356-MrMrRobat.md @@ -0,0 +1 @@ +Allow configuring models through class kwargs \ No newline at end of file diff --git a/docs/examples/model_config_class_kwargs.py b/docs/examples/model_config_class_kwargs.py new file mode 100644 index 0000000000..3a1841c57b --- /dev/null +++ b/docs/examples/model_config_class_kwargs.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel, ValidationError, Extra + + +class Model(BaseModel, extra=Extra.forbid): + a: str + + +try: + Model(a='spam', b='oh no') +except ValidationError as e: + print(e) diff --git a/docs/usage/model_config.md b/docs/usage/model_config.md index 91c8d3d649..c2c09fc930 100644 --- a/docs/usage/model_config.md +++ b/docs/usage/model_config.md @@ -89,8 +89,13 @@ not be included in the model schemas. **Note**: this means that attributes on th ``` _(This script is complete, it should run "as is")_ -Similarly, if using the `@dataclass` decorator: +Also, you can specify config options as model class kwargs: +```py +{!.tmp_examples/model_config_class_kwargs.py!} +``` +_(This script is complete, it should run "as is")_ +Similarly, if using the `@dataclass` decorator: ```py {!.tmp_examples/model_config_dataclass.py!} ``` diff --git a/pydantic/main.py b/pydantic/main.py index 0f833f16fa..fccdff6685 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -168,18 +168,18 @@ def prepare_field(cls, field: 'ModelField') -> None: pass -def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType') -> 'ConfigType': - namespace = {} +def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType', **namespace: Any) -> 'ConfigType': if not self_config: - base_classes = (parent_config,) + base_classes: Tuple['ConfigType', ...] = (parent_config,) elif self_config == parent_config: base_classes = (self_config,) else: - base_classes = self_config, parent_config # type: ignore - namespace['json_encoders'] = { - **getattr(parent_config, 'json_encoders', {}), - **getattr(self_config, 'json_encoders', {}), - } + base_classes = self_config, parent_config + + namespace['json_encoders'] = { + **getattr(parent_config, 'json_encoders', {}), + **getattr(self_config, 'json_encoders', {}), + } return type('Config', base_classes, namespace) @@ -251,7 +251,12 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901 private_attributes.update(base.__private_attributes__) class_vars.update(base.__class_vars__) - config = inherit_config(namespace.get('Config'), config) + config_kwargs = {key: kwargs.pop(key) for key in kwargs.keys() & BaseConfig.__dict__.keys()} + config_from_namespace = namespace.get('Config') + if config_kwargs and config_from_namespace: + raise TypeError('Specifying config in two places is ambiguous, use either Config attribute or class kwargs') + config = inherit_config(config_from_namespace, config, **config_kwargs) + validators = inherit_validators(extract_validators(namespace), validators) vg = ValidatorGroup(validators) diff --git a/tests/test_main.py b/tests/test_main.py index c6109f8427..2094734097 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1550,3 +1550,32 @@ class Item(BaseModel): assert id(image_1) == id(item.images[0]) assert id(image_2) == id(item.images[1]) + + +def test_class_kwargs_config(): + class Base(BaseModel, extra='forbid', alias_generator=str.upper): + a: int + + assert Base.__config__.extra is Extra.forbid + assert Base.__config__.alias_generator is str.upper + assert Base.__fields__['a'].alias == 'A' + + class Model(Base, extra='allow'): + b: int + + assert Model.__config__.extra is Extra.allow # overwritten as intended + assert Model.__config__.alias_generator is str.upper # inherited as intended + assert Model.__fields__['b'].alias == 'B' # alias_generator still works + + +def test_class_kwargs_config_and_attr_conflict(): + + with pytest.raises( + TypeError, match='Specifying config in two places is ambiguous, use either Config attribute or class kwargs' + ): + + class Model(BaseModel, extra='allow'): + b: int + + class Config: + extra = 'forbid'