switch dict() to include relations comming from _iterate_related_models and not only nested not nullable ones
This commit is contained in:
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user