Skip to content

Commit

Permalink
Introducing classproperty decorator for model_computed_fields (#8437)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jocelyn-Gas committed Jan 9, 2024
1 parent 1a5d084 commit 71e69ef
Show file tree
Hide file tree
Showing 3 changed files with 18 additions and 9 deletions.
5 changes: 5 additions & 0 deletions pydantic/_internal/_model_construction.py
Expand Up @@ -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
Expand Down
15 changes: 6 additions & 9 deletions pydantic/main.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions tests/test_computed_fields.py
Expand Up @@ -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):
Expand Down

0 comments on commit 71e69ef

Please sign in to comment.