Skip to content

Commit

Permalink
PropagateClassVars to sub-models (#2179)
Browse files Browse the repository at this point in the history
* Propagate`ClassVar`s to sub-models

Currently, if a `ClassVar` is defined on a model and re-defined
on a sub-model omitting the `ClassVar` annotation, Pydantic produces an
unrelated error:

    NameError: Field name "..." shadows a BaseModel attribute ...

This check was introduced to prevent shadowing Pydantic's own methods
and attributes defined on the `BaseModel` class.  Following this change,
class variables (that is, variables annotated with `ClassVar`)
defined on parent models will be inherited by sub-models and
will be overwritable without having to reapply the annotation.

Closes #2061.

* docs: explain how attributes are excluded and when to use `PrivateAttr`
  • Loading branch information
layday committed Feb 13, 2021
1 parent 61bdba3 commit 78934db
Show file tree
Hide file tree
Showing 4 changed files with 19 additions and 4 deletions.
1 change: 1 addition & 0 deletions changes/2061-layday.md
@@ -0,0 +1 @@
allow overwriting `ClassVar`s in sub-models without having to re-annotate them
8 changes: 7 additions & 1 deletion docs/usage/models.md
Expand Up @@ -543,9 +543,15 @@ Where `Field` refers to the [field function](schema.md#field-customisation).
Moreover if you want to validate default values with `validate_all`,
*pydantic* will need to call the `default_factory`, which could lead to side effects!

## Automatically excluded attributes

Class variables which begin with an underscore and attributes annotated with `typing.ClassVar` will be
automatically excluded from the model.

## Private model attributes

If you need to use internal attributes excluded from model fields, you can declare them using `PrivateAttr`:
If you need to vary or manipulate internal attributes on instances of the model, you can declare them
using `PrivateAttr`:

```py
{!.tmp_examples/private_attributes.py!}
Expand Down
8 changes: 5 additions & 3 deletions pydantic/main.py
Expand Up @@ -16,7 +16,6 @@
List,
Mapping,
Optional,
Set,
Tuple,
Type,
TypeVar,
Expand Down Expand Up @@ -232,8 +231,9 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901

pre_root_validators, post_root_validators = [], []
private_attributes: Dict[str, ModelPrivateAttr] = {}
slots: Set[str] = namespace.get('__slots__', ())
slots: SetStr = namespace.get('__slots__', ())
slots = {slots} if isinstance(slots, str) else set(slots)
class_vars: SetStr = set()

for base in reversed(bases):
if _is_base_model_class_defined and issubclass(base, BaseModel) and base != BaseModel:
Expand All @@ -243,6 +243,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
pre_root_validators += base.__pre_root_validators__
post_root_validators += base.__post_root_validators__
private_attributes.update(base.__private_attributes__)
class_vars.update(base.__class_vars__)

config = inherit_config(namespace.get('Config'), config)
validators = inherit_validators(extract_validators(namespace), validators)
Expand All @@ -258,7 +259,6 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901

prepare_config(config, name)

class_vars = set()
if (namespace.get('__module__'), namespace.get('__qualname__')) != ('pydantic.main', 'BaseModel'):
annotations = resolve_annotations(namespace.get('__annotations__', {}), namespace.get('__module__', None))
# annotation only fields need to come first in fields
Expand Down Expand Up @@ -337,6 +337,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
'__custom_root_type__': _custom_root_type,
'__private_attributes__': private_attributes,
'__slots__': slots | private_attributes.keys(),
'__class_vars__': class_vars,
**{n: v for n, v in namespace.items() if n not in exclude_from_namespace},
}

Expand All @@ -363,6 +364,7 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
__custom_root_type__: bool = False
__signature__: 'Signature'
__private_attributes__: Dict[str, Any]
__class_vars__: SetStr
__fields_set__: SetStr = set()

Config = BaseConfig
Expand Down
6 changes: 6 additions & 0 deletions tests/test_main.py
Expand Up @@ -908,6 +908,12 @@ class MyModel(BaseModel):

assert list(MyModel.__fields__.keys()) == ['c']

class MyOtherModel(MyModel):
a = ''
b = 2

assert list(MyOtherModel.__fields__.keys()) == ['c']


def test_fields_set():
class MyModel(BaseModel):
Expand Down

0 comments on commit 78934db

Please sign in to comment.