From 0b9b308ca586432ebb05bf0a694a7c6c2ec2d9bd Mon Sep 17 00:00:00 2001 From: Arseny Boykov <36469655+MrMrRobat@users.noreply.github.com> Date: Mon, 18 May 2020 23:41:24 +0300 Subject: [PATCH] Make signature class only (#1466) * Make signature class only * Add changes file --- changes/1466-MrMrRobat.md | 1 + pydantic/main.py | 4 +++- pydantic/utils.py | 21 +++++++++++++++++++++ tests/test_model_signature.py | 12 ++++++++++++ tests/test_utils.py | 24 +++++++++++++++++++++++- 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 changes/1466-MrMrRobat.md diff --git a/changes/1466-MrMrRobat.md b/changes/1466-MrMrRobat.md new file mode 100644 index 0000000..1e1cb3b --- /dev/null +++ b/changes/1466-MrMrRobat.md @@ -0,0 +1 @@ +Make `BaseModel.__signature__` class-only, so getting `__signature__` from model instance will raise `AttributeError` \ No newline at end of file diff --git a/pydantic/main.py b/pydantic/main.py index e1161d7..bb01d2e 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -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 diff --git a/pydantic/utils.py b/pydantic/utils.py index 1a31192..fdd4f59 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -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') diff --git a/tests/test_model_signature.py b/tests/test_model_signature.py index 6b384a7..789372b 100644 --- a/tests/test_model_signature.py +++ b/tests/test_model_signature.py @@ -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__') diff --git a/tests/test_utils.py b/tests/test_utils.py index a58e14f..feb373c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -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'