for now revert type changes to avoid mypy errors, add validation for through models, clean docs etc
This commit is contained in:
@ -29,7 +29,8 @@
|
|||||||
* you can include and exclude fields on through models
|
* you can include and exclude fields on through models
|
||||||
* through models are attached only to related models (i.e. if you query from A to B -> only on B)
|
* through models are attached only to related models (i.e. if you query from A to B -> only on B)
|
||||||
* note that through models are explicitly loaded without relations -> relation is already populated in ManyToMany field.
|
* note that through models are explicitly loaded without relations -> relation is already populated in ManyToMany field.
|
||||||
* note that just like before you cannot declare the relation fields on through model, they will be populated for you by `ormar`
|
* note that just like before you cannot declare the relation fields on through model, they will be populated for you by `ormar`,
|
||||||
|
but now if you try to do so `ModelDefinitionError` will be thrown
|
||||||
* check the updated ManyToMany relation docs for more information
|
* check the updated ManyToMany relation docs for more information
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from typing import Any, List, Optional, TYPE_CHECKING, Tuple, Type, Union, cast
|
|||||||
|
|
||||||
from pydantic.typing import ForwardRef, evaluate_forwardref
|
from pydantic.typing import ForwardRef, evaluate_forwardref
|
||||||
import ormar # noqa: I100
|
import ormar # noqa: I100
|
||||||
|
from ormar import ModelDefinitionError
|
||||||
from ormar.fields import BaseField
|
from ormar.fields import BaseField
|
||||||
from ormar.fields.foreign_key import ForeignKeyField
|
from ormar.fields.foreign_key import ForeignKeyField
|
||||||
|
|
||||||
@ -17,6 +18,21 @@ if TYPE_CHECKING: # pragma no cover
|
|||||||
REF_PREFIX = "#/components/schemas/"
|
REF_PREFIX = "#/components/schemas/"
|
||||||
|
|
||||||
|
|
||||||
|
def forbid_through_relations(through: Type["Model"]) -> None:
|
||||||
|
"""
|
||||||
|
Verifies if the through model does not have relations.
|
||||||
|
|
||||||
|
:param through: through Model to be checked
|
||||||
|
:type through: Type['Model]
|
||||||
|
"""
|
||||||
|
if any(field.is_relation for field in through.Meta.model_fields.values()):
|
||||||
|
raise ModelDefinitionError(
|
||||||
|
f"Through Models cannot have explicit relations "
|
||||||
|
f"defined. Remove the relations from Model "
|
||||||
|
f"{through.get_name(lower=False)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def populate_m2m_params_based_on_to_model(
|
def populate_m2m_params_based_on_to_model(
|
||||||
to: Type["Model"], nullable: bool
|
to: Type["Model"], nullable: bool
|
||||||
) -> Tuple[Any, Any]:
|
) -> Tuple[Any, Any]:
|
||||||
@ -77,6 +93,8 @@ def ManyToMany(
|
|||||||
nullable = kwargs.pop("nullable", True)
|
nullable = kwargs.pop("nullable", True)
|
||||||
owner = kwargs.pop("owner", None)
|
owner = kwargs.pop("owner", None)
|
||||||
self_reference = kwargs.pop("self_reference", False)
|
self_reference = kwargs.pop("self_reference", False)
|
||||||
|
if through is not None and through.__class__ != ForwardRef:
|
||||||
|
forbid_through_relations(cast(Type["Model"], through))
|
||||||
|
|
||||||
if to.__class__ == ForwardRef:
|
if to.__class__ == ForwardRef:
|
||||||
__type__ = to if not nullable else Optional[to]
|
__type__ = to if not nullable else Optional[to]
|
||||||
@ -189,6 +207,7 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro
|
|||||||
globalns,
|
globalns,
|
||||||
localns or None,
|
localns or None,
|
||||||
)
|
)
|
||||||
|
forbid_through_relations(cls.through)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_relation_name(cls) -> str:
|
def get_relation_name(cls) -> str:
|
||||||
|
|||||||
@ -375,25 +375,3 @@ async def test_excluding_fields_on_through_model() -> Any:
|
|||||||
for category in post3.categories:
|
for category in post3.categories:
|
||||||
assert category.postcategory.param_name is None
|
assert category.postcategory.param_name is None
|
||||||
assert category.postcategory.sort_order is None
|
assert category.postcategory.sort_order is None
|
||||||
|
|
||||||
|
|
||||||
# TODO: check/ modify following
|
|
||||||
|
|
||||||
# add to fields with class lower name (V)
|
|
||||||
# forward refs update (V)
|
|
||||||
# creating while adding to relation (kwargs in add) (V)
|
|
||||||
# creating in queryset proxy (dict with through name and kwargs) (V)
|
|
||||||
# loading the data into model instance of though model (V) <- fix fields ane exclude
|
|
||||||
# accessing from instance (V) <- no both sides only nested one is relevant, fix one side
|
|
||||||
# filtering in filter (through name normally) (V) < - table prefix from normal relation,
|
|
||||||
# check if is_through needed, resolved side of relation
|
|
||||||
# ordering by in order_by (V)
|
|
||||||
# updating in query (V)
|
|
||||||
# updating from querysetproxy (V)
|
|
||||||
# including/excluding in fields? (V)
|
|
||||||
# make through optional? auto-generated for cases other fields are missing? (V)
|
|
||||||
|
|
||||||
# modifying from instance (both sides?) (X) <= no, the loaded one doesn't have relations
|
|
||||||
# allowing to change fk fields names in through model? (X) <= separate issue
|
|
||||||
|
|
||||||
# prevent adding relation on through field definition
|
|
||||||
|
|||||||
51
tests/test_through_relations_fail.py
Normal file
51
tests/test_through_relations_fail.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# type: ignore
|
||||||
|
|
||||||
|
import databases
|
||||||
|
import pytest
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
import ormar
|
||||||
|
from ormar import ModelDefinitionError
|
||||||
|
from tests.settings import DATABASE_URL
|
||||||
|
|
||||||
|
database = databases.Database(DATABASE_URL, force_rollback=True)
|
||||||
|
metadata = sqlalchemy.MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
def test_through_with_relation_fails():
|
||||||
|
class BaseMeta(ormar.ModelMeta):
|
||||||
|
database = database
|
||||||
|
metadata = metadata
|
||||||
|
|
||||||
|
class Category(ormar.Model):
|
||||||
|
class Meta(BaseMeta):
|
||||||
|
tablename = "categories"
|
||||||
|
|
||||||
|
id = ormar.Integer(primary_key=True)
|
||||||
|
name = ormar.String(max_length=40)
|
||||||
|
|
||||||
|
class Blog(ormar.Model):
|
||||||
|
class Meta(BaseMeta):
|
||||||
|
pass
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
title: str = ormar.String(max_length=200)
|
||||||
|
|
||||||
|
class PostCategory(ormar.Model):
|
||||||
|
class Meta(BaseMeta):
|
||||||
|
tablename = "posts_x_categories"
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
sort_order: int = ormar.Integer(nullable=True)
|
||||||
|
param_name: str = ormar.String(default="Name", max_length=200)
|
||||||
|
blog = ormar.ForeignKey(Blog)
|
||||||
|
|
||||||
|
with pytest.raises(ModelDefinitionError):
|
||||||
|
|
||||||
|
class Post(ormar.Model):
|
||||||
|
class Meta(BaseMeta):
|
||||||
|
pass
|
||||||
|
|
||||||
|
id: int = ormar.Integer(primary_key=True)
|
||||||
|
title: str = ormar.String(max_length=200)
|
||||||
|
categories = ormar.ManyToMany(Category, through=PostCategory)
|
||||||
Reference in New Issue
Block a user