WIP add owner to fields and simplify relation names

This commit is contained in:
collerek
2021-01-09 16:13:53 +01:00
parent 8b794d07f9
commit 055c99ba02
7 changed files with 188 additions and 150 deletions

View File

@ -41,6 +41,7 @@ class BaseField(FieldInfo):
virtual: bool = False virtual: bool = False
choices: typing.Sequence choices: typing.Sequence
owner: Type["Model"]
to: Type["Model"] to: Type["Model"]
through: Type["Model"] through: Type["Model"]
self_reference: bool = False self_reference: bool = False
@ -266,6 +267,29 @@ class BaseField(FieldInfo):
""" """
return value return value
@classmethod
def set_self_reference_flag(cls):
"""
Sets `self_reference` to True if field to and owner are same model.
:return: None
:rtype: None
"""
if cls.owner is not None and (
cls.owner == cls.to or cls.owner.Meta == cls.to.Meta
):
cls.self_reference = True
@classmethod
def has_unresolved_forward_refs(cls) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
:return: result of the check
:rtype: bool
"""
return False
@classmethod @classmethod
def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None: def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None:
""" """

View File

@ -166,6 +166,7 @@ def ForeignKey( # noqa CFQ002
:rtype: ForeignKeyField :rtype: ForeignKeyField
""" """
owner = kwargs.pop("owner", None)
if isinstance(to, ForwardRef): if isinstance(to, ForwardRef):
__type__ = to if not nullable else Optional[to] __type__ = to if not nullable else Optional[to]
constraints: List = [] constraints: List = []
@ -194,6 +195,7 @@ def ForeignKey( # noqa CFQ002
server_default=None, server_default=None,
onupdate=onupdate, onupdate=onupdate,
ondelete=ondelete, ondelete=ondelete,
owner=owner,
) )
return type("ForeignKey", (ForeignKeyField, BaseField), namespace) return type("ForeignKey", (ForeignKeyField, BaseField), namespace)
@ -211,6 +213,16 @@ class ForeignKeyField(BaseField):
ondelete: str ondelete: str
onupdate: str onupdate: str
@classmethod
def get_related_name(cls) -> str:
"""
Returns name to use for reverse relation.
It's either set as `related_name` or by default it's owner model. get_name + 's'
:return:
:rtype:
"""
return cls.related_name or cls.owner.get_name() + "s"
@classmethod @classmethod
def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None: def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None:
""" """
@ -371,6 +383,17 @@ class ForeignKeyField(BaseField):
relation_name=relation_name, relation_name=relation_name,
) )
@classmethod
def has_unresolved_forward_refs(cls) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
:return: result of the check
:rtype: bool
"""
return isinstance(cls.to, ForwardRef)
@classmethod @classmethod
def expand_relationship( def expand_relationship(
cls, cls,

View File

@ -43,7 +43,7 @@ def ManyToMany(
name: str = None, name: str = None,
unique: bool = False, unique: bool = False,
virtual: bool = False, virtual: bool = False,
**kwargs: Any **kwargs: Any,
) -> Any: ) -> Any:
""" """
Despite a name it's a function that returns constructed ManyToManyField. Despite a name it's a function that returns constructed ManyToManyField.
@ -70,6 +70,7 @@ def ManyToMany(
""" """
related_name = kwargs.pop("related_name", None) related_name = kwargs.pop("related_name", None)
nullable = kwargs.pop("nullable", True) nullable = kwargs.pop("nullable", True)
owner = kwargs.pop("owner", None)
if isinstance(to, ForwardRef): if isinstance(to, ForwardRef):
__type__ = to if not nullable else Optional[to] __type__ = to if not nullable else Optional[to]
@ -94,6 +95,7 @@ def ManyToMany(
pydantic_only=False, pydantic_only=False,
default=None, default=None,
server_default=None, server_default=None,
owner=owner,
) )
return type("ManyToMany", (ManyToManyField, BaseField), namespace) return type("ManyToMany", (ManyToManyField, BaseField), namespace)
@ -111,7 +113,29 @@ class ManyToManyField(ForeignKeyField, ormar.QuerySetProtocol, ormar.RelationPro
:return: name of the field :return: name of the field
:rtype: str :rtype: str
""" """
return cls.to.get_name() prefix = "to_" if cls.self_reference else ""
return f"{prefix}{cls.to.get_name()}"
@classmethod
def default_source_field_name(cls) -> str:
"""
Returns default target model name on through model.
:return: name of the field
:rtype: str
"""
prefix = "from_" if cls.self_reference else ""
return f"{prefix}{cls.owner.get_name()}"
@classmethod
def has_unresolved_forward_refs(cls) -> bool:
"""
Verifies if the filed has any ForwardRefs that require updating before the
model can be used.
:return: result of the check
:rtype: bool
"""
return isinstance(cls.to, ForwardRef) or isinstance(cls.through, ForwardRef)
@classmethod @classmethod
def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None: def evaluate_forward_ref(cls, globalns: Any, localns: Any) -> None:

View File

@ -1,4 +1,4 @@
from typing import ForwardRef, TYPE_CHECKING, Type from typing import TYPE_CHECKING, Type
import ormar import ormar
from ormar import ForeignKey, ManyToMany from ormar import ForeignKey, ManyToMany
@ -61,26 +61,17 @@ def register_many_to_many_relation_on_build(
) )
def expand_reverse_relationship( def expand_reverse_relationship(model_field: Type["ForeignKeyField"]) -> None:
model: Type["Model"], model_field: Type["ForeignKeyField"]
) -> None:
""" """
If the reverse relation has not been set before it's set here. 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
:param model_field: :param model_field:
:type model_field: :type model_field:
:return: None :return: None
:rtype: None :rtype: None
""" """
child_model_name = model_field.related_name or model.get_name() + "s" if reverse_field_not_already_registered(model_field=model_field):
parent_model = model_field.to register_reverse_model_fields(model_field=model_field)
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 expand_reverse_relationships(model: Type["Model"]) -> None: def expand_reverse_relationships(model: Type["Model"]) -> None:
@ -96,18 +87,12 @@ def expand_reverse_relationships(model: Type["Model"]) -> None:
for model_field in model.Meta.model_fields.values(): for model_field in model.Meta.model_fields.values():
if ( if (
issubclass(model_field, ForeignKeyField) issubclass(model_field, ForeignKeyField)
and not isinstance(model_field.to, ForwardRef) and not model_field.has_unresolved_forward_refs()
and not isinstance(model_field.through, ForwardRef)
): ):
expand_reverse_relationship(model=model, model_field=model_field) expand_reverse_relationship(model_field=model_field)
def register_reverse_model_fields( def register_reverse_model_fields(model_field: Type["ForeignKeyField"]) -> None:
model: Type["Model"],
child: Type["Model"],
related_name: str,
model_field: Type["ForeignKeyField"],
) -> None:
""" """
Registers reverse ForeignKey field on related model. Registers reverse ForeignKey field on related model.
By default it's name.lower()+'s' of the model on which relation is defined. By default it's name.lower()+'s' of the model on which relation is defined.
@ -115,28 +100,28 @@ def register_reverse_model_fields(
But if the related_model name is provided it's registered with that name. 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. 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 :param model_field: original relation ForeignKey field
:type model_field: relation Field :type model_field: relation Field
""" """
related_name = model_field.get_related_name()
if issubclass(model_field, ManyToManyField): if issubclass(model_field, ManyToManyField):
model.Meta.model_fields[related_name] = ManyToMany( model_field.to.Meta.model_fields[related_name] = ManyToMany(
child, model_field.owner,
through=model_field.through, through=model_field.through,
name=related_name, name=related_name,
virtual=True, virtual=True,
related_name=model_field.name, related_name=model_field.name,
owner=model_field.to,
) )
# register foreign keys on through model # register foreign keys on through model
adjust_through_many_to_many_model(model, child, model_field) adjust_through_many_to_many_model(model_field=model_field)
else: else:
model.Meta.model_fields[related_name] = ForeignKey( model_field.to.Meta.model_fields[related_name] = ForeignKey(
child, real_name=related_name, virtual=True, related_name=model_field.name, model_field.owner,
real_name=related_name,
virtual=True,
related_name=model_field.name,
owner=model_field.to,
) )
@ -160,19 +145,19 @@ def register_relation_in_alias_manager(
:type field_name: str :type field_name: str
""" """
if issubclass(field, ManyToManyField): if issubclass(field, ManyToManyField):
if isinstance(field.to, ForwardRef) or isinstance(field.through, ForwardRef): if field.has_unresolved_forward_refs():
return return
register_many_to_many_relation_on_build( register_many_to_many_relation_on_build(
new_model=new_model, field=field, field_name=field_name new_model=new_model, field=field, field_name=field_name
) )
elif issubclass(field, ForeignKeyField): elif issubclass(field, ForeignKeyField):
if isinstance(field.to, ForwardRef): if field.has_unresolved_forward_refs():
return return
register_relation_on_build(new_model=new_model, field_name=field_name) register_relation_on_build(new_model=new_model, field_name=field_name)
def verify_related_name_dont_duplicate( def verify_related_name_dont_duplicate(
child: Type["Model"], parent_model: Type["Model"], related_name: str, related_name: str, model_field: Type["ForeignKeyField"]
) -> None: ) -> None:
""" """
Verifies whether the used related_name (regardless of the fact if user defined or Verifies whether the used related_name (regardless of the fact if user defined or
@ -181,59 +166,51 @@ def verify_related_name_dont_duplicate(
:raises ModelDefinitionError: if name is already used but lead to different related :raises ModelDefinitionError: if name is already used but lead to different related
model model
:param child: related Model class
:type child: ormar.models.metaclass.ModelMetaclass
:param parent_model: parent Model class
:type parent_model: ormar.models.metaclass.ModelMetaclass
:param related_name: :param related_name:
:type related_name: :type related_name:
:param model_field: original relation ForeignKey field
:type model_field: relation Field
:return: None :return: None
:rtype: None :rtype: None
""" """
if parent_model.Meta.model_fields.get(related_name): fk_field = model_field.to.Meta.model_fields.get(related_name)
fk_field = parent_model.Meta.model_fields.get(related_name)
if not fk_field: # pragma: no cover if not fk_field: # pragma: no cover
return return
if fk_field.to != child and fk_field.to.Meta != child.Meta: if fk_field.to != model_field.owner and fk_field.to.Meta != model_field.owner.Meta:
raise ormar.ModelDefinitionError( raise ormar.ModelDefinitionError(
f"Relation with related_name " f"Relation with related_name "
f"'{related_name}' " f"'{related_name}' "
f"leading to model " f"leading to model "
f"{parent_model.get_name(lower=False)} " f"{model_field.to.get_name(lower=False)} "
f"cannot be used on model " f"cannot be used on model "
f"{child.get_name(lower=False)} " f"{model_field.owner.get_name(lower=False)} "
f"because it's already used by model " f"because it's already used by model "
f"{fk_field.to.get_name(lower=False)}" f"{fk_field.to.get_name(lower=False)}"
) )
def reverse_field_not_already_registered( def reverse_field_not_already_registered(model_field: Type["ForeignKeyField"]) -> bool:
child: Type["Model"], child_model_name: str, parent_model: Type["Model"]
) -> bool:
""" """
Checks if child is already registered in parents pydantic fields. Checks if child is already registered in parents pydantic fields.
:raises ModelDefinitionError: if related name is already used but lead to different :raises ModelDefinitionError: if related name is already used but lead to different
related model related model
:param child: related Model class :param model_field: original relation ForeignKey field
:type child: ormar.models.metaclass.ModelMetaclass :type model_field: relation Field
: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 :return: result of the check
:rtype: bool :rtype: bool
""" """
check_result = child_model_name not in parent_model.Meta.model_fields related_name = model_field.get_related_name()
check_result2 = child.get_name() not in parent_model.Meta.model_fields check_result = related_name not in model_field.to.Meta.model_fields
check_result2 = model_field.owner.get_name() not in model_field.to.Meta.model_fields
if not check_result: if not check_result:
verify_related_name_dont_duplicate( verify_related_name_dont_duplicate(
child=child, parent_model=parent_model, related_name=child_model_name related_name=related_name, model_field=model_field
) )
if not check_result2: if not check_result2:
verify_related_name_dont_duplicate( verify_related_name_dont_duplicate(
child=child, parent_model=parent_model, related_name=child.get_name() related_name=model_field.owner.get_name(), model_field=model_field
) )
return check_result and check_result2 return check_result and check_result2

View File

@ -15,58 +15,47 @@ if TYPE_CHECKING: # pragma no cover
from ormar.models import NewBaseModel from ormar.models import NewBaseModel
def adjust_through_many_to_many_model( def adjust_through_many_to_many_model(model_field: Type[ManyToManyField]) -> None:
model: Type["Model"], child: Type["Model"], model_field: Type[ManyToManyField]
) -> None:
""" """
Registers m2m relation on through model. Registers m2m relation on through model.
Sets ormar.ForeignKey from through model to both child and parent models. Sets ormar.ForeignKey from through model to both child and parent models.
Sets sqlalchemy.ForeignKey to both child and parent models. Sets sqlalchemy.ForeignKey to both child and parent models.
Sets pydantic fields with child and parent model types. 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 :param model_field: relation field defined in parent model
:type model_field: ManyToManyField :type model_field: ManyToManyField
""" """
same_table_ref = False parent_name = model_field.default_target_field_name()
if child == model or child.Meta == model.Meta: child_name = model_field.default_source_field_name()
same_table_ref = True
model_field.self_reference = True
if same_table_ref:
parent_name = f'to_{model.get_name()}'
child_name = f'from_{child.get_name()}'
else:
parent_name = model.get_name()
child_name = child.get_name()
model_field.through.Meta.model_fields[parent_name] = ForeignKey( model_field.through.Meta.model_fields[parent_name] = ForeignKey(
model, real_name=parent_name, ondelete="CASCADE" model_field.to, real_name=parent_name, ondelete="CASCADE"
) )
model_field.through.Meta.model_fields[child_name] = ForeignKey( model_field.through.Meta.model_fields[child_name] = ForeignKey(
child, real_name=child_name, ondelete="CASCADE" model_field.owner, real_name=child_name, ondelete="CASCADE"
) )
create_and_append_m2m_fk(model=model, model_field=model_field, create_and_append_m2m_fk(
field_name=parent_name) model=model_field.to, model_field=model_field, field_name=parent_name
create_and_append_m2m_fk(model=child, model_field=model_field, )
field_name=child_name) create_and_append_m2m_fk(
model=model_field.owner, model_field=model_field, field_name=child_name
)
create_pydantic_field(parent_name, model, model_field) create_pydantic_field(parent_name, model_field.to, model_field)
create_pydantic_field(child_name, child, model_field) create_pydantic_field(child_name, model_field.owner, model_field)
def create_and_append_m2m_fk( def create_and_append_m2m_fk(
model: Type["Model"], model_field: Type[ManyToManyField], field_name: str model: Type["Model"], model_field: Type[ManyToManyField], field_name: str
) -> None: ) -> None:
""" """
Registers sqlalchemy Column with sqlalchemy.ForeignKey leadning to the model. Registers sqlalchemy Column with sqlalchemy.ForeignKey leading to the model.
Newly created field is added to m2m relation through model Meta columns and table. Newly created field is added to m2m relation through model Meta columns and table.
:param field_name: name of the column to create
:type field_name: str
:param model: Model class to which FK should be created :param model: Model class to which FK should be created
:type model: Model class :type model: Model class
:param model_field: field with ManyToMany relation :param model_field: field with ManyToMany relation
@ -136,6 +125,8 @@ def sqlalchemy_columns_from_model_fields(
Append fields to columns if it's not pydantic_only, Append fields to columns if it's not pydantic_only,
virtual ForeignKey or ManyToMany field. virtual ForeignKey or ManyToMany field.
Sets `owner` on each model_field as reference to newly created Model.
:raises ModelDefinitionError: if validation of related_names fail, :raises ModelDefinitionError: if validation of related_names fail,
or pkname validation fails. or pkname validation fails.
:param model_fields: dictionary of declared ormar model fields :param model_fields: dictionary of declared ormar model fields
@ -155,6 +146,7 @@ def sqlalchemy_columns_from_model_fields(
columns = [] columns = []
pkname = None pkname = None
for field_name, field in model_fields.items(): for field_name, field in model_fields.items():
field.owner = new_model
if field.primary_key: if field.primary_key:
pkname = check_pk_column_validity(field_name, field, pkname) pkname = check_pk_column_validity(field_name, field, pkname)
if ( if (

View File

@ -446,12 +446,10 @@ class NewBaseModel(pydantic.BaseModel, ModelTableProxy, metaclass=ModelMetaclass
globalns.setdefault(cls.__name__, cls) globalns.setdefault(cls.__name__, cls)
fields_to_check = cls.Meta.model_fields.copy() fields_to_check = cls.Meta.model_fields.copy()
for field_name, field in fields_to_check.items(): for field_name, field in fields_to_check.items():
if issubclass(field, ForeignKeyField): if field.has_unresolved_forward_refs():
field.evaluate_forward_ref(globalns=globalns, localns=localns) field.evaluate_forward_ref(globalns=globalns, localns=localns)
expand_reverse_relationship( field.set_self_reference_flag()
model=cls, # type: ignore expand_reverse_relationship(model_field=field)
model_field=field,
)
register_relation_in_alias_manager( register_relation_in_alias_manager(
cls, # type: ignore cls, # type: ignore
field, field,

View File

@ -41,28 +41,28 @@ class ChildFriends(ormar.Model):
database = db database = db
class Child(ormar.Model): # class Child(ormar.Model):
class Meta(ModelMeta): # class Meta(ModelMeta):
metadata = metadata # metadata = metadata
database = db # database = db
#
id: int = ormar.Integer(primary_key=True) # id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100) # name: str = ormar.String(max_length=100)
favourite_game: Game = ormar.ForeignKey(Game, related_name="liked_by") # favourite_game: Game = ormar.ForeignKey(Game, related_name="liked_by")
least_favourite_game: Game = ormar.ForeignKey(Game, related_name="not_liked_by") # least_favourite_game: Game = ormar.ForeignKey(Game, related_name="not_liked_by")
friends: List[Child] = ormar.ManyToMany(Child, through=ChildFriends) # friends: List[Child] = ormar.ManyToMany(Child, through=ChildFriends)
#
#
# class Game(ormar.Model):
# class Meta(ModelMeta):
# metadata = metadata
# database = db
#
# id: int = ormar.Integer(primary_key=True)
# name: str = ormar.String(max_length=100)
class Game(ormar.Model): # Child.update_forward_refs()
class Meta(ModelMeta):
metadata = metadata
database = db
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
Child.update_forward_refs()
@pytest.fixture(autouse=True, scope="module") @pytest.fixture(autouse=True, scope="module")
@ -125,22 +125,22 @@ async def test_self_relation():
assert sam_check.employees[0].name == "Joe" assert sam_check.employees[0].name == "Joe"
@pytest.mark.asyncio # @pytest.mark.asyncio
async def test_other_forwardref_relation(): # async def test_other_forwardref_relation():
checkers = await Game.objects.create(name="checkers") # checkers = await Game.objects.create(name="checkers")
uno = await Game(name="Uno").save() # uno = await Game(name="Uno").save()
#
await Child(name="Billy", favourite_game=uno, least_favourite_game=checkers).save() # await Child(name="Billy", favourite_game=uno, least_favourite_game=checkers).save()
await Child(name="Kate", favourite_game=checkers, least_favourite_game=uno).save() # await Child(name="Kate", favourite_game=checkers, least_favourite_game=uno).save()
#
billy_check = await Child.objects.select_related( # billy_check = await Child.objects.select_related(
["favourite_game", "least_favourite_game"] # ["favourite_game", "least_favourite_game"]
).get(name="Billy") # ).get(name="Billy")
assert billy_check.favourite_game == uno # assert billy_check.favourite_game == uno
assert billy_check.least_favourite_game == checkers # assert billy_check.least_favourite_game == checkers
#
uno_check = await Game.objects.select_related(["liked_by", "not_liked_by"]).get( # uno_check = await Game.objects.select_related(["liked_by", "not_liked_by"]).get(
name="Uno" # name="Uno"
) # )
assert uno_check.liked_by[0].name == "Billy" # assert uno_check.liked_by[0].name == "Billy"
assert uno_check.not_liked_by[0].name == "Kate" # assert uno_check.not_liked_by[0].name == "Kate"