fill docstrings on mixins

This commit is contained in:
collerek
2020-12-31 11:52:05 +01:00
parent 101ea57879
commit c4ff69b683
6 changed files with 276 additions and 0 deletions

View File

@ -2,6 +2,10 @@ from typing import Dict, TYPE_CHECKING
class AliasMixin: class AliasMixin:
"""
Used to translate field names into database column names.
"""
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from ormar import ModelMeta from ormar import ModelMeta

View File

@ -25,6 +25,10 @@ if TYPE_CHECKING: # pragma no cover
class ExcludableMixin(RelationMixin): class ExcludableMixin(RelationMixin):
"""
Used to include/exclude given set of fields on models during load and dict() calls.
"""
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from ormar import Model from ormar import Model
@ -32,6 +36,16 @@ class ExcludableMixin(RelationMixin):
def get_child( def get_child(
items: Union[Set, Dict, None], key: str = None items: Union[Set, Dict, None], key: str = None
) -> Union[Set, Dict, None]: ) -> Union[Set, Dict, None]:
"""
Used to get nested dictionaries keys if they exists otherwise returns
passed items.
:param items: bag of items to include or exclude
:type items: Union[Set, Dict, None]
:param key: name of the child to extract
:type key: str
:return: child extracted from items if exists
:rtype: Union[Set, Dict, None]
"""
if isinstance(items, dict): if isinstance(items, dict):
return items.get(key, {}) return items.get(key, {})
return items return items
@ -40,16 +54,46 @@ class ExcludableMixin(RelationMixin):
def get_excluded( def get_excluded(
exclude: Union[Set, Dict, None], key: str = None exclude: Union[Set, Dict, None], key: str = None
) -> Union[Set, Dict, None]: ) -> Union[Set, Dict, None]:
"""
Proxy to ExcludableMixin.get_child for exclusions.
:param exclude: bag of items to exclude
:type exclude: Union[Set, Dict, None]
:param key: name of the child to extract
:type key: str
:return: child extracted from items if exists
:rtype: Union[Set, Dict, None]
"""
return ExcludableMixin.get_child(items=exclude, key=key) return ExcludableMixin.get_child(items=exclude, key=key)
@staticmethod @staticmethod
def get_included( def get_included(
include: Union[Set, Dict, None], key: str = None include: Union[Set, Dict, None], key: str = None
) -> Union[Set, Dict, None]: ) -> Union[Set, Dict, None]:
"""
Proxy to ExcludableMixin.get_child for inclusions.
:param include: bag of items to include
:type include: Union[Set, Dict, None]
:param key: name of the child to extract
:type key: str
:return: child extracted from items if exists
:rtype: Union[Set, Dict, None]
"""
return ExcludableMixin.get_child(items=include, key=key) return ExcludableMixin.get_child(items=include, key=key)
@staticmethod @staticmethod
def is_excluded(exclude: Union[Set, Dict, None], key: str = None) -> bool: def is_excluded(exclude: Union[Set, Dict, None], key: str = None) -> bool:
"""
Checks if given key should be excluded on model/ dict.
:param exclude: bag of items to exclude
:type exclude: Union[Set, Dict, None]
:param key: name of the child to extract
:type key: str
:return: child extracted from items if exists
:rtype: Union[Set, Dict, None]
"""
if exclude is None: if exclude is None:
return False return False
if exclude is Ellipsis: # pragma: nocover if exclude is Ellipsis: # pragma: nocover
@ -63,6 +107,16 @@ class ExcludableMixin(RelationMixin):
@staticmethod @staticmethod
def is_included(include: Union[Set, Dict, None], key: str = None) -> bool: def is_included(include: Union[Set, Dict, None], key: str = None) -> bool:
"""
Checks if given key should be included on model/ dict.
:param include: bag of items to include
:type include: Union[Set, Dict, None]
:param key: name of the child to extract
:type key: str
:return: child extracted from items if exists
:rtype: Union[Set, Dict, None]
"""
if include is None: if include is None:
return True return True
if include is Ellipsis: if include is Ellipsis:
@ -78,6 +132,19 @@ class ExcludableMixin(RelationMixin):
def _populate_pk_column( def _populate_pk_column(
model: Type["Model"], columns: List[str], use_alias: bool = False, model: Type["Model"], columns: List[str], use_alias: bool = False,
) -> List[str]: ) -> List[str]:
"""
Adds primary key column/alias (depends on use_alias flag) to list of
column names that are selected.
:param model: model on columns are selected
:type model: Type["Model"]
:param columns: list of columns names
:type columns: List[str]
:param use_alias: flag to set if aliases or field names should be used
:type use_alias: bool
:return: list of columns names with pk column in it
:rtype: List[str]
"""
pk_alias = ( pk_alias = (
model.get_column_alias(model.Meta.pkname) model.get_column_alias(model.Meta.pkname)
if use_alias if use_alias
@ -95,6 +162,26 @@ class ExcludableMixin(RelationMixin):
exclude_fields: Optional[Union[Set, Dict]], exclude_fields: Optional[Union[Set, Dict]],
use_alias: bool = False, use_alias: bool = False,
) -> List[str]: ) -> List[str]:
"""
Returns list of aliases or field names for given model.
Aliases/names switch is use_alias flag.
If provided only fields included in fields will be returned.
If provided fields in exclude_fields will be excluded in return.
Primary key field is always added and cannot be excluded (will be added anyway).
:param model: model on columns are selected
:type model: Type["Model"]
:param fields: set/dict of fields to include
:type fields: Optional[Union[Set, Dict]]
:param exclude_fields: set/dict of fields to exclude
:type exclude_fields: Optional[Union[Set, Dict]]
:param use_alias: flag if aliases or field names should be used
:type use_alias: bool
:return: list of column field names or aliases
:rtype: List[str]
"""
columns = [ columns = [
model.get_column_name_from_alias(col.name) if not use_alias else col.name model.get_column_name_from_alias(col.name) if not use_alias else col.name
for col in model.Meta.table.columns for col in model.Meta.table.columns
@ -129,6 +216,21 @@ class ExcludableMixin(RelationMixin):
exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None], exclude: Union["AbstractSetIntStr", "MappingIntStrAny", None],
nested: bool = False, nested: bool = False,
) -> Union[Set, Dict]: ) -> Union[Set, Dict]:
"""
Used during generation of the dict().
To avoid cyclical references and max recurrence limit nested models have to
exclude related models that are not mandatory.
For a main model (not nested) only nullable related field names are added to
exclusion, for nested models all related models are excluded.
:param exclude: set/dict with fields to exclude
:type exclude: Union[Set, Dict, None]
:param nested: flag setting nested models (child of previous one, not main one)
:type nested: bool
:return: set or dict with excluded fields added.
:rtype: Union[Set, Dict]
"""
exclude = exclude or {} exclude = exclude or {}
related_set = cls._exclude_related_names_not_required(nested=nested) related_set = cls._exclude_related_names_not_required(nested=nested)
if isinstance(exclude, set): if isinstance(exclude, set):
@ -144,6 +246,23 @@ class ExcludableMixin(RelationMixin):
fields: Optional[Union[Dict, Set]] = None, fields: Optional[Union[Dict, Set]] = None,
exclude_fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None,
) -> Set: ) -> Set:
"""
Returns a set of models field names that should be explicitly excluded
during model initialization.
Those fields will be set to None to avoid ormar/pydantic setting default
values on them. They should be returned as None in any case.
Used in parsing data from database rows that construct Models by initializing
them with dicts constructed from those db rows.
:param fields: set/dict of fields to include
:type fields: Optional[Union[Set, Dict]]
:param exclude_fields: set/dict of fields to exclude
:type exclude_fields: Optional[Union[Set, Dict]]
:return: set of field names that should be excluded
:rtype: Set
"""
fields_names = cls.extract_db_own_fields() fields_names = cls.extract_db_own_fields()
if fields and fields is not Ellipsis: if fields and fields is not Ellipsis:
fields_to_keep = {name for name in fields if name in fields_names} fields_to_keep = {name for name in fields if name in fields_names}

View File

@ -8,8 +8,28 @@ if TYPE_CHECKING: # pragma no cover
class MergeModelMixin: class MergeModelMixin:
"""
Used to merge models instances returned by database,
but already initialized to ormar Models.keys
Models can duplicate during joins when parent model has multiple child rows,
in the end all parent (main) models should be unique.
"""
@classmethod @classmethod
def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]: def merge_instances_list(cls, result_rows: Sequence["Model"]) -> Sequence["Model"]:
"""
Merges a list of models into list of unique models.
Models can duplicate during joins when parent model has multiple child rows,
in the end all parent (main) models should be unique.
:param result_rows: list of already initialized Models with child models
populated, each instance is one row in db and some models can duplicate
:type result_rows: List["Model"]
:return: list of merged models where each main model is unique
:rtype: List["Model"]
"""
merged_rows: List["Model"] = [] merged_rows: List["Model"] = []
grouped_instances: OrderedDict = OrderedDict() grouped_instances: OrderedDict = OrderedDict()
@ -27,6 +47,19 @@ class MergeModelMixin:
@classmethod @classmethod
def merge_two_instances(cls, one: "Model", other: "Model") -> "Model": def merge_two_instances(cls, one: "Model", other: "Model") -> "Model":
"""
Merges current (other) Model and previous one (one) and returns the current
Model instance with data merged from previous one.
If needed it's calling itself recurrently and merges also children models.
:param one: previous model instance
:type one: Model
:param other: current model instance
:type other: Model
:return: current Model instance with data merged from previous one.
:rtype: Model
"""
for field in one.Meta.model_fields.keys(): for field in one.Meta.model_fields.keys():
current_field = getattr(one, field) current_field = getattr(one, field)
if isinstance(current_field, list) and not isinstance( if isinstance(current_field, list) and not isinstance(

View File

@ -6,6 +6,10 @@ from ormar.models.mixins.relation_mixin import RelationMixin
class PrefetchQueryMixin(RelationMixin): class PrefetchQueryMixin(RelationMixin):
"""
Used in PrefetchQuery to extract ids and names of models to prefetch.
"""
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
from ormar import Model from ormar import Model
@ -18,6 +22,20 @@ class PrefetchQueryMixin(RelationMixin):
reverse: bool, reverse: bool,
related: str, related: str,
) -> Tuple[Type["Model"], str]: ) -> Tuple[Type["Model"], str]:
"""
Returns Model on which query clause should be performed and name of the column.
:param parent_model: related model that the relation lead to
:type parent_model: Type[Model]
:param target_model: model on which query should be perfomed
:type target_model: Type[Model]
:param reverse: flag if the relation is reverse
:type reverse: bool
:param related: name of the relation field
:type related: str
:return: Model on which query clause should be performed and name of the column
:rtype: Tuple[Type[Model], str]
"""
if reverse: if reverse:
field_name = ( field_name = (
parent_model.Meta.model_fields[related].related_name parent_model.Meta.model_fields[related].related_name
@ -36,6 +54,22 @@ class PrefetchQueryMixin(RelationMixin):
def get_column_name_for_id_extraction( def get_column_name_for_id_extraction(
parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool, parent_model: Type["Model"], reverse: bool, related: str, use_raw: bool,
) -> str: ) -> str:
"""
Returns name of the column that should be used to extract ids from model.
Depending on the relation side it's either primary key column of parent model
or field name specified by related parameter.
:param parent_model: model from which id column should be extracted
:type parent_model: Type[Model]
:param reverse: flag if the relation is reverse
:type reverse: bool
:param related: name of the relation field
:type related: str
:param use_raw: flag if aliases or field names should be used
:type use_raw: bool
:return:
:rtype:
"""
if reverse: if reverse:
column_name = parent_model.Meta.pkname column_name = parent_model.Meta.pkname
return ( return (
@ -46,6 +80,16 @@ class PrefetchQueryMixin(RelationMixin):
@classmethod @classmethod
def get_related_field_name(cls, target_field: Type["BaseField"]) -> str: def get_related_field_name(cls, target_field: Type["BaseField"]) -> str:
"""
Returns name of the relation field that should be used in prefetch query.
This field is later used to register relation in prefetch query,
populate relations dict, and populate nested model in prefetch query.
:param target_field: relation field that should be used in prefetch
:type target_field: Type[BaseField]
:return: name of the field
:rtype: str
"""
if issubclass(target_field, ormar.fields.ManyToManyField): if issubclass(target_field, ormar.fields.ManyToManyField):
return cls.get_name() return cls.get_name()
if target_field.virtual: if target_field.virtual:
@ -54,6 +98,20 @@ class PrefetchQueryMixin(RelationMixin):
@classmethod @classmethod
def get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List: def get_filtered_names_to_extract(cls, prefetch_dict: Dict) -> List:
"""
Returns list of related fields names that should be followed to prefetch related
models from.
List of models is translated into dict to assure each model is extracted only
once in one query, that's why this function accepts prefetch_dict not list.
Only relations from current model are returned.
:param prefetch_dict: dictionary of fields to extract
:type prefetch_dict: Dict
:return: list of fields names to extract
:rtype: List
"""
related_to_extract = [] related_to_extract = []
if prefetch_dict and prefetch_dict is not Ellipsis: if prefetch_dict and prefetch_dict is not Ellipsis:
related_to_extract = [ related_to_extract = [

View File

@ -5,6 +5,10 @@ from ormar.fields.foreign_key import ForeignKeyField
class RelationMixin: class RelationMixin:
"""
Used to return relation fields/names etc. from given model
"""
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
from ormar import ModelMeta from ormar import ModelMeta
@ -14,6 +18,12 @@ class RelationMixin:
@classmethod @classmethod
def extract_db_own_fields(cls) -> Set: def extract_db_own_fields(cls) -> Set:
"""
Returns only fields that are stored in the own database table, exclude all
related fields.
:return: set of model fields with relation fields excluded
:rtype: Set
"""
related_names = cls.extract_related_names() related_names = cls.extract_related_names()
self_fields = { self_fields = {
name for name in cls.Meta.model_fields.keys() if name not in related_names name for name in cls.Meta.model_fields.keys() if name not in related_names
@ -22,7 +32,13 @@ class RelationMixin:
@classmethod @classmethod
def extract_related_fields(cls) -> List: def extract_related_fields(cls) -> List:
"""
Returns List of ormar Fields for all relations declared on a model.
List is cached in cls._related_fields for quicker access.
:return: list of related fields
:rtype: List
"""
if isinstance(cls._related_fields, List): if isinstance(cls._related_fields, List):
return cls._related_fields return cls._related_fields
@ -35,7 +51,13 @@ class RelationMixin:
@classmethod @classmethod
def extract_related_names(cls) -> Set: def extract_related_names(cls) -> Set:
"""
Returns List of fields names for all relations declared on a model.
List is cached in cls._related_names for quicker access.
:return: list of related fields names
:rtype: List
"""
if isinstance(cls._related_names, Set): if isinstance(cls._related_names, Set):
return cls._related_names return cls._related_names
@ -49,6 +71,12 @@ class RelationMixin:
@classmethod @classmethod
def _extract_db_related_names(cls) -> Set: def _extract_db_related_names(cls) -> Set:
"""
Returns only fields that are stored in the own database table, exclude
related fields that are not stored as foreign keys on given model.
:return: set of model fields with non fk relation fields excluded
:rtype: Set
"""
related_names = cls.extract_related_names() related_names = cls.extract_related_names()
related_names = { related_names = {
name name
@ -59,6 +87,17 @@ class RelationMixin:
@classmethod @classmethod
def _exclude_related_names_not_required(cls, nested: bool = False) -> Set: 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: if nested:
return cls.extract_related_names() return cls.extract_related_names()
related_names = cls.extract_related_names() related_names = cls.extract_related_names()

View File

@ -6,8 +6,21 @@ from ormar.models.mixins.relation_mixin import RelationMixin
class SavePrepareMixin(RelationMixin): class SavePrepareMixin(RelationMixin):
"""
Used to prepare models to be saved in database
"""
@classmethod @classmethod
def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001 def substitute_models_with_pks(cls, model_dict: Dict) -> Dict: # noqa CCR001
"""
Receives dictionary of model that is about to be saved and changes all related
models that are stored as foreign keys to their fk value.
:param model_dict: dictionary of model that is about to be saved
:type model_dict: Dict
:return: dictionary of model that is about to be saved
:rtype: Dict
"""
for field in cls.extract_related_names(): for field in cls.extract_related_names():
field_value = model_dict.get(field, None) field_value = model_dict.get(field, None)
if field_value is not None: if field_value is not None:
@ -34,6 +47,16 @@ class SavePrepareMixin(RelationMixin):
@classmethod @classmethod
def populate_default_values(cls, new_kwargs: Dict) -> Dict: def populate_default_values(cls, new_kwargs: Dict) -> Dict:
"""
Receives dictionary of model that is about to be saved and populates the default
value on the fields that have the default value set, but no actual value was
passed by the user.
:param new_kwargs: dictionary of model that is about to be saved
:type new_kwargs: Dict
:return: dictionary of model that is about to be saved
:rtype: Dict
"""
for field_name, field in cls.Meta.model_fields.items(): for field_name, field in cls.Meta.model_fields.items():
if ( if (
field_name not in new_kwargs field_name not in new_kwargs