add some docstrings in the model, fix quickstart

This commit is contained in:
collerek
2020-12-16 16:15:33 +01:00
parent 4de030b5e0
commit c096e6dbbd
3 changed files with 138 additions and 4 deletions

View File

@ -130,11 +130,11 @@ album = await Album.objects.select_related("tracks").all()
assert len(album.tracks) == 3 assert len(album.tracks) == 3
# Fetch instances, with a filter across an FK relationship. # 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 assert len(tracks) == 2
# Fetch instances, with a filter and operator across an FK relationship. # 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 assert len(tracks) == 2
# Limit a query # Limit a query

View File

@ -130,11 +130,11 @@ album = await Album.objects.select_related("tracks").all()
assert len(album.tracks) == 3 assert len(album.tracks) == 3
# Fetch instances, with a filter across an FK relationship. # 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 assert len(tracks) == 2
# Fetch instances, with a filter and operator across an FK relationship. # 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 assert len(tracks) == 2
# Limit a query # Limit a query

View File

@ -168,7 +168,32 @@ class Model(NewBaseModel):
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,
) -> dict: ) -> 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 # databases does not keep aliases in Record for postgres, change to raw row
source = row._row if cls.db_backend_name() == "postgresql" else row source = row._row if cls.db_backend_name() == "postgresql" else row
@ -190,11 +215,41 @@ class Model(NewBaseModel):
return item return item
async def upsert(self: T, **kwargs: Any) -> T: 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: if not self.pk:
return await self.save() return await self.save()
return await self.update(**kwargs) return await self.update(**kwargs)
async def save(self: T) -> T: 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() self_fields = self._extract_model_db_fields()
if not self.pk and self.Meta.model_fields[self.Meta.pkname].autoincrement: 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 async def save_related( # noqa: CCR001
self, follow: bool = False, visited: Set = None, update_count: int = 0 self, follow: bool = False, visited: Set = None, update_count: int = 0
) -> int: # noqa: CCR001 ) -> 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: if not visited:
visited = {self.__class__} visited = {self.__class__}
else: else:
@ -263,6 +342,22 @@ class Model(NewBaseModel):
async def _update_and_follow( async def _update_and_follow(
rel: T, follow: bool, visited: Set, update_count: int rel: T, follow: bool, visited: Set, update_count: int
) -> Tuple[int, Set]: ) -> 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: if follow and rel.__class__ not in visited:
update_count = await rel.save_related( update_count = await rel.save_related(
follow=follow, visited=visited, update_count=update_count follow=follow, visited=visited, update_count=update_count
@ -273,6 +368,21 @@ class Model(NewBaseModel):
return update_count, visited return update_count, visited
async def update(self: T, **kwargs: Any) -> T: 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: if kwargs:
self.update_from_dict(kwargs) self.update_from_dict(kwargs)
@ -294,6 +404,20 @@ class Model(NewBaseModel):
return self return self
async def delete(self: T) -> int: 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) await self.signals.pre_delete.send(sender=self.__class__, instance=self)
expr = self.Meta.table.delete() expr = self.Meta.table.delete()
expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname))) expr = expr.where(self.pk_column == (getattr(self, self.Meta.pkname)))
@ -303,6 +427,16 @@ class Model(NewBaseModel):
return result return result
async def load(self: T) -> T: 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) expr = self.Meta.table.select().where(self.pk_column == self.pk)
row = await self.Meta.database.fetch_one(expr) row = await self.Meta.database.fetch_one(expr)
if not row: # pragma nocover if not row: # pragma nocover