Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add examples and json_schema_extra to @computed_field #8013

Merged
merged 3 commits into from Nov 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 35 additions & 23 deletions pydantic/_internal/_generate_schema.py
Expand Up @@ -37,7 +37,7 @@
from typing_extensions import Annotated, Final, Literal, TypeAliasType, TypedDict, get_args, get_origin, is_typeddict

from ..annotated_handlers import GetCoreSchemaHandler, GetJsonSchemaHandler
from ..config import ConfigDict, JsonEncoder
from ..config import ConfigDict, JsonDict, JsonEncoder
from ..errors import PydanticSchemaGenerationError, PydanticUndefinedAnnotation, PydanticUserError
from ..json_schema import JsonSchemaValue
from ..version import version_short
Expand Down Expand Up @@ -987,15 +987,9 @@ def set_discriminator(schema: CoreSchema) -> CoreSchema:

json_schema_extra = field_info.json_schema_extra

def json_schema_update_func(schema: CoreSchemaOrField, handler: GetJsonSchemaHandler) -> JsonSchemaValue:
json_schema = {**handler(schema), **json_schema_updates}
if isinstance(json_schema_extra, dict):
json_schema.update(to_jsonable_python(json_schema_extra))
elif callable(json_schema_extra):
json_schema_extra(json_schema)
return json_schema

metadata = build_metadata_dict(js_annotation_functions=[json_schema_update_func])
metadata = build_metadata_dict(
js_annotation_functions=[get_json_schema_update_func(json_schema_updates, json_schema_extra)]
)

# apply alias generator
alias_generator = self._config_wrapper.alias_generator
Expand Down Expand Up @@ -1558,6 +1552,14 @@ def set_computed_field_metadata(schema: CoreSchemaOrField, handler: GetJsonSchem
if description is not None:
json_schema['description'] = description

examples = d.info.examples
if examples is not None:
json_schema['examples'] = to_jsonable_python(examples)

json_schema_extra = d.info.json_schema_extra
if json_schema_extra is not None:
add_json_schema_extra(json_schema, json_schema_extra)

return json_schema

metadata = build_metadata_dict(js_annotation_functions=[set_computed_field_metadata])
Expand Down Expand Up @@ -1707,20 +1709,8 @@ def _apply_single_annotation_json_schema(

json_schema_extra = metadata.json_schema_extra
if json_schema_update or json_schema_extra:

def json_schema_update_func(
core_schema: CoreSchemaOrField, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
json_schema = handler(core_schema)
json_schema.update(json_schema_update)
if isinstance(json_schema_extra, dict):
json_schema.update(to_jsonable_python(json_schema_extra))
elif callable(json_schema_extra):
json_schema_extra(json_schema)
return json_schema

CoreMetadataHandler(schema).metadata.setdefault('pydantic_js_annotation_functions', []).append(
json_schema_update_func
get_json_schema_update_func(json_schema_update, json_schema_extra)
)
return schema

Expand Down Expand Up @@ -2005,6 +1995,28 @@ def _extract_get_pydantic_json_schema(tp: Any, schema: CoreSchema) -> GetJsonSch
return js_modify_function


def get_json_schema_update_func(
json_schema_update: JsonSchemaValue, json_schema_extra: JsonDict | typing.Callable[[JsonDict], None] | None
) -> GetJsonSchemaFunction:
def json_schema_update_func(
core_schema_or_field: CoreSchemaOrField, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
json_schema = {**handler(core_schema_or_field), **json_schema_update}
add_json_schema_extra(json_schema, json_schema_extra)
return json_schema

return json_schema_update_func


def add_json_schema_extra(
json_schema: JsonSchemaValue, json_schema_extra: JsonDict | typing.Callable[[JsonDict], None] | None
):
if isinstance(json_schema_extra, dict):
json_schema.update(to_jsonable_python(json_schema_extra))
elif callable(json_schema_extra):
json_schema_extra(json_schema)


class _CommonField(TypedDict):
schema: core_schema.CoreSchema
validation_alias: str | list[str | int] | list[list[str | int]] | None
Expand Down
22 changes: 17 additions & 5 deletions pydantic/fields.py
Expand Up @@ -962,6 +962,8 @@ class ComputedFieldInfo:
alias_priority: priority of the alias. This affects whether an alias generator is used
title: Title of the computed field as in OpenAPI document, should be a short summary.
description: Description of the computed field as in OpenAPI document.
examples: Example values of the computed field as in OpenAPI document.
json_schema_extra: Dictionary of extra JSON schema properties.
sydney-runkle marked this conversation as resolved.
Show resolved Hide resolved
repr: A boolean indicating whether or not to include the field in the __repr__ output.
"""

Expand All @@ -972,6 +974,8 @@ class ComputedFieldInfo:
alias_priority: int | None
title: str | None
description: str | None
examples: list[Any] | None
json_schema_extra: JsonDict | typing.Callable[[JsonDict], None] | None
repr: bool


Expand All @@ -983,12 +987,14 @@ class ComputedFieldInfo:
@typing.overload
def computed_field(
*,
return_type: Any = PydanticUndefined,
alias: str | None = None,
alias_priority: int | None = None,
title: str | None = None,
description: str | None = None,
examples: list[Any] | None = None,
json_schema_extra: JsonDict | typing.Callable[[JsonDict], None] | None = None,
repr: bool = True,
return_type: Any = PydanticUndefined,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this so that the overload and the actual signature matched better so they were easier to compare. Then I noticed that here it's repr: bool = True but the actual signature has repr: bool | None = None, is that intentional?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question.

I don't think so. I think we should use repr: bool | None = None here as well. As far as I understand it, the reason we allow None here is so that we can enforce a default of True if the computed_field is public and False if it's not.

) -> typing.Callable[[PropertyT], PropertyT]:
...

Expand Down Expand Up @@ -1017,6 +1023,8 @@ def computed_field(
alias_priority: int | None = None,
title: str | None = None,
description: str | None = None,
examples: list[Any] | None = None,
json_schema_extra: JsonDict | typing.Callable[[JsonDict], None] | None = None,
repr: bool | None = None,
return_type: Any = PydanticUndefined,
) -> PropertyT | typing.Callable[[PropertyT], PropertyT]:
Expand Down Expand Up @@ -1140,9 +1148,11 @@ def _private_property(self) -> int:
__f: the function to wrap.
alias: alias to use when serializing this computed field, only used when `by_alias=True`
alias_priority: priority of the alias. This affects whether an alias generator is used
title: Title to used when including this computed field in JSON Schema, currently unused waiting for #4697
description: Description to used when including this computed field in JSON Schema, defaults to the functions
docstring, currently unused waiting for #4697
title: Title to use when including this computed field in JSON Schema
description: Description to use when including this computed field in JSON Schema, defaults to the function's
docstring
examples: Example values to use when including this computed field in JSON Schema
json_schema_extra: Dictionary of extra JSON schema properties.
repr: whether to include this computed field in model repr.
Default is `False` for private properties and `True` for public properties.
return_type: optional return for serialization logic to expect when serializing to JSON, if included
Expand All @@ -1169,7 +1179,9 @@ def dec(f: Any) -> Any:
else:
repr_ = repr

dec_info = ComputedFieldInfo(f, return_type, alias, alias_priority, title, description, repr_)
dec_info = ComputedFieldInfo(
f, return_type, alias, alias_priority, title, description, examples, json_schema_extra, repr_
)
return _decorators.PydanticDescriptorProxy(f, dec_info)

if __f is None:
Expand Down
9 changes: 8 additions & 1 deletion tests/test_computed_fields.py
Expand Up @@ -73,7 +73,12 @@ def area(self) -> int:
"""An awesome area"""
return self.width * self.length

@computed_field(title='Pikarea', description='Another area')
@computed_field(
title='Pikarea',
description='Another area',
examples=[100, 200],
json_schema_extra={'foo': 42},
)
@property
def area2(self) -> int:
return self.width * self.length
Expand Down Expand Up @@ -103,6 +108,8 @@ def double_width(self) -> int:
'area2': {
'title': 'Pikarea',
'description': 'Another area',
'examples': [100, 200],
'foo': 42,
'type': 'integer',
'readOnly': True,
},
Expand Down