Skip to content

Commit

Permalink
Add support for stringified annotations when using PrivateAttr with…
Browse files Browse the repository at this point in the history
… `Annotated` (pydantic#10157)

Co-authored-by: Alex Hall <[email protected]>
  • Loading branch information
Viicos and alexmojaki authored Aug 16, 2024
1 parent 7212484 commit ed54f28
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 4 deletions.
19 changes: 17 additions & 2 deletions pydantic/_internal/_model_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

import builtins
import operator
import sys
import typing
import warnings
import weakref
from abc import ABCMeta
from functools import lru_cache, partial
from types import FunctionType
from typing import Any, Callable, Generic, Literal, NoReturn
from typing import Any, Callable, ForwardRef, Generic, Literal, NoReturn

import typing_extensions
from pydantic_core import PydanticUndefined, SchemaSerializer
Expand All @@ -28,7 +29,13 @@
from ._mock_val_ser import set_model_mocks
from ._schema_generation_shared import CallbackGetCoreSchemaHandler
from ._signature import generate_pydantic_signature
from ._typing_extra import get_cls_types_namespace, is_annotated, is_classvar, parent_frame_namespace
from ._typing_extra import (
eval_type_backport,
get_cls_types_namespace,
is_annotated,
is_classvar,
parent_frame_namespace,
)
from ._utils import ClassAttribute, SafeGetItemProxy
from ._validate_call import ValidateCallWrapper

Expand Down Expand Up @@ -430,6 +437,14 @@ def inspect_namespace( # noqa C901
and ann_type not in all_ignored_types
and getattr(ann_type, '__module__', None) != 'functools'
):
if isinstance(ann_type, str):
# Walking up the frames to get the module namespace where the model is defined
# (as the model class wasn't created yet, we unfortunately can't use `cls.__module__`):
frame = sys._getframe(2)
if frame is not None:
ann_type = eval_type_backport(
ForwardRef(ann_type), globalns=frame.f_globals, localns=frame.f_locals
)
if is_annotated(ann_type):
_, *metadata = typing_extensions.get_args(ann_type)
private_attr = next((v for v in metadata if isinstance(v, ModelPrivateAttr)), None)
Expand Down
6 changes: 4 additions & 2 deletions tests/test_annotated.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,18 +424,20 @@ def test_annotated_private_field_with_default():
class AnnotatedPrivateFieldModel(BaseModel):
_foo: Annotated[int, PrivateAttr(default=1)]
_bar: Annotated[str, 'hello']
_baz: 'Annotated[str, PrivateAttr(default=2)]'

model = AnnotatedPrivateFieldModel()
assert model._foo == 1
assert model._baz == 2

assert model.__pydantic_private__ == {'_foo': 1}
assert model.__pydantic_private__ == {'_foo': 1, '_baz': 2}

with pytest.raises(AttributeError):
assert model._bar

model._bar = 'world'
assert model._bar == 'world'
assert model.__pydantic_private__ == {'_foo': 1, '_bar': 'world'}
assert model.__pydantic_private__ == {'_foo': 1, '_bar': 'world', '_baz': 2}

with pytest.raises(AttributeError):
assert model.bar
Expand Down

0 comments on commit ed54f28

Please sign in to comment.