From c096e6dbbd6b6b9f881455a8e76da9a05e666872 Mon Sep 17 00:00:00 2001 From: collerek Date: Wed, 16 Dec 2020 16:15:33 +0100 Subject: [PATCH] add some docstrings in the model, fix quickstart --- README.md | 4 +- docs/index.md | 4 +- ormar/models/model.py | 134 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3d4546f..ea70cfa 100644 --- a/README.md +++ b/README.md @@ -130,11 +130,11 @@ album = await Album.objects.select_related("tracks").all() assert len(album.tracks) == 3 # Fetch instances, with a filter across an FK relationship. -tracks = Track.objects.filter(album__name="Fantasies") +tracks = await Track.objects.filter(album__name="Fantasies").all() assert len(tracks) == 2 # Fetch instances, with a filter and operator across an FK relationship. -tracks = Track.objects.filter(album__name__iexact="fantasies") +tracks = await Track.objects.filter(album__name__iexact="fantasies").all() assert len(tracks) == 2 # Limit a query diff --git a/docs/index.md b/docs/index.md index 3d4546f..ea70cfa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -130,11 +130,11 @@ album = await Album.objects.select_related("tracks").all() assert len(album.tracks) == 3 # Fetch instances, with a filter across an FK relationship. -tracks = Track.objects.filter(album__name="Fantasies") +tracks = await Track.objects.filter(album__name="Fantasies").all() assert len(tracks) == 2 # Fetch instances, with a filter and operator across an FK relationship. -tracks = Track.objects.filter(album__name__iexact="fantasies") +tracks = await Track.objects.filter(album__name__iexact="fantasies").all() assert len(tracks) == 2 # Limit a query diff --git a/ormar/models/model.py b/ormar/models/model.py index 03a00bd..4e45bc3 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -168,7 +168,32 @@ class Model(NewBaseModel): fields: Optional[Union[Dict, Set]] = None, exclude_fields: Optional[Union[Dict, Set]] = None, ) -> dict: + """ + Extracts own fields from raw sql result, using a given prefix. + Prefix changes depending on the table's position in a join. + If the table is a main table, there is no prefix. + All joined tables have prefixes to allow duplicate column names, + as well as duplicated joins to the same table from multiple different tables. + + Extracted fields populates the item dict that is later used to construct a Model. + + :param item: dictionary of already populated nested models, otherwise empty dict + :type item: Dict + :param row: raw result row from the database + :type row: sqlalchemy.engine.result.ResultProxy + :param table_prefix: prefix of the table from AliasManager + each pair of tables have own prefix (two of them depending on direction) - used in joins + to allow multiple joins to the same table. + :type table_prefix: str + :param fields: fields and related model fields to include - if provided only those are included + :type fields: Optional[Union[Dict, Set]] + :param exclude_fields: fields and related model fields to exclude + excludes the fields even if they are provided in fields + :type exclude_fields: Optional[Union[Dict, Set]] + :return: dictionary with keys corresponding to model fields names and values are database values + :rtype: Dict + """ # databases does not keep aliases in Record for postgres, change to raw row source = row._row if cls.db_backend_name() == "postgresql" else row @@ -190,11 +215,41 @@ class Model(NewBaseModel): return item async def upsert(self: T, **kwargs: Any) -> T: + """ + Performs either a save or an update depending on the presence of the primary key. + If the pk field is filled it's an update, otherwise the save is performed. + For save kwargs are ignored, used only in update if provided. + + :param kwargs: list of fields to update + :type kwargs: Any + :return: saved Model + :rtype: Model + """ if not self.pk: return await self.save() return await self.update(**kwargs) async def save(self: T) -> T: + """ + Performs a save of given Model instance. + If primary key is already saved, db backend will throw integrity error. + + Related models are saved by pk number, reverse relation and many to many fields + are not saved - use corresponding relations methods. + + If there are fields with server_default set and those fields are not already filled + save will trigger also a second query to refreshed the fields populated server side. + + Does not recognize if model was previously saved. If you want to perform update or + insert depending on the pk fields presence use upsert. + + Sends pre_save and post_save signals. + + Sets model save status to True. + + :return: saved Model + :rtype: Model + """ self_fields = self._extract_model_db_fields() if not self.pk and self.Meta.model_fields[self.Meta.pkname].autoincrement: @@ -233,6 +288,30 @@ class Model(NewBaseModel): async def save_related( # noqa: CCR001 self, follow: bool = False, visited: Set = None, update_count: int = 0 ) -> int: # noqa: CCR001 + """ + Triggers a upsert method on all related models if the instances are not already saved. + By default saves only the directly related ones. + + If follow=True is set it saves also related models of related models. + + To not get stuck in an infinite loop as related models also keep a relation + to parent model visited models set is kept. + + That way already visited models that are nested are saved, but the save do not + follow them inside. So Model A -> Model B -> Model A -> Model C will save second + Model A but will never follow into Model C. Nested relations of those kind need to + be persisted manually. + + :param follow: flag to trigger deep save - by default only directly related models are saved + with follow=True also related models of related models are saved + :type follow: bool + :param visited: internal parameter for recursive calls - already visited models + :type visited: Set + :param update_count: internal parameter for recursive calls - no uf updated instances + :type update_count: int + :return: number of updated/saved models + :rtype: int + """ if not visited: visited = {self.__class__} else: @@ -263,6 +342,22 @@ class Model(NewBaseModel): async def _update_and_follow( rel: T, follow: bool, visited: Set, update_count: int ) -> Tuple[int, Set]: + """ + Internal method used in save_related to follow related models and update numbers + of updated related instances. + + :param rel: Model to follow + :type rel: Model + :param follow: flag to trigger deep save - by default only directly related models are saved + with follow=True also related models of related models are saved + :type follow: bool + :param visited: internal parameter for recursive calls - already visited models + :type visited: Set + :param update_count: internal parameter for recursive calls - no uf updated instances + :type update_count: int + :return: tuple of update count and visited + :rtype: Tuple[int, Set] + """ if follow and rel.__class__ not in visited: update_count = await rel.save_related( follow=follow, visited=visited, update_count=update_count @@ -273,6 +368,21 @@ class Model(NewBaseModel): return update_count, visited async def update(self: T, **kwargs: Any) -> T: + """ + Performs update of Model instance in the database. + Fields can be updated before or you can pass them as kwargs. + + Sends pre_update and post_update signals. + + Sets model save status to True. + + :raises: If the pk column is not set will throw ModelPersistenceError + + :param kwargs: list of fields to update as field=value pairs + :type kwargs: Any + :return: updated Model + :rtype: Model + """ if kwargs: self.update_from_dict(kwargs) @@ -294,6 +404,20 @@ class Model(NewBaseModel): return self async def delete(self: T) -> int: + """ + Removes the Model instance from the database. + + Sends pre_delete and post_delete signals. + + Sets model save status to False. + + Note it does not delete the Model itself (python object). + So you can delete and later save (since pk is deleted no conflict will arise) + or update and the Model will be saved in database again. + + :return: number of deleted rows (for some backends) + :rtype: int + """ await self.signals.pre_delete.send(sender=self.__class__, instance=self) expr = self.Meta.table.delete() expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname))) @@ -303,6 +427,16 @@ class Model(NewBaseModel): return result async def load(self: T) -> T: + """ + Allow to refresh existing Models fields from database. + Be careful as the related models can be overwritten by pk_only models during load. + Does NOT refresh the related models fields if they were loaded before. + + :raises: If given primary key is not found in database the NoMatch exception is raised. + + :return: reloaded Model + :rtype: Model + """ expr = self.Meta.table.select().where(self.pk_column == self.pk) row = await self.Meta.database.fetch_one(expr) if not row: # pragma nocover