diff --git a/changes/4407-tlambert03.md b/changes/4407-tlambert03.md index e315bb1487..a4b567399d 100644 --- a/changes/4407-tlambert03.md +++ b/changes/4407-tlambert03.md @@ -1 +1 @@ -Fix PEP487 protocol in `BaseModel`: call `__set_name__` on namespace values that implement the method. +Fix PEP487 `__set_name__` protocol in `BaseModel` for PrivateAttrs. diff --git a/pydantic/main.py b/pydantic/main.py index 0490216d7a..6c1eaf8ee4 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -287,10 +287,12 @@ def is_untouched(v: Any) -> bool: cls.__try_update_forward_refs__() # preserve `__set_name__` protocol defined in https://peps.python.org/pep-0487 + # for attributes not in `new_namespace` (e.g. private attributes) for name, obj in namespace.items(): - set_name = getattr(obj, '__set_name__', None) - if callable(set_name): - set_name(cls, name) + if name not in new_namespace: + set_name = getattr(obj, '__set_name__', None) + if callable(set_name): + set_name(cls, name) return cls diff --git a/tests/test_create_model.py b/tests/test_create_model.py index 21c96e20fd..42387d5b7a 100644 --- a/tests/test_create_model.py +++ b/tests/test_create_model.py @@ -225,10 +225,11 @@ class TestGenericModel(GenericModel): assert result.__config__.orm_mode is True -def test_set_name(): +@pytest.mark.parametrize('base', [ModelPrivateAttr, object]) +def test_set_name(base): calls = [] - class class_deco(ModelPrivateAttr): + class class_deco(base): def __init__(self, fn): super().__init__() self.fn = fn @@ -236,9 +237,22 @@ def __init__(self, fn): def __set_name__(self, owner, name): calls.append((owner, name)) + def __get__(self, obj, type=None): + return self.fn(obj) if obj else self + class A(BaseModel): + x: int + @class_deco def _some_func(self): - return self + return self.x assert calls == [(A, '_some_func')] + a = A(x=2) + + # we don't test whether calling the method on a PrivateAttr works: + # attribute access on privateAttributes is more complicated, it doesn't + # get added to the class namespace (and will also get set on the instance + # with _init_private_attributes), so the descriptor protocol won't work. + if base is object: + assert a._some_func == 2