8000 Make sure `__pydantic_private__` exists before setting private attrib… · pydantic/pydantic@da84149 · GitHub
[go: up one dir, main page]

Skip to content

Commit da84149

Browse files
authored
Make sure __pydantic_private__ exists before setting private attributes (#11666)
1 parent 0cfe853 commit da84149

File tree

2 files changed

+39
-1
lines changed

2 files changed

+39
-1
lines changed

pydantic/main.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,20 @@ def _model_field_setattr_handler(model: BaseModel, name: str, val: Any) -> None:
9999
model.__pydantic_fields_set__.add(name)
100100

101101

102+
def _private_setattr_handler(model: BaseModel, name: str, val: Any) -> None:
103+
if getattr(model, '__pydantic_private__', None) is None:
104+
# While the attribute should be present at this point, this may not be the case if
105+
# users do unusual stuff with `model_post_init()` (which is where the `__pydantic_private__`
106+
# is initialized, by wrapping the user-defined `model_post_init()`), e.g. if they mock
107+
# the `model_post_init()` call. Ideally we should find a better way to init private attrs.
108+
object.__setattr__(model, '__pydantic_private__', {})
109+
model.__pydanti 10000 c_private__[name] = val # pyright: ignore[reportOptionalSubscript]
110+
111+
102112
_SIMPLE_SETATTR_HANDLERS: Mapping[str, Callable[[BaseModel, str, Any], None]] = {
103113
'model_field': _model_field_setattr_handler,
104114
'validate_assignment': lambda model, name, val: model.__pydantic_validator__.validate_assignment(model, name, val), # pyright: ignore[reportAssignmentType]
105-
'private': lambda model, name, val: model.__pydantic_private__.__setitem__(name, val), # pyright: ignore[reportOptionalMemberAccess]
115+
'private': _private_setattr_handler,
106116
'cached_property': lambda model, name, val: model.__dict__.__setitem__(name, val),
107117
'extra_known': lambda model, name, val: _object_setattr(model, name, val),
108118
}

tests/test_main.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,6 +2307,34 @@ class C(A, B):
23072307
assert calls == ['C.model_post_init']
23082308

23092309

2310+
def test_model_post_init_mocked_setattr() -> None:
2311+
"""https://github.com/pydantic/pydantic/issues/11646
2312+
2313+
Fixes a small regression in 2.11. To instantiate private attributes on model instances
2314+
(and as such the `__pydantic_private__` instance attribute), Pydantic defines its own
2315+
`model_post_init()` (and wraps the user-defined one if it exists). In tests, some users
2316+
can mock their `model_post_init()` if they want to avoid unwanted side-effects (meaning
2317+
`__pydantic_private__` won't be instantiated).
2318+
In 2.11, the `BaseModel.__setattr__` logic was tweaked and required the `__pydantic_private__`
2319+
attribute to be present, resulting in attribute errors.
2320+
"""
2321+
2322+
class Model(BaseModel):
2323+
_a: int
2324+
2325+
def model_post_init(self, context: Any, /) -> None:
2326+
"""Do some stuff"""
2327+
2328+
# This reproduces `patch.object(Model, 'model_post_init')`:
2329+
Model.model_post_init = lambda *args, **kwargs: None
2330+
2331+
m = Model()
2332+
assert m.__pydantic_private__ is None
2333+
2334+
m._a = 2
2335+
assert m._a == 2
2336+
2337+
23102338
def test_del_model_attr():
23112339
class Model(BaseModel):
23122340
some_field: str

0 commit comments

Comments
 (0)
0