add skip_reverse parameter, add links to related libs, fix weakref error, fix through error with extra=forbid

This commit is contained in:
collerek
2021-04-11 18:43:23 +02:00
parent e553885221
commit b3b1c156b5
19 changed files with 675 additions and 48 deletions

View File

@ -75,7 +75,7 @@ class UndefinedType: # pragma no cover
Undefined = UndefinedType()
__version__ = "0.10.2"
__version__ = "0.10.3"
__all__ = [
"Integer",
"BigInteger",

View File

@ -53,6 +53,8 @@ class BaseField(FieldInfo):
"is_relation", None
) # ForeignKeyField + subclasses
self.is_through: bool = kwargs.pop("is_through", False) # ThroughFields
self.skip_reverse: bool = kwargs.pop("skip_reverse", False)
self.skip_field: bool = kwargs.pop("skip_field", False)
self.owner: Type["Model"] = kwargs.pop("owner", None)
self.to: Type["Model"] = kwargs.pop("to", None)

View File

@ -233,9 +233,13 @@ def ForeignKey( # noqa CFQ002
owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False)
orders_by = kwargs.pop("orders_by", None)
related_orders_by = kwargs.pop("related_orders_by", None)
skip_reverse = kwargs.pop("skip_reverse", False)
skip_field = kwargs.pop("skip_field", False)
validate_not_allowed_fields(kwargs)
if to.__class__ == ForwardRef:
@ -274,6 +278,8 @@ def ForeignKey( # noqa CFQ002
is_relation=True,
orders_by=orders_by,
related_orders_by=related_orders_by,
skip_reverse=skip_reverse,
skip_field=skip_field,
)
Field = type("ForeignKey", (ForeignKeyField, BaseField), {})
@ -312,6 +318,30 @@ class ForeignKeyField(BaseField):
"""
return self.related_name or self.owner.get_name() + "s"
def default_target_field_name(self, reverse: bool = False) -> str:
"""
Returns default target model name on through model.
:param reverse: flag to grab name without accessing related field
:type reverse: bool
:return: name of the field
:rtype: str
"""
self_rel_prefix = "from_" if not reverse else "to_"
prefix = self_rel_prefix if self.self_reference else ""
return f"{prefix}{self.to.get_name()}"
def default_source_field_name(self, reverse: bool = False) -> str:
"""
Returns default target model name on through model.
:param reverse: flag to grab name without accessing related field
:type reverse: bool
:return: name of the field
:rtype: str
"""
self_rel_prefix = "to_" if not reverse else "from_"
prefix = self_rel_prefix if self.self_reference else ""
return f"{prefix}{self.owner.get_name()}"
def evaluate_forward_ref(self, globalns: Any, localns: Any) -> None:
"""
Evaluates the ForwardRef to actual Field based on global and local namespaces

View File

@ -112,11 +112,16 @@ def ManyToMany(
"""
related_name = kwargs.pop("related_name", None)
nullable = kwargs.pop("nullable", True)
owner = kwargs.pop("owner", None)
self_reference = kwargs.pop("self_reference", False)
orders_by = kwargs.pop("orders_by", None)
related_orders_by = kwargs.pop("related_orders_by", None)
skip_reverse = kwargs.pop("skip_reverse", False)
skip_field = kwargs.pop("skip_field", False)
if through is not None and through.__class__ != ForwardRef:
forbid_through_relations(cast(Type["Model"], through))
@ -151,6 +156,8 @@ def ManyToMany(
is_multi=True,
orders_by=orders_by,
related_orders_by=related_orders_by,
skip_reverse=skip_reverse,
skip_field=skip_field,
)
Field = type("ManyToMany", (ManyToManyField, BaseField), {})
@ -184,24 +191,6 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro
or self.name
)
def default_target_field_name(self) -> str:
"""
Returns default target model name on through model.
:return: name of the field
:rtype: str
"""
prefix = "from_" if self.self_reference else ""
return f"{prefix}{self.to.get_name()}"
def default_source_field_name(self) -> str:
"""
Returns default target model name on through model.
:return: name of the field
:rtype: str
"""
prefix = "to_" if self.self_reference else ""
return f"{prefix}{self.owner.get_name()}"
def has_unresolved_forward_refs(self) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the

View File

@ -111,6 +111,7 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None:
self_reference=model_field.self_reference,
self_reference_primary=model_field.self_reference_primary,
orders_by=model_field.related_orders_by,
skip_field=model_field.skip_reverse,
)
# register foreign keys on through model
model_field = cast("ManyToManyField", model_field)
@ -125,6 +126,7 @@ def register_reverse_model_fields(model_field: "ForeignKeyField") -> None:
owner=model_field.to,
self_reference=model_field.self_reference,
orders_by=model_field.related_orders_by,
skip_field=model_field.skip_reverse,
)
@ -145,6 +147,7 @@ def register_through_shortcut_fields(model_field: "ManyToManyField") -> None:
virtual=True,
related_name=model_field.name,
owner=model_field.owner,
nullable=True,
)
model_field.to.Meta.model_fields[through_name] = Through(
@ -153,6 +156,7 @@ def register_through_shortcut_fields(model_field: "ManyToManyField") -> None:
virtual=True,
related_name=related_name,
owner=model_field.to,
nullable=True,
)

View File

@ -90,6 +90,7 @@ def add_cached_properties(new_model: Type["Model"]) -> None:
"""
new_model._quick_access_fields = quick_access_set
new_model._related_names = None
new_model._through_names = None
new_model._related_fields = None
new_model._pydantic_fields = {name for name in new_model.__fields__}
new_model._choices_fields = set()
@ -536,6 +537,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
new_model = populate_meta_tablename_columns_and_pk(name, new_model)
populate_meta_sqlalchemy_table_if_required(new_model.Meta)
expand_reverse_relationships(new_model)
# TODO: iterate only related fields
for field in new_model.Meta.model_fields.values():
register_relation_in_alias_manager(field=field)

View File

@ -20,6 +20,7 @@ class RelationMixin:
Meta: ModelMeta
_related_names: Optional[Set]
_through_names: Optional[Set]
_related_fields: Optional[List]
get_name: Callable
@ -57,19 +58,23 @@ class RelationMixin:
return related_fields
@classmethod
def extract_through_names(cls) -> Set:
def extract_through_names(cls) -> Set[str]:
"""
Extracts related fields through names which are shortcuts to through models.
:return: set of related through fields names
:rtype: Set
"""
related_fields = set()
for name in cls.extract_related_names():
field = cls.Meta.model_fields[name]
if field.is_multi:
related_fields.add(field.through.get_name(lower=True))
return related_fields
if isinstance(cls._through_names, Set):
return cls._through_names
related_names = set()
for name, field in cls.Meta.model_fields.items():
if isinstance(field, BaseField) and field.is_through:
related_names.add(name)
cls._through_names = related_names
return related_names
@classmethod
def extract_related_names(cls) -> Set[str]:
@ -89,6 +94,7 @@ class RelationMixin:
isinstance(field, BaseField)
and field.is_relation
and not field.is_through
and not field.skip_field
):
related_names.add(name)
cls._related_names = related_names

View File

@ -24,7 +24,11 @@ class Model(ModelRow):
Meta: ModelMeta
def __repr__(self) -> str: # pragma nocover
_repr = {k: getattr(self, k) for k, v in self.Meta.model_fields.items()}
_repr = {
k: getattr(self, k)
for k, v in self.Meta.model_fields.items()
if not v.skip_field
}
return f"{self.__class__.__name__}({str(_repr)})"
async def upsert(self: T, **kwargs: Any) -> T:

View File

@ -81,6 +81,7 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
_orm_id: int
_orm_saved: bool
_related_names: Optional[Set]
_through_names: Optional[Set]
_related_names_hash: str
_choices_fields: Optional[Set]
_pydantic_fields: Set
@ -165,6 +166,11 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
for field_to_nullify in excluded:
new_kwargs[field_to_nullify] = None
# extract through fields
through_tmp_dict = dict()
for field_name in self.extract_through_names():
through_tmp_dict[field_name] = new_kwargs.pop(field_name, None)
values, fields_set, validation_error = pydantic.validate_model(
self, new_kwargs # type: ignore
)
@ -174,6 +180,9 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
object.__setattr__(self, "__dict__", values)
object.__setattr__(self, "__fields_set__", fields_set)
# add back through fields
new_kwargs.update(through_tmp_dict)
# register the columns models after initialization
for related in self.extract_related_names().union(self.extract_through_names()):
self.Meta.model_fields[related].expand_relationship(
@ -592,13 +601,16 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
exclude=self._skip_ellipsis(exclude, field),
)
elif nested_model is not None:
dict_instance[field] = nested_model.dict(
relation_map=self._skip_ellipsis(
relation_map, field, default_return=dict()
),
include=self._skip_ellipsis(include, field),
exclude=self._skip_ellipsis(exclude, field),
)
try:
dict_instance[field] = nested_model.dict(
relation_map=self._skip_ellipsis(
relation_map, field, default_return=dict()
),
include=self._skip_ellipsis(include, field),
exclude=self._skip_ellipsis(exclude, field),
)
except ReferenceError:
dict_instance[field] = None
else:
dict_instance[field] = None
return dict_instance

View File

@ -22,7 +22,7 @@ if TYPE_CHECKING: # pragma no cover
from ormar.relations import Relation
from ormar.models import Model, T
from ormar.queryset import QuerySet
from ormar import RelationType
from ormar import RelationType, ForeignKeyField
else:
T = TypeVar("T", bound="Model")
@ -251,7 +251,7 @@ class QuerysetProxy(Generic[T]):
owner_column = self._owner.get_name()
else:
queryset = ormar.QuerySet(model_cls=self.relation.to) # type: ignore
owner_column = self.related_field.name
owner_column = self.related_field_name
kwargs = {owner_column: self._owner}
self._clean_items_on_load()
if keep_reversed and self.type_ == ormar.RelationType.REVERSE:
@ -367,7 +367,7 @@ class QuerysetProxy(Generic[T]):
"""
through_kwargs = kwargs.pop(self.through_model_name, {})
if self.type_ == ormar.RelationType.REVERSE:
kwargs[self.related_field.name] = self._owner
kwargs[self.related_field_name] = self._owner
created = await self.queryset.create(**kwargs)
self._register_related(created)
if self.type_ == ormar.RelationType.MULTIPLE:

View File

@ -124,15 +124,14 @@ class RelationProxy(Generic[T], list):
:rtype: QuerySet
"""
related_field_name = self.related_field_name
related_field = self.relation.to.Meta.model_fields[related_field_name]
pkname = self._owner.get_column_alias(self._owner.Meta.pkname)
self._check_if_model_saved()
kwargs = {f"{related_field.name}__{pkname}": self._owner.pk}
kwargs = {f"{related_field_name}__{pkname}": self._owner.pk}
queryset = (
ormar.QuerySet(
model_cls=self.relation.to, proxy_source_model=self._owner.__class__
)
.select_related(related_field.name)
.select_related(related_field_name)
.filter(**kwargs)
)
return queryset
@ -168,11 +167,12 @@ class RelationProxy(Generic[T], list):
super().remove(item)
relation_name = self.related_field_name
relation = item._orm._get(relation_name)
if relation is None: # pragma nocover
raise ValueError(
f"{self._owner.get_name()} does not have relation {relation_name}"
)
relation.remove(self._owner)
# if relation is None: # pragma nocover
# raise ValueError(
# f"{self._owner.get_name()} does not have relation {relation_name}"
# )
if relation:
relation.remove(self._owner)
self.relation.remove(item)
if self.type_ == ormar.RelationType.MULTIPLE:
await self.queryset_proxy.delete_through_instance(item)
@ -211,7 +211,7 @@ class RelationProxy(Generic[T], list):
self._check_if_model_saved()
if self.type_ == ormar.RelationType.MULTIPLE:
await self.queryset_proxy.create_through_instance(item, **kwargs)
setattr(item, relation_name, self._owner)
setattr(self._owner, self.field_name, item)
else:
setattr(item, relation_name, self._owner)
await item.update()