8000 Update fields collection logic · pydantic/pydantic@340d32d · GitHub
[go: up one dir, main page]

Skip to content

Commit 340d32d

Browse files
committed
Update fields collection logic
The source is provided, and the logic to detect final fields with a default value is moved *after* the `FieldInfo` instance is created. The reason to do so is to again support more edge cases, e.g. When the annotation was wrapped with `Annotated` (e.g. `Annotated[Final[int], ...] = 1`. This previously wasn't considered and as such wasn't treated as a classvar. As per the linked issue in this PR, this led to an inconsistency, so while technically this can be considered as a breaking change, we'll consider this as a bug.
1 parent ddff5ce commit 340d32d

File tree

2 files changed

+52
-31
lines changed

2 files changed

+52
-31
lines changed

pydantic/_internal/_fields.py

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pydantic_core import PydanticUndefined
1515
from typing_extensions import TypeIs, get_origin
1616
from typing_inspection import typing_objects
17+
from typing_inspection.introspection import AnnotationSource
1718

1819
from pydantic import PydanticDeprecatedSince211
1920
from pydantic.errors import PydanticUserError
@@ -166,17 +167,6 @@ def collect_model_fields( # noqa: C901
166167

167168
assigned_value = getattr(cls, ann_name, PydanticUndefined)
168169

169-
if _is_finalvar_with_default_val(ann_type, assigned_value):
170-
warnings.warn(
171-
f'Annotation {ann_name!r} is marked as final and has a default value. Pydantic treats {ann_name!r} as a '
172-
'class variable, but it will be considered as a normal field in V3 to be aligned with dataclasses. If you '
173-
f'still want {ann_name!r} to be considered as a class variable, annotate it as: `ClassVar[<type>] = <default>.`',
174-
category=PydanticDeprecatedSince211,
175-
# Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case:
176-
stacklevel=4,
177-
)
178-
class_vars.add(ann_name)
179-
continue
180170
if not is_valid_field_name(ann_name):
181171
continue
182172
if cls.__pydantic_root_model__ and ann_name != 'root':
@@ -223,7 +213,7 @@ def collect_model_fields( # noqa: C901
223213
# The field was not found on any base classes; this seems to be caused by fields not getting
224214
# generated thanks to models not being fully defined while initializing recursive models.
225215
# Nothing stops us from just creating a new FieldInfo for this type hint, so we do this.
226-
field_info = FieldInfo_.from_annotation(ann_type)
216+
field_info = FieldInfo_.from_annotation(ann_type, _source=AnnotationSource.CLASS)
227217

228218
if not evaluated:
229219
field_info._complete = False
@@ -245,13 +235,24 @@ def collect_model_fields( # noqa: C901
245235
copy(assigned_value) if not evaluated and isinstance(assigned_value, FieldInfo_) else assigned_value
246236
)
247237

248-
field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value)
238+
field_info = FieldInfo_.from_annotated_attribute(ann_type, assigned_value, _source=AnnotationSource.CLASS)
249239
if not evaluated:
250240
field_info._complete = False
251241
# Store the original annotation and assignment value that should be used to rebuild
252242
# the field info later:
253243
field_info._original_annotation = ann_type
254244
field_info._original_assignment = original_assignment
245+
elif 'final' in field_info._qualifiers and not field_info.is_required():
246+
warnings.warn(
247+
f'Annotation {ann_name!r} is marked as final and has a default value. Pydantic treats {ann_name!r} as a '
248+
'class variable, but it will be considered as a normal field in V3 to be aligned with dataclasses. If you '
249+
f'still want {ann_name!r} to be considered as a class variable, annotate it as: `ClassVar[<type>] = <default>.`',
250+
category=PydanticDeprecatedSince211,
251+
# Incorrect when `create_model` is used, but the chance that final with a default is used is low in that case:
252+
stacklevel=4,
253+
)
254+
class_vars.add(ann_name)
255+
continue
255256

256257
# attributes which are fields are removed from the class namespace:
257258
# 1. To match the behaviour of annotation-only fields
@@ -297,20 +298,6 @@ def _warn_on_nested_alias_in_annotation(ann_type: type[Any], ann_name: str) -> N
297298
return
298299

299300

300-
def _is_finalvar_with_default_val(ann_type: type[Any], assigned_value: Any) -> bool:
301-
if assigned_value is PydanticUndefined:
302-
return False
303-
304-
FieldInfo = import_cached_field_info()
305-
306-
if isinstance(assigned_value, FieldInfo) and assigned_value.is_required():
307-
return False
308-
elif not _typing_extra.is_finalvar(ann_type):
309-
return False
310-
else:
311-
return True
312-
313-
314301
def rebuild_model_fields(
315302
cls: type[BaseModel],
316303
*,
@@ -340,9 +327,11 @@ def rebuild_model_fields(
340327
ann = _generics.replace_types(ann, typevars_map)
341328

342329
if (assign := field_info._original_assignment) is PydanticUndefined:
343-
rebuilt_fields[f_name] = FieldInfo_.from_annotation(ann)
330+
rebuilt_fields[f_name] = FieldInfo_.from_annotation(ann, _source=AnnotationSource.CLASS)
344331
else:
345-
rebuilt_fields[f_name] = FieldInfo_.from_annotated_attribute(ann, assign)
332+
rebuilt_fields[f_name] = FieldInfo_.from_annotated_attribute(
333+
ann, assign, _source=AnnotationSource.CLASS
334+
)
346335

347336
return rebuilt_fields
348337

@@ -411,9 +400,13 @@ def collect_dataclass_fields(
411400

412401
# TODO: same note as above re validate_assignment
413402
continue
414-
field_info = FieldInfo_.from_annotated_attribute(ann_type, dataclass_field.default)
403+
field_info = FieldInfo_.from_annotated_attribute(
404+
ann_type, dataclass_field.default, _source=AnnotationSource.DATACLASS
405+
)
415406
else:
416-
field_info = FieldInfo_.from_annotated_attribute(ann_type, dataclass_field)
407+
field_info = FieldInfo_.from_annotated_attribute(
408+
ann_type, dataclass_field, _source=AnnotationSource.DATACLASS
409+
)
417410

418411
fields[ann_name] = field_info
419412

tests/test_main.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,6 +2033,34 @@ class Model(BaseModel):
20332033
assert 'a' not in Model.model_fields
20342034

20352035

2036+
@pytest.mark.parametrize(
2037+
'ann',
2038+
[Final, Final[int]],
2039+
ids=['no-arg', 'with-arg'],
2040+
)
2041+
def test_deprecated_annotated_final_field_decl_with_default_val(ann):
2042+
with pytest.warns(PydanticDeprecatedSince211):
2043+
2044+
class Model(BaseModel):
2045+
a: Annotated[ann, ...] = 10
2046+
2047+
assert 'a' in Model.__class_vars__
2048+
assert 'a' not in Model.model_fields
2049+
2050+
2051+
@pytest.mark.xfail(reason="When rebuilding fields, we don't consider the field as a class variable")
2052+
def test_deprecated_final_field_with_default_val_rebuild():
2053+
class Model(BaseModel):
2054+
a: 'Final[MyInt]' = 1
2055+
2056+
MyInt = int
2057+
2058+
Model.model_rebuild()
2059+
2060+
assert 'a' in Model.__class_vars__
2061+
assert 'a' not in Model.model_fields
2062+
2063+
20362064
def test_final_field_reassignment():
20372065
class Model(BaseModel):
20382066
model_config = ConfigDict(validate_assignment=True)

0 commit comments

Comments
 (0)
0