Make signature class only (#1466)

* Make signature class only

* Add changes file
This commit is contained in:
Arseny Boykov
2020-05-18 23:41:24 +03:00
committed by GitHub
parent c8906ce810
commit 0b9b308ca5
5 changed files with 60 additions and 2 deletions
+1
View File
@@ -0,0 +1 @@
Make `BaseModel.__signature__` class-only, so getting `__signature__` from model instance will raise `AttributeError`
+3 -1
View File
@@ -35,6 +35,7 @@ from .schema import model_schema
from .types import PyObject, StrBytes
from .typing import AnyCallable, AnyType, ForwardRef, is_classvar, resolve_annotations, update_field_forward_refs
from .utils import (
ClassAttribute,
GetterDict,
Representation,
ValueItems,
@@ -300,7 +301,8 @@ class ModelMetaclass(ABCMeta):
}
cls = super().__new__(mcs, name, bases, new_namespace, **kwargs)
cls.__signature__ = generate_model_signature(cls.__init__, fields, config)
# set __signature__ attr only for model class, but not for its instances
cls.__signature__ = ClassAttribute('__signature__', generate_model_signature(cls.__init__, fields, config))
return cls
+21
View File
@@ -45,6 +45,7 @@ __all__ = (
'GetterDict',
'ValueItems',
'version_info', # required here to match behaviour in v1.3
'ClassAttribute',
)
@@ -439,3 +440,23 @@ class ValueItems(Representation):
def __repr_args__(self) -> 'ReprArgs':
return [(None, self._items)]
class ClassAttribute:
"""
Hide class attribute from its instances
"""
__slots__ = (
'name',
'value',
)
def __init__(self, name: str, value: Any) -> None:
self.name = name
self.value = value
def __get__(self, instance: Any, owner: Type[Any]) -> None:
if instance is None:
return self.value
raise AttributeError(f'{self.name!r} attribute of {owner.__name__!r} is class-only')
+12
View File
@@ -126,3 +126,15 @@ def test_extra_allow_conflict_custom_signature():
extra = Extra.allow
assert _equals(str(signature(Model)), '(extra_data: int = 1, **foobar: Any) -> None')
def test_signature_is_class_only():
class Model(BaseModel):
foo: int = 123
def __call__(self, a: int) -> bool:
pass
assert _equals(str(signature(Model)), '(*, foo: int = 123) -> None')
assert _equals(str(signature(Model())), '(a: int) -> bool')
assert not hasattr(Model(), '__signature__')
+23 -1
View File
@@ -12,7 +12,15 @@ from pydantic.color import Color
from pydantic.dataclasses import dataclass
from pydantic.fields import Undefined
from pydantic.typing import display_as_type, is_new_type, new_type_supertype
from pydantic.utils import ValueItems, deep_update, get_model, import_string, lenient_issubclass, truncate
from pydantic.utils import (
ClassAttribute,
ValueItems,
deep_update,
get_model,
import_string,
lenient_issubclass,
truncate,
)
from pydantic.version import version_info
try:
@@ -298,3 +306,17 @@ def test_version_info():
def test_version_strict():
assert str(StrictVersion(VERSION)) == VERSION
def test_class_attribute():
class Foo:
attr = ClassAttribute('attr', 'foo')
assert Foo.attr == 'foo'
with pytest.raises(AttributeError, match="'attr' attribute of 'Foo' is class-only"):
Foo().attr
f = Foo()
f.attr = 'not foo'
assert f.attr == 'not foo'