From 8aad3a2f58b234f07b2df0f00db10865e33b7100 Mon Sep 17 00:00:00 2001 From: Alex Hedges Date: Wed, 28 Oct 2020 06:17:45 -0400 Subject: [PATCH] Refactor PrivateAttr to type-check like Field (#2057) * Refactor PrivateAttr to type-check like Field * Change TypeError to ValueError for consistency * Add PrivateAttr example to mypy tests --- changes/2048-aphedges.md | 1 + pydantic/fields.py | 38 ++++++++++++++++++++++---------- pydantic/main.py | 6 ++--- tests/mypy/modules/success.py | 6 ++++- tests/test_private_attributes.py | 2 +- 5 files changed, 36 insertions(+), 17 deletions(-) create mode 100644 changes/2048-aphedges.md diff --git a/changes/2048-aphedges.md b/changes/2048-aphedges.md new file mode 100644 index 0000000..0407f23 --- /dev/null +++ b/changes/2048-aphedges.md @@ -0,0 +1 @@ +Fix mypy assignment error when using `PrivateAttr` diff --git a/pydantic/fields.py b/pydantic/fields.py index c451770..36ea690 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -799,21 +799,10 @@ class ModelField(Representation): return args -class PrivateAttr(Representation): - """ - Indicates that attribute is only used internally and never mixed with regular fields. - - Types or values of private attrs are not checked by pydantic and it's up to you to keep them relevant. - - Private attrs are stored in model __slots__. - """ - +class ModelPrivateAttr(Representation): __slots__ = ('default', 'default_factory') def __init__(self, default: Any = Undefined, *, default_factory: Optional[NoArgAnyCallable] = None) -> None: - if default is not Undefined and default_factory is not None: - raise TypeError('default and default_factory args can not be used together') - self.default = default self.default_factory = default_factory @@ -825,3 +814,28 @@ class PrivateAttr(Representation): other.default, other.default_factory, ) + + +def PrivateAttr( + default: Any = Undefined, + *, + default_factory: Optional[NoArgAnyCallable] = None, +) -> Any: + """ + Indicates that attribute is only used internally and never mixed with regular fields. + + Types or values of private attrs are not checked by pydantic and it's up to you to keep them relevant. + + Private attrs are stored in model __slots__. + + :param default: the attribute’s default value + :param default_factory: callable that will be called when a default value is needed for this attribute + If both `default` and `default_factory` are set, an error is raised. + """ + if default is not Undefined and default_factory is not None: + raise ValueError('cannot specify both default and default_factory') + + return ModelPrivateAttr( + default, + default_factory=default_factory, + ) diff --git a/pydantic/main.py b/pydantic/main.py index bc5444a..bc0b5be 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -29,7 +29,7 @@ from typing import ( from .class_validators import ValidatorGroup, extract_root_validators, extract_validators, inherit_validators from .error_wrappers import ErrorWrapper, ValidationError from .errors import ConfigError, DictError, ExtraError, MissingError -from .fields import SHAPE_MAPPING, ModelField, PrivateAttr, Undefined +from .fields import SHAPE_MAPPING, ModelField, ModelPrivateAttr, PrivateAttr, Undefined from .json import custom_pydantic_encoder, pydantic_encoder from .parse import Protocol, load_file, load_str_bytes from .schema import default_ref_template, model_schema @@ -215,7 +215,7 @@ class ModelMetaclass(ABCMeta): validators: 'ValidatorListDict' = {} pre_root_validators, post_root_validators = [], [] - private_attributes: Dict[str, PrivateAttr] = {} + private_attributes: Dict[str, ModelPrivateAttr] = {} slots: Set[str] = namespace.get('__slots__', ()) slots = {slots} if isinstance(slots, str) else set(slots) @@ -271,7 +271,7 @@ class ModelMetaclass(ABCMeta): for var_name, value in namespace.items(): can_be_changed = var_name not in class_vars and not isinstance(value, untouched_types) - if isinstance(value, PrivateAttr): + if isinstance(value, ModelPrivateAttr): if not is_valid_private_name(var_name): raise NameError( f'Private attributes "{var_name}" must not be a valid field name; ' diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py index 202184c..3d48a33 100644 --- a/tests/mypy/modules/success.py +++ b/tests/mypy/modules/success.py @@ -9,7 +9,7 @@ from datetime import date, datetime from typing import Any, Dict, Generic, List, Optional, TypeVar from pydantic import BaseModel, NoneStr, PyObject, StrictBool, root_validator, validate_arguments, validator -from pydantic.fields import Field +from pydantic.fields import Field, PrivateAttr from pydantic.generics import GenericModel from pydantic.typing import ForwardRef @@ -145,3 +145,7 @@ class MyConf(BaseModel): conf = MyConf() var1: date = conf.str_pyobject(2020, 12, 20) var2: date = conf.callable_pyobject(2111, 1, 1) + + +class MyPrivateAttr(BaseModel): + _private_field: str = PrivateAttr() diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py index 07f702c..9621452 100644 --- a/tests/test_private_attributes.py +++ b/tests/test_private_attributes.py @@ -158,5 +158,5 @@ def test_slots_are_ignored(): def test_default_and_default_factory_used_error(): - with pytest.raises(TypeError, match='default and default_factory args can not be used together'): + with pytest.raises(ValueError, match='cannot specify both default and default_factory'): PrivateAttr(default=123, default_factory=lambda: 321)