Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make signature class only #1466

Merged
merged 2 commits into from May 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'