Skip to content

Commit

Permalink
Make signature class only (#1466)
Browse files Browse the repository at this point in the history
* Make signature class only

* Add changes file
  • Loading branch information
Bobronium committed May 18, 2020
1 parent c8906ce commit 0b9b308
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 2 deletions.
1 change: 1 addition & 0 deletions changes/1466-MrMrRobat.md
@@ -0,0 +1 @@
Make `BaseModel.__signature__` class-only, so getting `__signature__` from model instance will raise `AttributeError`
4 changes: 3 additions & 1 deletion pydantic/main.py
Expand Up @@ -35,6 +35,7 @@
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,
Expand Down Expand Up @@ -300,7 +301,8 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901
}

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


Expand Down
21 changes: 21 additions & 0 deletions pydantic/utils.py
Expand Up @@ -45,6 +45,7 @@
'GetterDict',
'ValueItems',
'version_info', # required here to match behaviour in v1.3
'ClassAttribute',
)


Expand Down Expand Up @@ -439,3 +440,23 @@ def _normalize_indexes(

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 changes: 12 additions & 0 deletions tests/test_model_signature.py
Expand Up @@ -126,3 +126,15 @@ class Config:
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__')
24 changes: 23 additions & 1 deletion tests/test_utils.py
Expand Up @@ -12,7 +12,15 @@
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:
Expand Down Expand Up @@ -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'

0 comments on commit 0b9b308

Please sign in to comment.