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

Allow to configure models through class kwargs #2356

Merged
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 changes/2356-MrMrRobat.md
@@ -0,0 +1 @@
Allow configuring models through class kwargs
11 changes: 11 additions & 0 deletions 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)
7 changes: 6 additions & 1 deletion docs/usage/model_config.md
Expand Up @@ -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!}
```
Expand Down
23 changes: 14 additions & 9 deletions pydantic/main.py
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
29 changes: 29 additions & 0 deletions tests/test_main.py
Expand Up @@ -1551,3 +1551,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'