refactor methaclass functions into helper files, add docstrings

This commit is contained in:
collerek
2020-12-17 15:45:06 +01:00
parent c096e6dbbd
commit e98300233e
15 changed files with 923 additions and 468 deletions

View File

@ -199,9 +199,12 @@ class BaseField(FieldInfo):
@classmethod @classmethod
def construct_contraints(cls) -> List: def construct_contraints(cls) -> List:
return [sqlalchemy.schema.ForeignKey( return [
sqlalchemy.schema.ForeignKey(
con.name, ondelete=con.ondelete, onupdate=con.onupdate con.name, ondelete=con.ondelete, onupdate=con.onupdate
) for con in cls.constraints] )
for con in cls.constraints
]
@classmethod @classmethod
def get_column(cls, name: str) -> sqlalchemy.Column: def get_column(cls, name: str) -> sqlalchemy.Column:

View File

@ -2,7 +2,6 @@ import uuid
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, List, Optional, TYPE_CHECKING, Type, Union from typing import Any, List, Optional, TYPE_CHECKING, Type, Union
import sqlalchemy
from pydantic import BaseModel, create_model from pydantic import BaseModel, create_model
from sqlalchemy import UniqueConstraint from sqlalchemy import UniqueConstraint
@ -140,7 +139,9 @@ def ForeignKey( # noqa CFQ002
name=kwargs.pop("real_name", None), name=kwargs.pop("real_name", None),
nullable=nullable, nullable=nullable,
constraints=[ constraints=[
ForeignKeyConstraint(name=fk_string, ondelete=ondelete, onupdate=onupdate) ForeignKeyConstraint(
name=fk_string, ondelete=ondelete, onupdate=onupdate # type: ignore
)
], ],
unique=unique, unique=unique,
column_type=to_field.column_type, column_type=to_field.column_type,

View File

@ -1,5 +1,4 @@
from ormar.models.newbasemodel import NewBaseModel # noqa I100 from ormar.models.newbasemodel import NewBaseModel # noqa I100
from ormar.models.model import Model # noqa I100 from ormar.models.model import Model # noqa I100
from ormar.models.metaclass import expand_reverse_relationships # noqa I100
__all__ = ["NewBaseModel", "Model", "expand_reverse_relationships"] __all__ = ["NewBaseModel", "Model"]

View File

View File

@ -0,0 +1,39 @@
from typing import Dict, List, Optional, TYPE_CHECKING, Type
from ormar import ModelDefinitionError
from ormar.fields.foreign_key import ForeignKeyField
if TYPE_CHECKING: # pragma no cover
from ormar import Model
def validate_related_names_in_relations(
model_fields: Dict, new_model: Type["Model"]
) -> None:
"""
Performs a validation of relation_names in relation fields.
If multiple fields are leading to the same related model
only one can have empty related_name param
(populated by default as model.name.lower()+'s').
Also related_names have to be unique for given related model.
:raises: ModelDefinitionError if validation of related_names fail
:param model_fields: dictionary of declared ormar model fields
:type model_fields: Dict[str, ormar.Field]
:param new_model:
:type new_model: Model class
"""
already_registered: Dict[str, List[Optional[str]]] = dict()
for field in model_fields.values():
if issubclass(field, ForeignKeyField):
previous_related_names = already_registered.setdefault(field.to, [])
if field.related_name in previous_related_names:
raise ModelDefinitionError(
f"Multiple fields declared on {new_model.get_name(lower=False)} "
f"model leading to {field.to.get_name(lower=False)} model without "
f"related_name property set. \nThere can be only one relation with "
f"default/empty name: '{new_model.get_name() + 's'}'"
f"\nTip: provide different related_name for FK and/or M2M fields"
)
else:
previous_related_names.append(field.related_name)

View File

@ -0,0 +1,221 @@
import warnings
from typing import Dict, Optional, TYPE_CHECKING, Tuple, Type
from pydantic import BaseConfig
from pydantic.fields import ModelField
from pydantic.utils import lenient_issubclass
import ormar # noqa: I100, I202
from ormar.fields import BaseField, ManyToManyField
if TYPE_CHECKING: # pragma no cover
from ormar import Model
def reverse_field_not_already_registered(
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
) -> bool:
"""
Checks if child is already registered in parents pydantic fields.
:param child: related Model class
:type child: ormar.models.metaclass.ModelMetaclass
:param child_model_name: related_name of the child if provided
:type child_model_name: str
:param parent_model: parent Model class
:type parent_model: ormar.models.metaclass.ModelMetaclass
:return: result of the check
:rtype: bool
"""
return (
child_model_name not in parent_model.__fields__
and child.get_name() not in parent_model.__fields__
)
def create_pydantic_field(
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
) -> None:
"""
Registers pydantic field on through model that leads to passed model
and is registered as field_name passed.
Through model is fetched from through attributed on passed model_field.
:param field_name: field name to register
:type field_name: str
:param model: type of field to register
:type model: Model class
:param model_field: relation field from which through model is extracted
:type model_field: ManyToManyField class
"""
model_field.through.__fields__[field_name] = ModelField(
name=field_name,
type_=model,
model_config=model.__config__,
required=False,
class_validators={},
)
def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField":
"""
Extracts field type and if it's required from Model model_fields by passed
field_name. Returns a pydantic field with type of field_name field type.
:param field_name: field name to fetch from Model and name of pydantic field
:type field_name: str
:param model: type of field to register
:type model: Model class
:return: newly created pydantic field
:rtype: pydantic.ModelField
"""
return ModelField(
name=field_name,
type_=model.Meta.model_fields[field_name].__type__, # type: ignore
model_config=model.__config__,
required=not model.Meta.model_fields[field_name].nullable,
class_validators={},
)
def populate_default_pydantic_field_value(
ormar_field: Type[BaseField], field_name: str, attrs: dict
) -> dict:
"""
Grabs current value of the ormar Field in class namespace
(so the default_value declared on ormar model if set)
and converts it to pydantic.FieldInfo
that pydantic is able to extract later.
On FieldInfo there are saved all needed params like max_length of the string
and other constraints that pydantic can use to build
it's own field validation used by ormar.
:param ormar_field: field to convert
:type ormar_field: ormar Field
:param field_name: field to convert name
:type field_name: str
:param attrs: current class namespace
:type attrs: Dict
:return: updated namespace dict
:rtype: Dict
"""
curr_def_value = attrs.get(field_name, ormar.Undefined)
if lenient_issubclass(curr_def_value, ormar.fields.BaseField):
curr_def_value = ormar.Undefined
if curr_def_value is None:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info(allow_null=True)
else:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info()
return attrs
def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
"""
Extracts ormar fields from annotations (deprecated) and from namespace
dictionary of the class. Fields declared on model are all subclasses of the
BaseField class.
Trigger conversion of ormar field into pydantic FieldInfo, which has all needed
paramaters saved.
Overwrites the annotations of ormar fields to corresponding types declared on
ormar fields (constructed dynamically for relations).
Those annotations are later used by pydantic to construct it's own fields.
:param attrs: current class namespace
:type attrs: Dict
:return: namespace of the class updated, dict of extracted model_fields
:rtype: Tuple[Dict, Dict]
"""
model_fields = {}
potential_fields = {
k: v
for k, v in attrs["__annotations__"].items()
if lenient_issubclass(v, BaseField)
}
if potential_fields:
warnings.warn(
"Using ormar.Fields as type Model annotation has been deprecated,"
" check documentation of current version",
DeprecationWarning,
)
potential_fields.update(get_potential_fields(attrs))
for field_name, field in potential_fields.items():
field.name = field_name
attrs = populate_default_pydantic_field_value(field, field_name, attrs)
model_fields[field_name] = field
attrs["__annotations__"][field_name] = (
field.__type__ if not field.nullable else Optional[field.__type__]
)
return attrs, model_fields
def get_pydantic_base_orm_config() -> Type[BaseConfig]:
"""
Returns empty pydantic Config with orm_mode set to True.
:return: empty default config with orm_mode set.
:rtype: pydantic Config
"""
class Config(BaseConfig):
orm_mode = True
return Config
def populate_default_options_values(
new_model: Type["Model"], model_fields: Dict
) -> None:
"""
Sets all optional Meta values to it's defaults
and set model_fields that were already previously extracted.
Here should live all options that are not overwritten/set for all models.
Current options are:
* constraints = []
* abstract = False
:param new_model: newly constructed Model
:type new_model: Model class
:param model_fields:
:type model_fields: Union[Dict[str, type], Dict]
"""
if not hasattr(new_model.Meta, "constraints"):
new_model.Meta.constraints = []
if not hasattr(new_model.Meta, "model_fields"):
new_model.Meta.model_fields = model_fields
if not hasattr(new_model.Meta, "abstract"):
new_model.Meta.abstract = False
def get_potential_fields(attrs: Dict) -> Dict:
"""
Gets all the fields in current class namespace that are Fields.
:param attrs: current class namespace
:type attrs: Dict
:return: extracted fields that are ormar Fields
:rtype: Dict
"""
return {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
def extract_annotations_and_default_vals(attrs: Dict) -> Tuple[Dict, Dict]:
"""
Extracts annotations from class namespace dict and triggers
extraction of ormar model_fields.
:param attrs: namespace of the class created
:type attrs: Dict
:return: namespace of the class updated, dict of extracted model_fields
:rtype: Tuple[Dict, Dict]
"""
key = "__annotations__"
attrs[key] = attrs.get(key, {})
attrs, model_fields = populate_pydantic_default_values(attrs)
return attrs, model_fields

View File

@ -0,0 +1,138 @@
from typing import TYPE_CHECKING, Type
from ormar import ForeignKey, ManyToMany
from ormar.fields import ManyToManyField
from ormar.fields.foreign_key import ForeignKeyField
from ormar.models.helpers.pydantic import reverse_field_not_already_registered
from ormar.models.helpers.sqlalchemy import adjust_through_many_to_many_model
from ormar.relations import AliasManager
if TYPE_CHECKING: # pragma no cover
from ormar import Model
alias_manager = AliasManager()
def register_relation_on_build(new_model: Type["Model"], field_name: str) -> None:
"""
Registers ForeignKey relation in alias_manager to set a table_prefix.
Registration include also reverse relation side to be able to join both sides.
Relation is registered by model name and relation field name to allow for multiple
relations between two Models that needs to have different
aliases for proper sql joins.
:param new_model: constructed model
:type new_model: Model class
:param field_name: name of the related field
:type field_name: str
"""
alias_manager.add_relation_type(new_model, field_name)
def register_many_to_many_relation_on_build(
new_model: Type["Model"], field: Type[ManyToManyField]
) -> None:
"""
Registers connection between through model and both sides of the m2m relation.
Registration include also reverse relation side to be able to join both sides.
Relation is registered by model name and relation field name to allow for multiple
relations between two Models that needs to have different
aliases for proper sql joins.
By default relation name is a model.name.lower().
:param new_model: model on which m2m field is declared
:type new_model: Model class
:param field: relation field
:type field: ManyToManyField class
"""
alias_manager.add_relation_type(field.through, new_model.get_name(), is_multi=True)
alias_manager.add_relation_type(field.through, field.to.get_name(), is_multi=True)
def expand_reverse_relationships(model: Type["Model"]) -> None:
"""
Iterates through model_fields of given model and verifies if all reverse
relation have been populated on related models.
If the reverse relation has not been set before it's set here.
:param model: model on which relation should be checked and registered
:type model: Model class
"""
for model_field in model.Meta.model_fields.values():
if issubclass(model_field, ForeignKeyField):
child_model_name = model_field.related_name or model.get_name() + "s"
parent_model = model_field.to
child = model
if reverse_field_not_already_registered(
child, child_model_name, parent_model
):
register_reverse_model_fields(
parent_model, child, child_model_name, model_field
)
def register_reverse_model_fields(
model: Type["Model"],
child: Type["Model"],
related_name: str,
model_field: Type["ForeignKeyField"],
) -> None:
"""
Registers reverse ForeignKey field on related model.
By default it's name.lower()+'s' of the model on which relation is defined.
But if the related_model name is provided it's registered with that name.
Autogenerated reverse fields also set related_name to the original field name.
:param model: related model on which reverse field should be defined
:type model: Model class
:param child: parent model with relation definition
:type child: Model class
:param related_name: name by which reverse key should be registered
:type related_name: str
:param model_field: original relation ForeignKey field
:type model_field: relation Field
"""
if issubclass(model_field, ManyToManyField):
model.Meta.model_fields[related_name] = ManyToMany(
child,
through=model_field.through,
name=related_name,
virtual=True,
related_name=model_field.name,
)
# register foreign keys on through model
adjust_through_many_to_many_model(model, child, model_field)
else:
model.Meta.model_fields[related_name] = ForeignKey(
child, real_name=related_name, virtual=True, related_name=model_field.name,
)
def register_relation_in_alias_manager(
new_model: Type["Model"], field: Type[ForeignKeyField], field_name: str
) -> None:
"""
Registers the relation (and reverse relation) in alias manager.
The m2m relations require registration of through model between
actual end models of the relation.
Delegates the actual registration to:
m2m - register_many_to_many_relation_on_build
fk - register_relation_on_build
:param new_model: model on which relation field is declared
:type new_model: Model class
:param field: relation field
:type field: ForeignKey or ManyToManyField class
:param field_name: name of the relation key
:type field_name: str
"""
if issubclass(field, ManyToManyField):
register_many_to_many_relation_on_build(new_model=new_model, field=field)
elif issubclass(field, ForeignKeyField):
register_relation_on_build(new_model=new_model, field_name=field_name)

View File

@ -0,0 +1,208 @@
import logging
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Type
import sqlalchemy
from ormar import ForeignKey, Integer, ModelDefinitionError # noqa: I202
from ormar.fields import BaseField, ManyToManyField
from ormar.models.helpers.models import validate_related_names_in_relations
from ormar.models.helpers.pydantic import create_pydantic_field
if TYPE_CHECKING: # pragma no cover
from ormar import Model
def adjust_through_many_to_many_model(
model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField]
) -> None:
"""
Registers m2m relation on through model.
Sets ormar.ForeignKey from through model to both child and parent models.
Sets sqlalchemy.ForeignKey to both child and parent models.
Sets pydantic fields with child and parent model types.
:param model: model on which relation is declared
:type model: Model class
:param child: model to which m2m relation leads
:type child: Model class
:param model_field: relation field defined in parent model
:type model_field: ManyToManyField
"""
model_field.through.Meta.model_fields[model.get_name()] = ForeignKey(
model, real_name=model.get_name(), ondelete="CASCADE"
)
model_field.through.Meta.model_fields[child.get_name()] = ForeignKey(
child, real_name=child.get_name(), ondelete="CASCADE"
)
create_and_append_m2m_fk(model, model_field)
create_and_append_m2m_fk(child, model_field)
create_pydantic_field(model.get_name(), model, model_field)
create_pydantic_field(child.get_name(), child, model_field)
def create_and_append_m2m_fk(
model: Type["Model"], model_field: Type[ManyToManyField]
) -> None:
"""
Registers sqlalchemy Column with sqlalchemy.ForeignKey leadning to the model.
Newly created field is added to m2m relation through model Meta columns and table.
:param model: Model class to which FK should be created
:type model: Model class
:param model_field: field with ManyToMany relation
:type model_field: ManyToManyField field
"""
column = sqlalchemy.Column(
model.get_name(),
model.Meta.table.columns.get(model.get_column_alias(model.Meta.pkname)).type,
sqlalchemy.schema.ForeignKey(
model.Meta.tablename + "." + model.get_column_alias(model.Meta.pkname),
ondelete="CASCADE",
onupdate="CASCADE",
),
)
model_field.through.Meta.columns.append(column)
model_field.through.Meta.table.append_column(column)
def check_pk_column_validity(
field_name: str, field: BaseField, pkname: Optional[str]
) -> Optional[str]:
"""
Receives the field marked as primary key and verifies if the pkname
was not already set (only one allowed per model) and if field is not marked
as pydantic_only as it needs to be a database field.
:raises: ModelDefintionError if pkname already set or field is pydantic_only
:param field_name: name of field
:type field_name: str
:param field: ormar.Field
:type field: BaseField
:param pkname: already set pkname
:type pkname: Optional[str]
:return: name of the field that should be set as pkname
:rtype: str
"""
if pkname is not None:
raise ModelDefinitionError("Only one primary key column is allowed.")
if field.pydantic_only:
raise ModelDefinitionError("Primary key column cannot be pydantic only")
return field_name
def sqlalchemy_columns_from_model_fields(
model_fields: Dict, new_model: Type["Model"]
) -> Tuple[Optional[str], List[sqlalchemy.Column]]:
"""
Iterates over declared on Model model fields and extracts fields that
should be treated as database fields.
If the model is empty it sets mandatory id field as primary key
(used in through models in m2m relations).
Triggers a validation of relation_names in relation fields. If multiple fields
are leading to the same related model only one can have empty related_name param.
Also related_names have to be unique.
Trigger validation of primary_key - only one and required pk can be set,
cannot be pydantic_only.
Append fields to columns if it's not pydantic_only,
virtual ForeignKey or ManyToMany field.
:raises: ModelDefinitionError if validation of related_names fail,
or pkname validation fails.
:param model_fields: dictionary of declared ormar model fields
:type model_fields: Dict[str, ormar.Field]
:param new_model:
:type new_model: Model class
:return: pkname, list of sqlalchemy columns
:rtype: Tuple[Optional[str], List[sqlalchemy.Column]]
"""
columns = []
pkname = None
if len(model_fields.keys()) == 0:
model_fields["id"] = Integer(name="id", primary_key=True)
logging.warning(
"Table {table_name} had no fields so auto "
"Integer primary key named `id` created."
)
validate_related_names_in_relations(model_fields, new_model)
for field_name, field in model_fields.items():
if field.primary_key:
pkname = check_pk_column_validity(field_name, field, pkname)
if (
not field.pydantic_only
and not field.virtual
and not issubclass(field, ManyToManyField)
):
columns.append(field.get_column(field.get_alias()))
return pkname, columns
def populate_meta_tablename_columns_and_pk(
name: str, new_model: Type["Model"]
) -> Type["Model"]:
"""
Sets Model tablename if it's not already set in Meta.
Default tablename if not present is class name lower + s (i.e. Bed becomes -> beds)
Checks if Model's Meta have pkname and columns set.
If not calls the sqlalchemy_columns_from_model_fields to populate
columns from ormar.fields definitions.
:raises: if pkname is not present raises ModelDefinitionError.
Each model has to have pk.
:param name: name of the current Model
:type name: str
:param new_model: currently constructed Model
:type new_model: ormar.models.metaclass.ModelMetaclass
:return: Model with populated pkname and columns in Meta
:rtype: ormar.models.metaclass.ModelMetaclass
"""
tablename = name.lower() + "s"
new_model.Meta.tablename = (
new_model.Meta.tablename if hasattr(new_model.Meta, "tablename") else tablename
)
pkname: Optional[str]
if hasattr(new_model.Meta, "columns"):
columns = new_model.Meta.table.columns
pkname = new_model.Meta.pkname
else:
pkname, columns = sqlalchemy_columns_from_model_fields(
new_model.Meta.model_fields, new_model
)
if pkname is None:
raise ModelDefinitionError("Table has to have a primary key.")
new_model.Meta.columns = columns
new_model.Meta.pkname = pkname
return new_model
def populate_meta_sqlalchemy_table_if_required(
new_model: Type["Model"],
) -> Type["Model"]:
"""
Constructs sqlalchemy table out of columns and parameters set on Meta class.
It populates name, metadata, columns and constraints.
:param new_model: class without sqlalchemy table constructed
:type new_model: Model class
:return: class with populated Meta.table
:rtype: Model class
"""
if not hasattr(new_model.Meta, "table"):
new_model.Meta.table = sqlalchemy.Table(
new_model.Meta.tablename,
new_model.Meta.metadata,
*new_model.Meta.columns,
*new_model.Meta.constraints,
)
return new_model

View File

@ -1,6 +1,3 @@
import copy
import logging
import warnings
from typing import ( from typing import (
Any, Any,
Dict, Dict,
@ -11,21 +8,36 @@ from typing import (
Tuple, Tuple,
Type, Type,
Union, Union,
cast,
) )
import databases import databases
import pydantic import pydantic
import sqlalchemy import sqlalchemy
from pydantic import BaseConfig from pydantic.fields import FieldInfo
from pydantic.fields import FieldInfo, ModelField
from pydantic.utils import lenient_issubclass
from sqlalchemy.sql.schema import ColumnCollectionConstraint from sqlalchemy.sql.schema import ColumnCollectionConstraint
import ormar # noqa I100 import ormar # noqa I100
from ormar import ForeignKey, Integer, ModelDefinitionError # noqa I100 from ormar import ForeignKey, Integer, ModelDefinitionError # noqa I100
from ormar.fields import BaseField from ormar.fields import BaseField
from ormar.fields.foreign_key import ForeignKeyField from ormar.fields.foreign_key import ForeignKeyField
from ormar.fields.many_to_many import ManyToMany, ManyToManyField from ormar.fields.many_to_many import ManyToManyField
from ormar.models.helpers.pydantic import (
extract_annotations_and_default_vals,
get_potential_fields,
get_pydantic_base_orm_config,
get_pydantic_field,
populate_default_options_values,
)
from ormar.models.helpers.relations import (
alias_manager,
register_relation_in_alias_manager,
)
from ormar.models.helpers.relations import expand_reverse_relationships
from ormar.models.helpers.sqlalchemy import (
populate_meta_sqlalchemy_table_if_required,
populate_meta_tablename_columns_and_pk,
)
from ormar.models.quick_access_views import quick_access_set from ormar.models.quick_access_views import quick_access_set
from ormar.queryset import QuerySet from ormar.queryset import QuerySet
from ormar.relations.alias_manager import AliasManager from ormar.relations.alias_manager import AliasManager
@ -34,7 +46,6 @@ from ormar.signals import Signal, SignalEmitter
if TYPE_CHECKING: # pragma no cover if TYPE_CHECKING: # pragma no cover
from ormar import Model from ormar import Model
alias_manager = AliasManager()
PARSED_FIELDS_KEY = "__parsed_fields__" PARSED_FIELDS_KEY = "__parsed_fields__"
CONFIG_KEY = "Config" CONFIG_KEY = "Config"
@ -56,295 +67,6 @@ class ModelMeta:
abstract: bool abstract: bool
def register_relation_on_build_new(new_model: Type["Model"], field_name: str) -> None:
alias_manager.add_relation_type_new(new_model, field_name)
def register_many_to_many_relation_on_build_new(
new_model: Type["Model"], field: Type[ManyToManyField]
) -> None:
alias_manager.add_relation_type_new(
field.through, new_model.get_name(), is_multi=True
)
alias_manager.add_relation_type_new(
field.through, field.to.get_name(), is_multi=True
)
def reverse_field_not_already_registered(
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
) -> bool:
return (
child_model_name not in parent_model.__fields__
and child.get_name() not in parent_model.__fields__
)
def expand_reverse_relationships(model: Type["Model"]) -> None:
for model_field in model.Meta.model_fields.values():
if issubclass(model_field, ForeignKeyField):
child_model_name = model_field.related_name or model.get_name() + "s"
parent_model = model_field.to
child = model
if reverse_field_not_already_registered(
child, child_model_name, parent_model
):
register_reverse_model_fields(
parent_model, child, child_model_name, model_field
)
def register_reverse_model_fields(
model: Type["Model"],
child: Type["Model"],
child_model_name: str,
model_field: Type["ForeignKeyField"],
) -> None:
if issubclass(model_field, ManyToManyField):
model.Meta.model_fields[child_model_name] = ManyToMany(
child,
through=model_field.through,
name=child_model_name,
virtual=True,
related_name=model_field.name,
)
# register foreign keys on through model
adjust_through_many_to_many_model(model, child, model_field, child_model_name)
else:
model.Meta.model_fields[child_model_name] = ForeignKey(
child,
real_name=child_model_name,
virtual=True,
related_name=model_field.name,
)
def adjust_through_many_to_many_model(
model: Type["Model"],
child: Type["Model"],
model_field: Type[ManyToManyField],
child_model_name: str,
) -> None:
model_field.through.Meta.model_fields[model.get_name()] = ForeignKey(
model, real_name=model.get_name(), ondelete="CASCADE"
)
model_field.through.Meta.model_fields[child.get_name()] = ForeignKey(
child, real_name=child.get_name(), ondelete="CASCADE"
)
create_and_append_m2m_fk(model, model_field)
create_and_append_m2m_fk(child, model_field)
create_pydantic_field(model.get_name(), model, model_field)
create_pydantic_field(child.get_name(), child, model_field)
def create_pydantic_field(
field_name: str, model: Type["Model"], model_field: Type[ManyToManyField]
) -> None:
model_field.through.__fields__[field_name] = ModelField(
name=field_name,
type_=model,
model_config=model.__config__,
required=False,
class_validators={},
)
def get_pydantic_field(field_name: str, model: Type["Model"]) -> "ModelField":
return ModelField(
name=field_name,
type_=model.Meta.model_fields[field_name].__type__, # type: ignore
model_config=model.__config__,
required=not model.Meta.model_fields[field_name].nullable,
class_validators={},
)
def create_and_append_m2m_fk(
model: Type["Model"], model_field: Type[ManyToManyField]
) -> None:
column = sqlalchemy.Column(
model.get_name(),
model.Meta.table.columns.get(model.get_column_alias(model.Meta.pkname)).type,
sqlalchemy.schema.ForeignKey(
model.Meta.tablename + "." + model.get_column_alias(model.Meta.pkname),
ondelete="CASCADE",
onupdate="CASCADE",
),
)
model_field.through.Meta.columns.append(column)
model_field.through.Meta.table.append_column(column)
def check_pk_column_validity(
field_name: str, field: BaseField, pkname: Optional[str]
) -> Optional[str]:
if pkname is not None:
raise ModelDefinitionError("Only one primary key column is allowed.")
if field.pydantic_only:
raise ModelDefinitionError("Primary key column cannot be pydantic only")
return field_name
def validate_related_names_in_relations(
model_fields: Dict, new_model: Type["Model"]
) -> None:
already_registered: Dict[str, List[Optional[str]]] = dict()
for field in model_fields.values():
if issubclass(field, ForeignKeyField):
previous_related_names = already_registered.setdefault(field.to, [])
if field.related_name in previous_related_names:
raise ModelDefinitionError(
f"Multiple fields declared on {new_model.get_name(lower=False)} "
f"model leading to {field.to.get_name(lower=False)} model without "
f"related_name property set. \nThere can be only one relation with "
f"default/empty name: '{new_model.get_name() + 's'}'"
f"\nTip: provide different related_name for FK and/or M2M fields"
)
else:
previous_related_names.append(field.related_name)
def sqlalchemy_columns_from_model_fields(
model_fields: Dict, new_model: Type["Model"]
) -> Tuple[Optional[str], List[sqlalchemy.Column]]:
columns = []
pkname = None
if len(model_fields.keys()) == 0:
model_fields["id"] = Integer(name="id", primary_key=True)
logging.warning(
"Table {table_name} had no fields so auto "
"Integer primary key named `id` created."
)
validate_related_names_in_relations(model_fields, new_model)
for field_name, field in model_fields.items():
if field.primary_key:
pkname = check_pk_column_validity(field_name, field, pkname)
if (
not field.pydantic_only
and not field.virtual
and not issubclass(field, ManyToManyField)
):
columns.append(field.get_column(field.get_alias()))
return pkname, columns
def register_relation_in_alias_manager_new(
new_model: Type["Model"], field: Type[ForeignKeyField], field_name: str
) -> None:
if issubclass(field, ManyToManyField):
register_many_to_many_relation_on_build_new(new_model=new_model, field=field)
elif issubclass(field, ForeignKeyField):
register_relation_on_build_new(new_model=new_model, field_name=field_name)
def populate_default_pydantic_field_value(
ormar_field: Type[BaseField], field_name: str, attrs: dict
) -> dict:
curr_def_value = attrs.get(field_name, ormar.Undefined)
if lenient_issubclass(curr_def_value, ormar.fields.BaseField):
curr_def_value = ormar.Undefined
if curr_def_value is None:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info(allow_null=True)
else:
attrs[field_name] = ormar_field.convert_to_pydantic_field_info()
return attrs
def populate_pydantic_default_values(attrs: Dict) -> Tuple[Dict, Dict]:
model_fields = {}
potential_fields = {
k: v
for k, v in attrs["__annotations__"].items()
if lenient_issubclass(v, BaseField)
}
if potential_fields:
warnings.warn(
"Using ormar.Fields as type Model annotation has been deprecated,"
" check documentation of current version",
DeprecationWarning,
)
potential_fields.update(get_potential_fields(attrs))
for field_name, field in potential_fields.items():
field.name = field_name
attrs = populate_default_pydantic_field_value(field, field_name, attrs)
model_fields[field_name] = field
attrs["__annotations__"][field_name] = (
field.__type__ if not field.nullable else Optional[field.__type__]
)
return attrs, model_fields
def extract_annotations_and_default_vals(attrs: dict) -> Tuple[Dict, Dict]:
key = "__annotations__"
attrs[key] = attrs.get(key, {})
attrs, model_fields = populate_pydantic_default_values(attrs)
return attrs, model_fields
def populate_meta_tablename_columns_and_pk(
name: str, new_model: Type["Model"]
) -> Type["Model"]:
tablename = name.lower() + "s"
new_model.Meta.tablename = (
new_model.Meta.tablename if hasattr(new_model.Meta, "tablename") else tablename
)
pkname: Optional[str]
if hasattr(new_model.Meta, "columns"):
columns = new_model.Meta.table.columns
pkname = new_model.Meta.pkname
else:
pkname, columns = sqlalchemy_columns_from_model_fields(
new_model.Meta.model_fields, new_model
)
if pkname is None:
raise ModelDefinitionError("Table has to have a primary key.")
new_model.Meta.columns = columns
new_model.Meta.pkname = pkname
return new_model
def populate_meta_sqlalchemy_table_if_required(
new_model: Type["Model"],
) -> Type["Model"]:
"""
Constructs sqlalchemy table out of columns and parameters set on Meta class.
It populates name, metadata, columns and constraints.
:param new_model: class without sqlalchemy table constructed
:type new_model: Model class
:return: class with populated Meta.table
:rtype: Model class
"""
if not hasattr(new_model.Meta, "table"):
new_model.Meta.table = sqlalchemy.Table(
new_model.Meta.tablename,
new_model.Meta.metadata,
*new_model.Meta.columns,
*new_model.Meta.constraints,
)
return new_model
def get_pydantic_base_orm_config() -> Type[BaseConfig]:
"""
Returns empty pydantic Config with orm_mode set to True.
:return: empty default config with orm_mode set.
:rtype: pydantic Config
"""
class Config(BaseConfig):
orm_mode = True
return Config
def check_if_field_has_choices(field: Type[BaseField]) -> bool: def check_if_field_has_choices(field: Type[BaseField]) -> bool:
""" """
Checks if given field has choices populated. Checks if given field has choices populated.
@ -400,32 +122,6 @@ def populate_choices_validators(model: Type["Model"]) -> None: # noqa CCR001
model.__pre_root_validators__ = validators model.__pre_root_validators__ = validators
def populate_default_options_values(
new_model: Type["Model"], model_fields: Dict
) -> None:
"""
Sets all optional Meta values to it's defaults
and set model_fields that were already previously extracted.
Here should live all options that are not overwritten/set for all models.
Current options are:
* constraints = []
* abstract = False
:param new_model: newly constructed Model
:type new_model: Model class
:param model_fields:
:type model_fields: Union[Dict[str, type], Dict]
"""
if not hasattr(new_model.Meta, "constraints"):
new_model.Meta.constraints = []
if not hasattr(new_model.Meta, "model_fields"):
new_model.Meta.model_fields = model_fields
if not hasattr(new_model.Meta, "abstract"):
new_model.Meta.abstract = False
def add_cached_properties(new_model: Type["Model"]) -> None: def add_cached_properties(new_model: Type["Model"]) -> None:
""" """
Sets cached properties for both pydantic and ormar models. Sets cached properties for both pydantic and ormar models.
@ -510,18 +206,6 @@ def register_signals(new_model: Type["Model"]) -> None: # noqa: CCR001
new_model.Meta.signals = signals new_model.Meta.signals = signals
def get_potential_fields(attrs: Dict) -> Dict:
"""
Gets all the fields in current class namespace that are Fields.
:param attrs: current class namespace
:type attrs: Dict
:return: extracted fields that are ormar Fields
:rtype: Dict
"""
return {k: v for k, v in attrs.items() if lenient_issubclass(v, BaseField)}
def check_conflicting_fields( def check_conflicting_fields(
new_fields: Set, new_fields: Set,
attrs: Dict, attrs: Dict,
@ -612,6 +296,78 @@ def update_attrs_from_base_meta( # noqa: CCR001
setattr(attrs["Meta"], param, parent_value) setattr(attrs["Meta"], param, parent_value)
def copy_data_from_parent_model( # noqa: CCR001
base_class: Type["Model"],
curr_class: type,
attrs: Dict,
model_fields: Dict[
str, Union[Type[BaseField], Type[ForeignKeyField], Type[ManyToManyField]]
],
) -> Tuple[Dict, Dict]:
"""
Copy the key parameters [databse, metadata, property_fields and constraints]
and fields from parent models. Overwrites them if needed.
Only abstract classes can be subclassed.
Since relation fields requires different related_name for different children
:raises: ModelDefinitionError if non abstract model is subclassed
:param base_class: one of the parent classes
:type base_class: Model or model parent class
:param curr_class: current constructed class
:type curr_class: Model or model parent class
:param attrs: new namespace for class being constructed
:type attrs: Dict
:param model_fields: ormar fields in defined in current class
:type model_fields: Dict[str, BaseField]
:return: updated attrs and model_fields
:rtype: Tuple[Dict, Dict]
"""
if attrs.get("Meta"):
new_fields = set(base_class.Meta.model_fields.keys()) # type: ignore
previous_fields = set({k for k, v in attrs.items() if isinstance(v, FieldInfo)})
check_conflicting_fields(
new_fields=new_fields,
attrs=attrs,
base_class=base_class,
curr_class=curr_class,
previous_fields=previous_fields,
)
if previous_fields and not base_class.Meta.abstract: # type: ignore
raise ModelDefinitionError(
f"{curr_class.__name__} cannot inherit "
f"from non abstract class {base_class.__name__}"
)
update_attrs_from_base_meta(
base_class=base_class, # type: ignore
attrs=attrs,
)
parent_fields = dict()
meta = attrs.get("Meta")
if not meta: # pragma: no cover
raise ModelDefinitionError(
f"Model {curr_class.__name__} declared without Meta"
)
table_name = (
meta.tablename
if hasattr(meta, "tablename") and meta.tablename
else attrs.get("__name__", "").lower() + "s"
)
for field_name, field in base_class.Meta.model_fields.items():
if issubclass(field, ForeignKeyField) and field.related_name:
copy_field = type(field.__name__, (field,), dict(field.__dict__))
related_name = field.related_name + "_" + table_name
copy_field.related_name = related_name # type: ignore
parent_fields[field_name] = copy_field
else:
parent_fields[field_name] = field
model_fields.update(parent_fields) # type: ignore
return attrs, model_fields
def extract_from_parents_definition( # noqa: CCR001 def extract_from_parents_definition( # noqa: CCR001
base_class: type, base_class: type,
curr_class: type, curr_class: type,
@ -644,40 +400,13 @@ def extract_from_parents_definition( # noqa: CCR001
:rtype: Tuple[Dict, Dict] :rtype: Tuple[Dict, Dict]
""" """
if hasattr(base_class, "Meta"): if hasattr(base_class, "Meta"):
if attrs.get("Meta"): base_class = cast(Type["Model"], base_class)
new_fields = set(base_class.Meta.model_fields.keys()) # type: ignore return copy_data_from_parent_model(
previous_fields = set(
{k for k, v in attrs.items() if isinstance(v, FieldInfo)}
)
check_conflicting_fields(
new_fields=new_fields,
attrs=attrs,
base_class=base_class, base_class=base_class,
curr_class=curr_class, curr_class=curr_class,
previous_fields=previous_fields,
)
if previous_fields and not base_class.Meta.abstract: # type: ignore
raise ModelDefinitionError(
f"{curr_class.__name__} cannot inherit "
f"from non abstract class {base_class.__name__}"
)
update_attrs_from_base_meta(
base_class=base_class, # type: ignore
attrs=attrs, attrs=attrs,
model_fields=model_fields,
) )
parent_fields = dict()
table_name = attrs.get("Meta").tablename if hasattr(attrs.get("Meta"), "tablename") else attrs.get(
'__name__').lower() + 's'
for field_name, field in base_class.Meta.model_fields.items():
if issubclass(field, ForeignKeyField) and field.related_name:
copy_field = type(field.__name__, (field,), dict(field.__dict__))
copy_field.related_name = field.related_name + '_' + table_name
parent_fields[field_name] = copy_field
else:
parent_fields[field_name] = field
model_fields.update(parent_fields) # type: ignore
return attrs, model_fields
key = "__annotations__" key = "__annotations__"
if hasattr(base_class, PARSED_FIELDS_KEY): if hasattr(base_class, PARSED_FIELDS_KEY):
@ -736,6 +465,35 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
def __new__( # type: ignore # noqa: CCR001 def __new__( # type: ignore # noqa: CCR001
mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict mcs: "ModelMetaclass", name: str, bases: Any, attrs: dict
) -> "ModelMetaclass": ) -> "ModelMetaclass":
"""
Metaclass used by ormar Models that performs configuration
and build of ormar Models.
Sets pydantic configuration.
Extract model_fields and convert them to pydantic FieldInfo,
updates class namespace.
Extracts settings and fields from parent classes.
Fetches methods decorated with @property_field decorator
to expose them later in dict().
Construct parent pydantic Metaclass/ Model.
If class has Meta class declared (so actual ormar Models) it also:
* populate sqlalchemy columns, pkname and tables from model_fields
* register reverse relationships on related models
* registers all relations in alias manager that populates table_prefixes
* exposes alias manager on each Model
* creates QuerySet for each model and exposes it on a class
:param name: name of current class
:type name: str
:param bases: base classes
:type bases: Tuple
:param attrs: class namespace
:type attrs: Dict
"""
attrs["Config"] = get_pydantic_base_orm_config() attrs["Config"] = get_pydantic_base_orm_config()
attrs["__name__"] = name attrs["__name__"] = name
attrs, model_fields = extract_annotations_and_default_vals(attrs) attrs, model_fields = extract_annotations_and_default_vals(attrs)
@ -760,7 +518,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
new_model = populate_meta_sqlalchemy_table_if_required(new_model) new_model = populate_meta_sqlalchemy_table_if_required(new_model)
expand_reverse_relationships(new_model) expand_reverse_relationships(new_model)
for field_name, field in new_model.Meta.model_fields.items(): for field_name, field in new_model.Meta.model_fields.items():
register_relation_in_alias_manager_new(new_model, field, field_name) register_relation_in_alias_manager(new_model, field, field_name)
if new_model.Meta.pkname not in attrs["__annotations__"]: if new_model.Meta.pkname not in attrs["__annotations__"]:
field_name = new_model.Meta.pkname field_name = new_model.Meta.pkname

View File

@ -22,6 +22,20 @@ from ormar.models.metaclass import ModelMeta
def group_related_list(list_: List) -> Dict: def group_related_list(list_: List) -> Dict:
"""
Translates the list of related strings into a dictionary.
That way nested models are grouped to traverse them in a right order
and to avoid repetition.
Sample: ["people__houses", "people__cars__models", "people__cars__colors"]
will become:
{'people': {'houses': [], 'cars': ['models', 'colors']}}
:param list_: list of related models used in select related
:type list_: List[str]
:return: list converted to dictionary to avoid repetition and group nested models
:rtype: Dict[str, List]
"""
test_dict: Dict[str, Any] = dict() test_dict: Dict[str, Any] = dict()
grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0]) grouped = itertools.groupby(list_, key=lambda x: x.split("__")[0])
for key, group in grouped: for key, group in grouped:
@ -63,7 +77,38 @@ 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,
) -> Optional[T]: ) -> Optional[T]:
"""
Model method to convert raw sql row from database into ormar.Model instance.
Traverses nested models if they were specified in select_related for query.
Called recurrently and returns model instance if it's present in the row.
Note that it's processing one row at a time, so if there are duplicates of
parent row that needs to be joined/combined
(like parent row in sql join with 2+ child rows)
instances populated in this method are later combined in the QuerySet.
Other method working directly on raw database results is in prefetch_query,
where rows are populated in a different way as they do not have
nested models in result.
:param row: raw result row from the database
:type row: sqlalchemy.engine.result.ResultProxy
:param select_related: list of names of related models fetched from database
:type select_related: List
:param related_models: list or dict of related models
:type related_models: Union[List, Dict]
:param previous_model: internal param for nested models to specify table_prefix
:type previous_model: Model class
:param related_name: internal parameter - name of current nested model
:type related_name: 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: returns model if model is populated from database
:rtype: Optional[Model]
"""
item: Dict[str, Any] = {} item: Dict[str, Any] = {}
select_related = select_related or [] select_related = select_related or []
related_models = related_models or [] related_models = related_models or []
@ -86,7 +131,7 @@ class Model(NewBaseModel):
previous_model = through_field.through # type: ignore previous_model = through_field.through # type: ignore
if previous_model and rel_name2: if previous_model and rel_name2:
table_prefix = cls.Meta.alias_manager.resolve_relation_join_new( table_prefix = cls.Meta.alias_manager.resolve_relation_join(
previous_model, rel_name2 previous_model, rel_name2
) )
else: else:
@ -127,6 +172,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:
"""
Traverses structure of related models and populates the nested models
from the database row.
Related models can be a list if only directly related models are to be
populated, converted to dict if related models also have their own related
models to be populated.
Recurrently calls from_row method on nested instances and create nested
instances. In the end those instances are added to the final model dictionary.
: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 related_models: list or dict of related models
:type related_models: Union[Dict, List]
: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
"""
for related in related_models: for related in related_models:
if isinstance(related_models, dict) and related_models[related]: if isinstance(related_models, dict) and related_models[related]:
first_part, remainder = related, related_models[related] first_part, remainder = related, related_models[related]
@ -176,22 +247,26 @@ class Model(NewBaseModel):
All joined tables have prefixes to allow duplicate column names, All joined tables have prefixes to allow duplicate column names,
as well as duplicated joins to the same table from multiple different tables. 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. Extracted fields populates the item dict later used to construct a Model.
Used in Model.from_row and PrefetchQuery._populate_rows methods.
:param item: dictionary of already populated nested models, otherwise empty dict :param item: dictionary of already populated nested models, otherwise empty dict
:type item: Dict :type item: Dict
:param row: raw result row from the database :param row: raw result row from the database
:type row: sqlalchemy.engine.result.ResultProxy :type row: sqlalchemy.engine.result.ResultProxy
:param table_prefix: prefix of the table from AliasManager :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 each pair of tables have own prefix (two of them depending on direction) -
to allow multiple joins to the same table. used in joins to allow multiple joins to the same table.
:type table_prefix: str :type table_prefix: str
:param fields: fields and related model fields to include - if provided only those are included :param fields: fields and related model fields to include -
if provided only those are included
:type fields: Optional[Union[Dict, Set]] :type fields: Optional[Union[Dict, Set]]
:param exclude_fields: fields and related model fields to exclude :param exclude_fields: fields and related model fields to exclude
excludes the fields even if they are provided in fields excludes the fields even if they are provided in fields
:type exclude_fields: Optional[Union[Dict, Set]] :type exclude_fields: Optional[Union[Dict, Set]]
:return: dictionary with keys corresponding to model fields names and values are database values :return: dictionary with keys corresponding to model fields names
and values are database values
:rtype: Dict :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
@ -216,7 +291,7 @@ class Model(NewBaseModel):
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. Performs either a save or an update depending on the presence of the pk.
If the pk field is filled it's an update, otherwise the save is performed. 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. For save kwargs are ignored, used only in update if provided.
@ -237,11 +312,13 @@ class Model(NewBaseModel):
Related models are saved by pk number, reverse relation and many to many fields Related models are saved by pk number, reverse relation and many to many fields
are not saved - use corresponding relations methods. are not saved - use corresponding relations methods.
If there are fields with server_default set and those fields are not already filled If there are fields with server_default set and those fields
save will trigger also a second query to refreshed the fields populated server side. 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 Does not recognize if model was previously saved.
insert depending on the pk fields presence use upsert. If you want to perform update or insert depending on the pk
fields presence use upsert.
Sends pre_save and post_save signals. Sends pre_save and post_save signals.
@ -289,7 +366,8 @@ class Model(NewBaseModel):
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. Triggers a upsert method on all related models
if the instances are not already saved.
By default saves only the directly related ones. By default saves only the directly related ones.
If follow=True is set it saves also related models of related models. If follow=True is set it saves also related models of related models.
@ -299,15 +377,17 @@ class Model(NewBaseModel):
That way already visited models that are nested are saved, but the save do not 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 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 Model A but will never follow into Model C.
be persisted manually. 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 :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 with follow=True also related models of related models are saved
:type follow: bool :type follow: bool
:param visited: internal parameter for recursive calls - already visited models :param visited: internal parameter for recursive calls - already visited models
:type visited: Set :type visited: Set
:param update_count: internal parameter for recursive calls - no uf updated instances :param update_count: internal parameter for recursive calls -
number of updated instances
:type update_count: int :type update_count: int
:return: number of updated/saved models :return: number of updated/saved models
:rtype: int :rtype: int
@ -348,12 +428,14 @@ class Model(NewBaseModel):
:param rel: Model to follow :param rel: Model to follow
:type rel: Model :type rel: Model
:param follow: flag to trigger deep save - by default only directly related models are saved :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 with follow=True also related models of related models are saved
:type follow: bool :type follow: bool
:param visited: internal parameter for recursive calls - already visited models :param visited: internal parameter for recursive calls - already visited models
:type visited: Set :type visited: Set
:param update_count: internal parameter for recursive calls - no uf updated instances :param update_count: internal parameter for recursive calls -
number of updated instances
:type update_count: int :type update_count: int
:return: tuple of update count and visited :return: tuple of update count and visited
:rtype: Tuple[int, Set] :rtype: Tuple[int, Set]
@ -429,10 +511,10 @@ class Model(NewBaseModel):
async def load(self: T) -> T: async def load(self: T) -> T:
""" """
Allow to refresh existing Models fields from database. Allow to refresh existing Models fields from database.
Be careful as the related models can be overwritten by pk_only models during load. Be careful as the related models can be overwritten by pk_only models in load.
Does NOT refresh the related models fields if they were loaded before. 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. :raises: If given pk is not found in database the NoMatch exception is raised.
:return: reloaded Model :return: reloaded Model
:rtype: Model :rtype: Model

View File

@ -141,7 +141,7 @@ class QueryClause:
through_field.through, through_field.to, explicit_multi=True through_field.through, through_field.to, explicit_multi=True
) )
manager = model_cls.Meta.alias_manager manager = model_cls.Meta.alias_manager
table_prefix = manager.resolve_relation_join_new(previous_model, part2) table_prefix = manager.resolve_relation_join(previous_model, part2)
model_cls = model_cls.Meta.model_fields[part].to model_cls = model_cls.Meta.model_fields[part].to
previous_model = model_cls previous_model = model_cls
return select_related, table_prefix, model_cls return select_related, table_prefix, model_cls

View File

@ -135,7 +135,7 @@ class SqlJoin:
model_cls = join_params.model_cls.Meta.model_fields[part].to model_cls = join_params.model_cls.Meta.model_fields[part].to
to_table = model_cls.Meta.table.name to_table = model_cls.Meta.table.name
alias = model_cls.Meta.alias_manager.resolve_relation_join_new( alias = model_cls.Meta.alias_manager.resolve_relation_join(
join_params.prev_model, part join_params.prev_model, part
) )
if alias not in self.used_aliases: if alias not in self.used_aliases:

View File

@ -328,7 +328,7 @@ class PrefetchQuery:
if issubclass(target_field, ManyToManyField): if issubclass(target_field, ManyToManyField):
query_target = target_field.through query_target = target_field.through
select_related = [target_name] select_related = [target_name]
table_prefix = target_field.to.Meta.alias_manager.resolve_relation_join_new( table_prefix = target_field.to.Meta.alias_manager.resolve_relation_join(
query_target, target_name query_target, target_name
) )
self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix self.already_extracted.setdefault(target_name, {})["prefix"] = table_prefix

View File

@ -39,7 +39,7 @@ class AliasManager:
def prefixed_table_name(alias: str, name: str) -> text: def prefixed_table_name(alias: str, name: str) -> text:
return text(f"{name} {alias}_{name}") return text(f"{name} {alias}_{name}")
def add_relation_type_new( def add_relation_type(
self, source_model: Type["Model"], relation_name: str, is_multi: bool = False self, source_model: Type["Model"], relation_name: str, is_multi: bool = False
) -> None: ) -> None:
parent_key = f"{source_model.get_name()}_{relation_name}" parent_key = f"{source_model.get_name()}_{relation_name}"
@ -56,7 +56,7 @@ class AliasManager:
if child_key not in self._aliases_new: if child_key not in self._aliases_new:
self._aliases_new[child_key] = get_table_alias() self._aliases_new[child_key] = get_table_alias()
def resolve_relation_join_new( def resolve_relation_join(
self, from_model: Type["Model"], relation_name: str self, from_model: Type["Model"], relation_name: str
) -> str: ) -> str:
alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "") alias = self._aliases_new.get(f"{from_model.get_name()}_{relation_name}", "")

View File

@ -100,7 +100,7 @@ class Car(ormar.Model):
id: int = ormar.Integer(primary_key=True) id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50) name: str = ormar.String(max_length=50)
owner: Person = ormar.ForeignKey(Person) owner: Person = ormar.ForeignKey(Person)
co_owner: Person = ormar.ForeignKey(Person, related_name='coowned') co_owner: Person = ormar.ForeignKey(Person, related_name="coowned")
class Truck(Car): class Truck(Car):
@ -113,7 +113,7 @@ class Truck(Car):
class Bus(Car): class Bus(Car):
class Meta: class Meta:
tablename = 'buses' tablename = "buses"
metadata = metadata metadata = metadata
database = db database = db
@ -134,6 +134,7 @@ def test_init_of_abstract_model():
def test_field_redefining_raises_error(): def test_field_redefining_raises_error():
with pytest.raises(ModelDefinitionError): with pytest.raises(ModelDefinitionError):
class WrongField(DateFieldsModel): # pragma: no cover class WrongField(DateFieldsModel): # pragma: no cover
class Meta(ormar.ModelMeta): class Meta(ormar.ModelMeta):
tablename = "wrongs" tablename = "wrongs"
@ -146,6 +147,7 @@ def test_field_redefining_raises_error():
def test_model_subclassing_non_abstract_raises_error(): def test_model_subclassing_non_abstract_raises_error():
with pytest.raises(ModelDefinitionError): with pytest.raises(ModelDefinitionError):
class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover class WrongField2(DateFieldsModelNoSubclass): # pragma: no cover
class Meta(ormar.ModelMeta): class Meta(ormar.ModelMeta):
tablename = "wrongs" tablename = "wrongs"
@ -243,15 +245,19 @@ async def test_fields_inherited_from_mixin():
async def test_inheritance_with_relation(): async def test_inheritance_with_relation():
async with db: async with db:
async with db.transaction(force_rollback=True): async with db.transaction(force_rollback=True):
sam = await Person(name='Sam').save() sam = await Person(name="Sam").save()
joe = await Person(name='Joe').save() joe = await Person(name="Joe").save()
await Truck(name='Shelby wanna be', max_capacity=1400, owner=sam, co_owner=joe).save() await Truck(
name="Shelby wanna be", max_capacity=1400, owner=sam, co_owner=joe
).save()
shelby = await Truck.objects.select_related(['owner', 'co_owner']).get() shelby = await Truck.objects.select_related(["owner", "co_owner"]).get()
assert shelby.name == 'Shelby wanna be' assert shelby.name == "Shelby wanna be"
assert shelby.owner.name == 'Sam' assert shelby.owner.name == "Sam"
assert shelby.co_owner.name == 'Joe' assert shelby.co_owner.name == "Joe"
joe_check = await Person.objects.select_related('coowned_trucks').get(name='Joe') joe_check = await Person.objects.select_related("coowned_trucks").get(
name="Joe"
)
assert joe_check.pk == joe.pk assert joe_check.pk == joe.pk
assert joe_check.coowned_trucks[0] == shelby assert joe_check.coowned_trucks[0] == shelby