diff --git a/ormar/models/helpers/validation.py b/ormar/models/helpers/validation.py index d861911..c557de4 100644 --- a/ormar/models/helpers/validation.py +++ b/ormar/models/helpers/validation.py @@ -1,8 +1,9 @@ import datetime import decimal +import numbers import uuid from enum import Enum -from typing import Any, Dict, List, TYPE_CHECKING, Tuple, Type +from typing import Any, Dict, List, Set, TYPE_CHECKING, Tuple, Type, Union try: import orjson as json @@ -138,15 +139,79 @@ def generate_model_example(model: Type["Model"], relation_map: Dict = None) -> D if not field.is_relation: example[name] = field.__sample__ elif isinstance(relation_map, dict) and name in relation_map: - value = generate_model_example( - field.to, relation_map=relation_map.get(name, {}) + example[name] = get_nested_model_example( + name=name, field=field, relation_map=relation_map ) - new_value = [value] if field.is_multi or field.virtual else value - example[name] = new_value + to_exclude = {name for name in model.Meta.model_fields} + pydantic_repr = generate_pydantic_example(pydantic_model=model, exclude=to_exclude) + example.update(pydantic_repr) return example +def get_nested_model_example( + name: str, field: "BaseField", relation_map: Dict +) -> Union[List, Dict]: + """ + Gets representation of nested model. + + :param name: name of the field to follow + :type name: str + :param field: ormar field + :type field: BaseField + :param relation_map: dict with relation map + :type relation_map: Dict + :return: nested model or list of nested model repr + :rtype: Union[List, Dict] + """ + value = generate_model_example(field.to, relation_map=relation_map.get(name, {})) + new_value: Union[List, Dict] = [value] if field.is_multi or field.virtual else value + return new_value + + +def generate_pydantic_example( + pydantic_model: Type[pydantic.BaseModel], exclude: Set = None +) -> Dict: + """ + Generates dict with example. + + :param pydantic_model: model to parse + :type pydantic_model: Type[pydantic.BaseModel] + :param exclude: list of fields to exclude + :type exclude: Optional[Set] + :return: dict with fields and sample values + :rtype: Dict + """ + example: Dict[str, Any] = dict() + exclude = exclude or set() + for name in pydantic_model.__fields__: + if name not in exclude: + field = pydantic_model.__fields__[name] + type_ = field.type_ + if getattr(field.outer_type_, "_name", None) == "List": + example[name] = [get_pydantic_example_repr(type_)] + else: + example[name] = get_pydantic_example_repr(type_) + return example + + +def get_pydantic_example_repr(type_: Any) -> Any: + """ + Gets sample representation of pydantic field for example dict. + + :param type_: type of pydantic field + :type type_: Any + :return: representation to include in example + :rtype: Any + """ + if issubclass(type_, (numbers.Number, decimal.Decimal)): + return 0 + elif issubclass(type_, pydantic.BaseModel): + return generate_pydantic_example(pydantic_model=type_) + else: + return "string" + + def construct_modify_schema_function(fields_with_choices: List) -> SchemaExtraCallable: """ Modifies the schema to include fields with choices validator. diff --git a/ormar/models/mixins/save_mixin.py b/ormar/models/mixins/save_mixin.py index a7ac562..6a3aab0 100644 --- a/ormar/models/mixins/save_mixin.py +++ b/ormar/models/mixins/save_mixin.py @@ -275,12 +275,12 @@ class SavePrepareMixin(RelationMixin, AliasMixin): :rtype: int """ for field in fields_list: - value = getattr(self, field.name) or [] - if not isinstance(value, list): - value = [value] - for val in value: + values = getattr(self, field.name) or [] + if not isinstance(values, list): + values = [values] + for value in values: if follow: - update_count = await val.save_related( + update_count = await value.save_related( follow=follow, save_all=save_all, relation_map=self._skip_ellipsis( # type: ignore @@ -291,8 +291,8 @@ class SavePrepareMixin(RelationMixin, AliasMixin): relation_field=field, ) else: - update_count = await val._upsert_model( - instance=val, + update_count = await value._upsert_model( + instance=value, save_all=save_all, previous_model=self, relation_field=field, diff --git a/ormar/models/newbasemodel.py b/ormar/models/newbasemodel.py index 865a48b..c4dc612 100644 --- a/ormar/models/newbasemodel.py +++ b/ormar/models/newbasemodel.py @@ -316,11 +316,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass k, model_fields[k].expand_relationship(v, self, to_register=False,) if k in model_fields - else ( - v - if k in pydantic_fields - else model_fields["HAP&*YA^)*GW^&QT6567q56gGG%$%"] - ), + else (v if k in pydantic_fields else model_fields[k]), "dumps", ) for k, v in kwargs.items() diff --git a/tests/test_fastapi/test_fastapi_docs.py b/tests/test_fastapi/test_fastapi_docs.py index 94ac9e1..e19fe04 100644 --- a/tests/test_fastapi/test_fastapi_docs.py +++ b/tests/test_fastapi/test_fastapi_docs.py @@ -1,4 +1,5 @@ -from typing import List +import datetime +from typing import List, Optional import databases import pydantic @@ -35,6 +36,17 @@ class LocalMeta: database = database +class PTestA(pydantic.BaseModel): + c: str + d: bytes + e: datetime.datetime + + +class PTestP(pydantic.BaseModel): + a: int + b: Optional[PTestA] + + class Category(ormar.Model): class Meta(LocalMeta): tablename = "categories" @@ -49,6 +61,8 @@ class Item(ormar.Model): id: int = ormar.Integer(primary_key=True) name: str = ormar.String(max_length=100) + pydantic_int: Optional[int] + test_P: Optional[List[PTestP]] categories = ormar.ManyToMany(Category) @@ -126,16 +140,27 @@ def test_schema_modification(): ) assert schema["properties"]["categories"]["title"] == "Categories" assert schema["example"] == { + "categories": [{"id": 0, "name": "string"}], "id": 0, "name": "string", - "categories": [{"id": 0, "name": "string"}], + "pydantic_int": 0, + "test_P": [{"a": 0, "b": {"c": "string", "d": "string", "e": "string"}}], } schema = Category.schema() assert schema["example"] == { "id": 0, "name": "string", - "items": [{"id": 0, "name": "string"}], + "items": [ + { + "id": 0, + "name": "string", + "pydantic_int": 0, + "test_P": [ + {"a": 0, "b": {"c": "string", "d": "string", "e": "string"}} + ], + } + ], }