From 71e69efe3f54dd0dd29bef635f2bd1729cb989f3 Mon Sep 17 00:00:00 2001 From: Jocelyn-Gas <78849683+Jocelyn-Gas@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:19:41 +0100 Subject: [PATCH] Introducing classproperty decorator for model_computed_fields (#8437) --- pydantic/_internal/_model_construction.py | 5 +++++ pydantic/main.py | 15 ++++++--------- tests/test_computed_fields.py | 7 +++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py index e90636f6f3..49da37c449 100644 --- a/pydantic/_internal/_model_construction.py +++ b/pydantic/_internal/_model_construction.py @@ -188,6 +188,11 @@ def wrapped_model_post_init(self: BaseModel, __context: Any) -> None: types_namespace=types_namespace, create_model_module=_create_model_module, ) + + # If this is placed before the complete_model_class call above, + # the generic computed fields return type is set to PydanticUndefined + cls.model_computed_fields = {k: v.info for k, v in cls.__pydantic_decorators__.computed_fields.items()} + # using super(cls, cls) on the next line ensures we only call the parent class's __pydantic_init_subclass__ # I believe the `type: ignore` is only necessary because mypy doesn't realize that this code branch is # only hit for _proper_ subclasses of BaseModel diff --git a/pydantic/main.py b/pydantic/main.py index dff7893022..59c62ad9e3 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -122,16 +122,22 @@ class BaseModel(metaclass=_model_construction.ModelMetaclass): __pydantic_serializer__: ClassVar[SchemaSerializer] __pydantic_validator__: ClassVar[SchemaValidator] + model_computed_fields: ClassVar[dict[str, ComputedFieldInfo]] + """A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects.""" + # Instance attributes # Note: we use the non-existent kwarg `init=False` in pydantic.fields.Field below so that @dataclass_transform # doesn't think these are valid as keyword arguments to the class initializer. __pydantic_extra__: dict[str, Any] | None = _Field(init=False) # type: ignore __pydantic_fields_set__: set[str] = _Field(init=False) # type: ignore __pydantic_private__: dict[str, Any] | None = _Field(init=False) # type: ignore + else: # `model_fields` and `__pydantic_decorators__` must be set for # pydantic._internal._generate_schema.GenerateSchema.model_schema to work for a plain BaseModel annotation model_fields = {} + model_computed_fields = {} + __pydantic_decorators__ = _decorators.DecoratorInfos() __pydantic_parent_namespace__ = None # Prevent `BaseModel` from being instantiated directly: @@ -167,15 +173,6 @@ def __init__(self, /, **data: Any) -> None: # type: ignore # The following line sets a flag that we use to determine when `__init__` gets overridden by the user __init__.__pydantic_base_init__ = True - @property - def model_computed_fields(self) -> dict[str, ComputedFieldInfo]: - """Get the computed fields of this model instance. - - Returns: - A dictionary of computed field names and their corresponding `ComputedFieldInfo` objects. - """ - return {k: v.info for k, v in self.__pydantic_decorators__.computed_fields.items()} - @property def model_extra(self) -> dict[str, Any] | None: """Get extra fields set during validation. diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py index 9a4377a21c..0bc0d0a7e2 100644 --- a/tests/test_computed_fields.py +++ b/tests/test_computed_fields.py @@ -62,6 +62,13 @@ def double_width(self) -> int: assert rect.model_dump() == {'width': 10, 'length': 5, 'area': 50, 'area2': 50} assert rect.model_dump_json() == '{"width":10,"length":5,"area":50,"area2":50}' + assert set(Rectangle.model_fields) == {'width', 'length'} + assert set(Rectangle.model_computed_fields) == {'area', 'area2'} + + assert Rectangle.model_computed_fields['area'].description == 'An awesome area' + assert Rectangle.model_computed_fields['area2'].title == 'Pikarea' + assert Rectangle.model_computed_fields['area2'].description == 'Another area' + def test_computed_fields_json_schema(): class Rectangle(BaseModel):