Skip to content

Commit b332725

Browse files
authored
Add support for computed property (#286)
1 parent e09de1a commit b332725

File tree

5 files changed

+79
-12
lines changed

5 files changed

+79
-12
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ Please follow [the Keep a Changelog standard](https://keepachangelog.com/en/1.0.
55

66
## [Unreleased]
77

8+
## [5.4.2]
9+
10+
### Fixed
11+
12+
* Added support for computed fields
13+
814
## [5.4.1]
915

1016
### Fixed

cadwyn/schema_generation.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ def _wrap_validator(func: Callable, is_pydantic_v1_style_validator: Any, decorat
236236
func = func.__func__
237237
kwargs = dataclasses.asdict(decorator_info)
238238
decorator_fields = kwargs.pop("fields", None)
239+
240+
# wrapped_property is not accepted by computed_field()
241+
if isinstance(decorator_info, ComputedFieldInfo):
242+
kwargs.pop("wrapped_property", None)
243+
239244
actual_decorator = PYDANTIC_DECORATOR_TYPE_TO_DECORATOR_MAP[type(decorator_info)]
240245
if is_pydantic_v1_style_validator:
241246
# There's an inconsistency in their interfaces so we gotta resort to this
@@ -1061,19 +1066,30 @@ def _delete_field_attributes(
10611066

10621067

10631068
def _delete_field_from_model(model: _PydanticModelWrapper, field_name: str, version_change_name: str):
1064-
if field_name not in model.fields:
1069+
if field_name in model.fields:
1070+
model.fields.pop(field_name)
1071+
model.annotations.pop(field_name)
1072+
for validator_name, validator in model.validators.copy().items():
1073+
if isinstance(validator, _PerFieldValidatorWrapper) and field_name in validator.fields:
1074+
validator.fields.remove(field_name)
1075+
# TODO: This behavior doesn't feel natural
1076+
if not validator.fields:
1077+
model.validators[validator_name].is_deleted = True
1078+
1079+
elif (
1080+
field_name in model.validators
1081+
and isinstance(model.validators[field_name], _ValidatorWrapper)
1082+
and hasattr(model.validators[field_name], "decorator")
1083+
and model.validators[field_name].decorator == pydantic.computed_field
1084+
):
1085+
validator = model.validators[field_name]
1086+
model.validators[field_name].is_deleted = True
1087+
model.annotations.pop(field_name, None)
1088+
else:
10651089
raise InvalidGenerationInstructionError(
10661090
f'You tried to delete a field "{field_name}" from "{model.name}" '
10671091
f'in "{version_change_name}" but it doesn\'t have such a field.',
10681092
)
1069-
model.fields.pop(field_name)
1070-
model.annotations.pop(field_name)
1071-
for validator_name, validator in model.validators.copy().items():
1072-
if isinstance(validator, _PerFieldValidatorWrapper) and field_name in validator.fields:
1073-
validator.fields.remove(field_name)
1074-
# TODO: This behavior doesn't feel natural
1075-
if not validator.fields:
1076-
model.validators[validator_name].is_deleted = True
10771093

10781094

10791095
class _DummyEnum(Enum):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cadwyn"
3-
version = "5.4.1"
3+
version = "5.4.2"
44
description = "Production-ready community-driven modern Stripe-like API versioning in FastAPI"
55
authors = [{ name = "Stanislav Zmiev", email = "zmievsa@gmail.com" }]
66
license = "MIT"

tests/test_schema_generation/test_schema_field.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Annotated, Any, Literal, Union
44

55
import pytest
6-
from pydantic import BaseModel, Field, StringConstraints, ValidationError, conint, constr
6+
from pydantic import BaseModel, Field, StringConstraints, ValidationError, computed_field, conint, constr
77
from pydantic.fields import FieldInfo
88

99
from cadwyn.exceptions import (
@@ -762,3 +762,48 @@ def test__schema_field_had__nonexistent_field__should_raise_error(create_runtime
762762
),
763763
):
764764
create_runtime_schemas(version_change(schema(SchemaWithOneStrField).field("boo").had(type=int)))
765+
766+
767+
def test__schema_with_computed_field__should_be_recreated_in_older_version(
768+
create_runtime_schemas: CreateRuntimeSchemas,
769+
):
770+
class SchemaWithComputedField(BaseModel):
771+
image_file_key: str = Field(exclude=True)
772+
773+
@computed_field
774+
@property
775+
def image_url(self) -> str:
776+
return f"https://example.com/{self.image_file_key}"
777+
778+
schemas = create_runtime_schemas(version_change())
779+
780+
latest_model = schemas["2001-01-01"][SchemaWithComputedField]
781+
old_model = schemas["2000-01-01"][SchemaWithComputedField]
782+
783+
latest_instance = latest_model(image_file_key="test.jpg")
784+
old_instance = old_model(image_file_key="test.jpg")
785+
786+
assert latest_instance.image_url == "https://example.com/test.jpg"
787+
assert old_instance.image_url == "https://example.com/test.jpg"
788+
789+
790+
def test__schema_with_computed_field__remove_computed_field(create_runtime_schemas: CreateRuntimeSchemas):
791+
class SchemaWithComputedField(BaseModel):
792+
image_file_key: str = Field(exclude=True)
793+
794+
@computed_field
795+
@property
796+
def image_url(self) -> str:
797+
return f"https://example.com/{self.image_file_key}"
798+
799+
schemas = create_runtime_schemas(version_change(schema(SchemaWithComputedField).field("image_url").didnt_exist))
800+
801+
latest_model = schemas["2001-01-01"][SchemaWithComputedField]
802+
latest_instance = latest_model(image_file_key="test.jpg")
803+
assert latest_instance.image_url == "https://example.com/test.jpg"
804+
805+
old_model = schemas["2000-01-01"][SchemaWithComputedField]
806+
old_instance = old_model(image_file_key="test.jpg")
807+
808+
assert not hasattr(old_instance, "image_url")
809+
assert "image_url" not in old_instance.model_dump()

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)