switch dict() to include relations comming from _iterate_related_models and not only nested not nullable ones

This commit is contained in:
collerek
2021-03-30 12:16:33 +02:00
parent 844ecae8f9
commit f0023773e3
5 changed files with 58 additions and 47 deletions

View File

@ -4,8 +4,12 @@
* `save_related(follow=False)` now accept also second argument `save_related(follow=False, save_all=False)`. * `save_related(follow=False)` now accept also second argument `save_related(follow=False, save_all=False)`.
By default so with `save_all=False` `ormar` only upserts models that are no saved (so new or updated ones), By default so with `save_all=False` `ormar` only upserts models that are no saved (so new or updated ones),
with `save_all=True` all related models are saved, regardless of `saved` status, which might be usefull if updated with `save_all=True` all related models are saved, regardless of `saved` status, which might be useful if updated
models comes from api call, so are not changed in backend. models comes from api call, so are not changed in backend.
* `dict()` method previously included only directly related models or nested models if they were not nullable and not virtual,
now all related models not previosuly visited without loops are included in `dict()`. This should be not breaking
as just more data will be dumped to dict, but it should not be missing.
## Fixes ## Fixes

View File

@ -138,10 +138,8 @@ class ExcludableMixin(RelationMixin):
return columns return columns
@classmethod @classmethod
def _update_excluded_with_related_not_required( def _update_excluded_with_related(
cls, cls, exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None],
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None],
nested: bool = False,
) -> Union[Set, Dict]: ) -> Union[Set, Dict]:
""" """
Used during generation of the dict(). Used during generation of the dict().
@ -159,8 +157,9 @@ class ExcludableMixin(RelationMixin):
:rtype: Union[Set, Dict] :rtype: Union[Set, Dict]
""" """
exclude = exclude or {} exclude = exclude or {}
related_set = cls._exclude_related_names_not_required(nested=nested) related_set = cls.extract_related_names()
if isinstance(exclude, set): if isinstance(exclude, set):
exclude = {s for s in exclude}
exclude.union(related_set) exclude.union(related_set)
else: else:
related_dict = translate_list_to_dict(related_set) related_dict = translate_list_to_dict(related_set)

View File

@ -111,27 +111,6 @@ class RelationMixin:
} }
return related_names return related_names
@classmethod
def _exclude_related_names_not_required(cls, nested: bool = False) -> Set:
"""
Returns a set of non mandatory related models field names.
For a main model (not nested) only nullable related field names are returned,
for nested models all related models are returned.
:param nested: flag setting nested models (child of previous one, not main one)
:type nested: bool
:return: set of non mandatory related fields
:rtype: Set
"""
if nested:
return cls.extract_related_names()
related_names = cls.extract_related_names()
related_names = {
name for name in related_names if cls.Meta.model_fields[name].nullable
}
return related_names
@classmethod @classmethod
def _iterate_related_models( # noqa: CCR001 def _iterate_related_models( # noqa: CCR001
cls, node_list: NodeList = None, source_relation: str = None cls, node_list: NodeList = None, source_relation: str = None

View File

@ -64,7 +64,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
the logic concerned with database connection and data persistance. the logic concerned with database connection and data persistance.
""" """
__slots__ = ("_orm_id", "_orm_saved", "_orm", "_pk_column") __slots__ = ("_orm_id", "_orm_saved", "_orm", "_pk_column", "__pk_only__")
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
pk: Any pk: Any
@ -134,6 +134,8 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
) )
pk_only = kwargs.pop("__pk_only__", False) pk_only = kwargs.pop("__pk_only__", False)
object.__setattr__(self, "__pk_only__", pk_only)
excluded: Set[str] = kwargs.pop("__excluded__", set()) excluded: Set[str] = kwargs.pop("__excluded__", set())
if "pk" in kwargs: if "pk" in kwargs:
@ -343,8 +345,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
or (self.pk == other.pk and self.pk is not None) or (self.pk == other.pk and self.pk is not None)
or ( or (
(self.pk is None and other.pk is None) (self.pk is None and other.pk is None)
and self.dict(exclude=self.extract_related_names()) and {
== other.dict(exclude=other.extract_related_names()) k: v
for k, v in self.__dict__.items()
if k not in self.extract_related_names()
}
== {
k: v
for k, v in other.__dict__.items()
if k not in other.extract_related_names()
}
) )
) )
@ -496,6 +506,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
@staticmethod @staticmethod
def _extract_nested_models_from_list( def _extract_nested_models_from_list(
relation_map: Dict,
models: MutableSequence, models: MutableSequence,
include: Union[Set, Dict, None], include: Union[Set, Dict, None],
exclude: Union[Set, Dict, None], exclude: Union[Set, Dict, None],
@ -516,14 +527,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
for model in models: for model in models:
try: try:
result.append( result.append(
model.dict(nested=True, include=include, exclude=exclude,) model.dict(
relation_map=relation_map, include=include, exclude=exclude,
)
) )
except ReferenceError: # pragma no cover except ReferenceError: # pragma no cover
continue continue
return result return result
def _skip_ellipsis( def _skip_ellipsis(
self, items: Union[Set, Dict, None], key: str self, items: Union[Set, Dict, None], key: str, default_return: Any = None
) -> Union[Set, Dict, None]: ) -> Union[Set, Dict, None]:
""" """
Helper to traverse the include/exclude dictionaries. Helper to traverse the include/exclude dictionaries.
@ -538,11 +551,11 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
:rtype: Union[Set, Dict, None] :rtype: Union[Set, Dict, None]
""" """
result = self.get_child(items, key) result = self.get_child(items, key)
return result if result is not Ellipsis else None return result if result is not Ellipsis else default_return
def _extract_nested_models( # noqa: CCR001 def _extract_nested_models( # noqa: CCR001
self, self,
nested: bool, relation_map: Dict,
dict_instance: Dict, dict_instance: Dict,
include: Optional[Dict], include: Optional[Dict],
exclude: Optional[Dict], exclude: Optional[Dict],
@ -566,18 +579,23 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
fields = self._get_related_not_excluded_fields(include=include, exclude=exclude) fields = self._get_related_not_excluded_fields(include=include, exclude=exclude)
for field in fields: for field in fields:
if self.Meta.model_fields[field].virtual and nested: if not relation_map or field not in relation_map:
continue continue
nested_model = getattr(self, field) nested_model = getattr(self, field)
if isinstance(nested_model, MutableSequence): if isinstance(nested_model, MutableSequence):
dict_instance[field] = self._extract_nested_models_from_list( dict_instance[field] = self._extract_nested_models_from_list(
relation_map=self._skip_ellipsis(
relation_map, field, default_return=dict() # type: ignore
),
models=nested_model, models=nested_model,
include=self._skip_ellipsis(include, field), include=self._skip_ellipsis(include, field),
exclude=self._skip_ellipsis(exclude, field), exclude=self._skip_ellipsis(exclude, field),
) )
elif nested_model is not None: elif nested_model is not None:
dict_instance[field] = nested_model.dict( dict_instance[field] = nested_model.dict(
nested=True, relation_map=self._skip_ellipsis(
relation_map, field, default_return=dict()
),
include=self._skip_ellipsis(include, field), include=self._skip_ellipsis(include, field),
exclude=self._skip_ellipsis(exclude, field), exclude=self._skip_ellipsis(exclude, field),
) )
@ -595,7 +613,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
exclude_unset: bool = False, exclude_unset: bool = False,
exclude_defaults: bool = False, exclude_defaults: bool = False,
exclude_none: bool = False, exclude_none: bool = False,
nested: bool = False, relation_map: Dict = None,
) -> "DictStrAny": # noqa: A003' ) -> "DictStrAny": # noqa: A003'
""" """
@ -620,14 +638,14 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
:type exclude_defaults: bool :type exclude_defaults: bool
:param exclude_none: flag to exclude None values - passed to pydantic :param exclude_none: flag to exclude None values - passed to pydantic
:type exclude_none: bool :type exclude_none: bool
:param nested: flag if the current model is nested :param relation_map: map of the relations to follow to avoid circural deps
:type nested: bool :type relation_map: Dict
:return: :return:
:rtype: :rtype:
""" """
dict_instance = super().dict( dict_instance = super().dict(
include=include, include=include,
exclude=self._update_excluded_with_related_not_required(exclude, nested), exclude=self._update_excluded_with_related(exclude),
by_alias=by_alias, by_alias=by_alias,
skip_defaults=skip_defaults, skip_defaults=skip_defaults,
exclude_unset=exclude_unset, exclude_unset=exclude_unset,
@ -640,12 +658,19 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
if exclude and isinstance(exclude, Set): if exclude and isinstance(exclude, Set):
exclude = translate_list_to_dict(exclude) exclude = translate_list_to_dict(exclude)
dict_instance = self._extract_nested_models( relation_map = (
nested=nested, relation_map
dict_instance=dict_instance, if relation_map is not None
include=include, # type: ignore else translate_list_to_dict(self._iterate_related_models())
exclude=exclude, # type: ignore
) )
pk_only = object.__getattribute__(self, "__pk_only__")
if relation_map and not pk_only:
dict_instance = self._extract_nested_models(
relation_map=relation_map,
dict_instance=dict_instance,
include=include, # type: ignore
exclude=exclude, # type: ignore
)
# include model properties as fields in dict # include model properties as fields in dict
if object.__getattribute__(self, "Meta").property_fields: if object.__getattribute__(self, "Meta").property_fields:
@ -721,7 +746,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
:rtype: Dict :rtype: Dict
""" """
related_names = self.extract_related_names() related_names = self.extract_related_names()
self_fields = self.dict(exclude=related_names) self_fields = {k: v for k, v in self.__dict__.items() if k not in related_names}
return self_fields return self_fields
def _extract_model_db_fields(self) -> Dict: def _extract_model_db_fields(self) -> Dict:

View File

@ -48,7 +48,11 @@ def test_read_main():
) )
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == { assert response.json() == {
"category": {"id": None, "name": "test cat"}, "category": {
"id": None,
"items": [{"id": 1, "name": "test"}],
"name": "test cat",
},
"id": 1, "id": 1,
"name": "test", "name": "test",
} }