Files
pydantic/tests/test_private_attributes.py
T
Samuel Colvin 594effa279 Switching to pydantic_core (#4516)
* working on core schema generation

* adapting main.py

* getting tests to run

* fix tests

* disable pyright, fix mypy

* moving to class-based model generation

* working on validators

* change how models are created

* start fixing test_main.py

* fixing mypy

* SelfType

* recursive models working, more tests fixed

* fix tests on <3.10

* get docs build to pass

* starting to cleanup types.py

* starting works on custom types

* working on using annotated-types

* using annoated types for constraints

* lots of cleanup, fixing network tests

* network tests passing 🎉

* working on types

* working on types and cleanup

* fixing UUID type, restructing again

* more types and newer pydantic-core

* working on Iterable

* more test_types tests

* support newer pydantic-core, fixing more test_types.py

* working through more test_types.py

* test_types.py at last passing locally 🎉

* fixing more tests in test_types.py

* fix datetime_parse tests and linting

* get tests running again, rename to test_datetime.py

* renaming internal modules

* working through mypy errors

* fixing mypy

* refactoring _generate_schema.py

* test_main.py passing

* uprev deps

* fix conftest and linting?

* importing Annotated

* ltining

* import Annotated from typing_extensions

* fixing 3.7 compatibility

* fixing tests on 3.9

* fix linting

* fixing SecretField and 3.9 tests

* customising get_type_hints

* ignore warnings on 3.11

* spliting repr out of utils

* removing unused bits of _repr, fix tests for 3.7

* more cleanup, removing many type aliases

* clean up repr

* support namedtuples and typeddicts

* test is_union

* removing errors, uprev pydantic-core

* fix tests on 3.8

* fixing private attributes and model_post_init

* renaming and cleanup

* remove unnecessary PydanticMetadata inheritance

* fixing forward refs and mypy tests

* fix signatures, change how xfail works

* revert mypy tests to 3.7 syntax

* correct model title

* try to fix tests

* fixing ClassVar forward refs

* uprev pydantic-core, new error format

* add "force" argument to model_rebuild

* Apply suggestions from code review

Suggestions from @tiangolo and @hramezani 🙏

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>

* more suggestions from @tiangolo

* extra -> json_schema_extra on Field

Co-authored-by: Hasan Ramezani <hasan.r67@gmail.com>
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2022-11-02 12:01:17 +00:00

270 lines
6.8 KiB
Python

from typing import ClassVar, Generic, TypeVar
import pytest
from pydantic import BaseModel, Extra, PrivateAttr
from pydantic.fields import Undefined
from pydantic.generics import GenericModel
def test_private_attribute():
default = {'a': {}}
class Model(BaseModel):
_foo = PrivateAttr(default)
assert Model.__slots__ == {'_foo'}
assert repr(Model._foo) == "<member '_foo' of 'Model' objects>"
assert Model.__private_attributes__ == {'_foo': PrivateAttr(default)}
m = Model()
assert m._foo == default
assert m._foo is not default
assert m._foo['a'] is not default['a']
m._foo = None
assert m._foo is None
assert m.dict() == {}
assert m.__dict__ == {}
def test_private_attribute_nested():
class SubModel(BaseModel):
_foo = PrivateAttr(42)
x: int
class Model(BaseModel):
y: int
sub: SubModel
m = Model(y=1, sub={'x': 2})
assert m.sub._foo == 42
def test_private_attribute_factory():
default = {'a': {}}
def factory():
return default
class Model(BaseModel):
_foo = PrivateAttr(default_factory=factory)
assert Model.__slots__ == {'_foo'}
assert repr(Model._foo) == "<member '_foo' of 'Model' objects>"
assert Model.__private_attributes__ == {'_foo': PrivateAttr(default_factory=factory)}
m = Model()
assert m._foo == default
assert m._foo is default
assert m._foo['a'] is default['a']
m._foo = None
assert m._foo is None
assert m.dict() == {}
assert m.__dict__ == {}
def test_private_attribute_annotation():
class Model(BaseModel):
"""The best model"""
_foo: str
class Config:
underscore_attrs_are_private = True
assert Model.__slots__ == {'_foo'}
assert repr(Model._foo) == "<member '_foo' of 'Model' objects>"
assert Model.__private_attributes__ == {'_foo': PrivateAttr(Undefined)}
assert repr(Model.__doc__) == "'The best model'"
m = Model()
with pytest.raises(AttributeError):
m._foo
m._foo = '123'
assert m._foo == '123'
m._foo = None
assert m._foo is None
del m._foo
with pytest.raises(AttributeError):
m._foo
m._foo = '123'
assert m._foo == '123'
assert m.dict() == {}
assert m.__dict__ == {}
@pytest.mark.xfail(reason='ClassVar needs work')
def test_underscore_attrs_are_private():
class Model(BaseModel):
_foo: str = 'abc'
_bar: ClassVar[str] = 'cba'
class Config:
underscore_attrs_are_private = True
assert Model.__slots__ == {'_foo'}
assert repr(Model._foo) == "<member '_foo' of 'Model' objects>"
assert Model._bar == 'cba'
assert Model.__private_attributes__ == {'_foo': PrivateAttr('abc')}
m = Model()
assert m._foo == 'abc'
m._foo = None
assert m._foo is None
with pytest.raises(ValueError, match='"Model" object has no field "_bar"'):
m._bar = 1
def test_private_attribute_intersection_with_extra_field():
class Model(BaseModel):
_foo = PrivateAttr('private_attribute')
class Config:
extra = Extra.allow
assert Model.__slots__ == {'_foo'}
m = Model(_foo='field')
assert m._foo == 'private_attribute'
assert m.__dict__ == m.dict() == {'_foo': 'field'}
m._foo = 'still_private'
assert m._foo == 'still_private'
assert m.__dict__ == m.dict() == {'_foo': 'field'}
def test_private_attribute_invalid_name():
with pytest.raises(
NameError,
match='Private attributes "foo" must not be a valid field name; Use sunder names, e. g. "_foo"',
):
class Model(BaseModel):
foo = PrivateAttr()
@pytest.mark.xfail(reason="not sure what's going on here???")
def test_slots_are_ignored():
class Model(BaseModel):
__slots__ = (
'foo',
'_bar',
)
def __init__(self):
super().__init__()
for attr_ in self.__slots__:
object.__setattr__(self, attr_, 'spam')
assert Model.__private_attributes__ == {}
assert set(Model.__slots__) == {'foo', '_bar'}
m = Model()
for attr in Model.__slots__:
assert object.__getattribute__(m, attr) == 'spam'
with pytest.raises(ValueError, match=f'"Model" object has no field "{attr}"'):
setattr(m, attr, 'not spam')
def test_default_and_default_factory_used_error():
with pytest.raises(ValueError, match='cannot specify both default and default_factory'):
PrivateAttr(default=123, default_factory=lambda: 321)
def test_config_override_init():
class MyModel(BaseModel):
x: str
_private_attr: int
def __init__(self, **data) -> None:
super().__init__(**data)
self._private_attr = 123
class Config:
underscore_attrs_are_private = True
m = MyModel(x='hello')
assert m.dict() == {'x': 'hello'}
assert m._private_attr == 123
@pytest.mark.xfail(reason='generics not implemented')
def test_generic_private_attribute():
T = TypeVar('T')
class Model(GenericModel, Generic[T]):
value: T
_private_value: T
class Config:
underscore_attrs_are_private = True
m = Model[int](value=1, _private_attr=3)
m._private_value = 3
assert m.dict() == {'value': 1}
@pytest.mark.xfail(reason='config inheritance error')
def test_private_attribute_multiple_inheritance():
# We need to test this since PrivateAttr uses __slots__ and that has some restrictions with regards to
# multiple inheritance
default = {'a': {}}
class GrandParentModel(BaseModel):
_foo = PrivateAttr(default)
class ParentAModel(GrandParentModel):
pass
class ParentBModel(GrandParentModel):
_bar = PrivateAttr(default)
class Model(ParentAModel, ParentBModel):
_baz = PrivateAttr(default)
assert GrandParentModel.__slots__ == {'_foo'}
assert ParentBModel.__slots__ == {'_bar'}
assert Model.__slots__ == {'_baz'}
assert repr(Model._foo) == "<member '_foo' of 'GrandParentModel' objects>"
assert repr(Model._bar) == "<member '_bar' of 'ParentBModel' objects>"
assert repr(Model._baz) == "<member '_baz' of 'Model' objects>"
assert Model.__private_attributes__ == {
'_foo': PrivateAttr(default),
'_bar': PrivateAttr(default),
'_baz': PrivateAttr(default),
}
m = Model()
assert m._foo == default
assert m._foo is not default
assert m._foo['a'] is not default['a']
assert m._bar == default
assert m._bar is not default
assert m._bar['a'] is not default['a']
assert m._baz == default
assert m._baz is not default
assert m._baz['a'] is not default['a']
m._foo = None
assert m._foo is None
m._bar = None
assert m._bar is None
m._baz = None
assert m._baz is None
assert m.dict() == {}
assert m.__dict__ == {}