mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
Make signature class only (#1466)
* Make signature class only * Add changes file
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Make `BaseModel.__signature__` class-only, so getting `__signature__` from model instance will raise `AttributeError`
|
||||
+3
-1
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user