Skip to content

Commit

Permalink
refactor: move _search_for_field_attributes from global to ModelMet…
Browse files Browse the repository at this point in the history
…a class
  • Loading branch information
waketzheng committed Jan 21, 2025
1 parent dda50a2 commit 1be0587
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 98 deletions.
2 changes: 1 addition & 1 deletion tests/fields/test_db_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from pypika_tortoise.terms import Field

from tests.testmodels import ModelWithIndexes
from tortoise import fields
from tortoise.contrib import test
from tortoise.exceptions import ConfigurationError
from tortoise.indexes import Index
from tests.testmodels import ModelWithIndexes


class CustomIndex(Index):
Expand Down
193 changes: 96 additions & 97 deletions tortoise/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,48 +471,100 @@ def _generate_filters(self) -> None:
self.filters[key] = filter_info


# Searching for Field attributes in the class hierarchy
def _search_for_field_attributes(base: Type, attrs: dict) -> None:
"""
Searching for class attributes of type fields.Field
in the given class.
class ModelMeta(type):
__slots__ = ()

If an attribute of the class is an instance of fields.Field,
then it will be added to the fields dict. But only, if the
key is not already in the dict. So derived classes have a higher
precedence. Multiple Inheritance is supported from left to right.
def __new__(cls, name: str, bases: tuple[Type, ...], attrs: dict[str, Any]) -> "ModelMeta":
fields_db_projection: dict[str, str] = {}
meta_class: "Model.Meta" = attrs.get("Meta", type("Meta", (), {}))
pk_attr: str = "id"

After checking the given class, the function will look into
the classes according to the MRO (method resolution order).
# Start searching for fields in the base classes.
inherited_attrs: dict = {}
for base in bases:
cls._search_for_field_attributes(base, inherited_attrs)
if inherited_attrs:
# Ensure that the inherited fields are before the defined ones.
attrs = {**inherited_attrs, **attrs}
is_abstract = getattr(meta_class, "abstract", False)
if name != "Model":
attrs, pk_attr = cls._parse_custom_pk(attrs, pk_attr, name, is_abstract)
fields_map, filters, fk_fields, m2m_fields, o2o_fields = cls._dispatch_fields(
attrs, fields_db_projection, is_abstract
)

The MRO is 'natural' order, in which python traverses methods and
fields. For more information on the magic behind check out:
`The Python 2.3 Method Resolution Order
<https://www.python.org/download/releases/2.3/mro/>`_.
"""
for parent in base.__mro__[1:]:
_search_for_field_attributes(parent, attrs)
meta = getattr(base, "_meta", None)
if meta:
# For abstract classes
for key, value in meta.fields_map.items():
attrs[key] = value
# For abstract classes manager
for key, value in base.__dict__.items():
if isinstance(value, Manager) and key not in attrs:
attrs[key] = value.__class__()
else:
# For mixin classes
for key, value in base.__dict__.items():
if isinstance(value, Field) and key not in attrs:
attrs[key] = value
# Clean the class attributes
for slot in fields_map:
attrs.pop(slot, None)
attrs["_meta"] = meta = cls.build_meta(
meta_class,
fields_map,
fields_db_projection,
filters,
fk_fields,
o2o_fields,
m2m_fields,
pk_attr,
)

new_class = super().__new__(cls, name, bases, attrs)
for field in meta.fields_map.values():
field.model = new_class # type: ignore

class ModelMeta(type):
__slots__ = ()
for fname, comment in _get_comments(new_class).items(): # type: ignore
if fname in fields_map:
fields_map[fname].docstring = comment
if fields_map[fname].description is None:
fields_map[fname].description = comment.split("\n")[0]

if new_class.__doc__ and not meta.table_description:
meta.table_description = inspect.cleandoc(new_class.__doc__).split("\n")[0]
for value in attrs.values():
if isinstance(value, Manager):
value._model = new_class
meta._model = new_class # type: ignore
meta.manager._model = new_class
meta.finalise_fields()
return new_class

@classmethod
def _search_for_field_attributes(cls, base: Type, attrs: dict) -> None:
"""
Searching for class attributes of type fields.Field
in the given class.
If an attribute of the class is an instance of fields.Field,
then it will be added to the fields dict. But only, if the
key is not already in the dict. So derived classes have a higher
precedence. Multiple Inheritance is supported from left to right.
After checking the given class, the function will look into
the classes according to the MRO (method resolution order).
The MRO is 'natural' order, in which python traverses methods and
fields. For more information on the magic behind check out:
`The Python 2.3 Method Resolution Order
<https://www.python.org/download/releases/2.3/mro/>`_.
"""
for parent in base.__mro__[1:]:
# Searching for Field attributes in the class hierarchy
cls._search_for_field_attributes(parent, attrs)
if meta := getattr(base, "_meta", None):
# For abstract classes
for key, value in meta.fields_map.items():
attrs[key] = value
# For abstract classes manager
for key, value in base.__dict__.items():
if isinstance(value, Manager) and key not in attrs:
attrs[key] = value.__class__()
else:
# For mixin classes
for key, value in base.__dict__.items():
if isinstance(value, Field) and key not in attrs:
attrs[key] = value

@staticmethod
def parse_custom_pk(attrs: dict, pk_attr: str, name: str, is_abstract) -> tuple[dict, str]:
def _parse_custom_pk(attrs: dict, pk_attr: str, name: str, is_abstract) -> tuple[dict, str]:
custom_pk_present = False
for key, value in attrs.items():
if isinstance(value, Field):
Expand Down Expand Up @@ -541,7 +593,7 @@ def parse_custom_pk(attrs: dict, pk_attr: str, name: str, is_abstract) -> tuple[
return attrs, pk_attr

@staticmethod
def dispatch_fields(attrs: dict, fields_db_projection: dict, is_abstract) -> tuple[
def _dispatch_fields(attrs: dict, fields_db_projection: dict, is_abstract) -> tuple[
dict[str, Field],
dict[str, FilterInfoDict],
set[str],
Expand Down Expand Up @@ -584,14 +636,14 @@ def dispatch_fields(attrs: dict, fields_db_projection: dict, is_abstract) -> tup

@staticmethod
def build_meta(
meta_class,
fields_map,
fields_db_projection,
filters,
fk_fields,
o2o_fields,
m2m_fields,
pk_attr,
meta_class: "Model.Meta",
fields_map: dict[str, Field],
fields_db_projection: dict[str, str],
filters: dict[str, FilterInfoDict],
fk_fields: set[str],
o2o_fields: set[str],
m2m_fields: set[str],
pk_attr: str,
) -> MetaInfo:
meta = MetaInfo(meta_class)
meta.fields_map = fields_map
Expand All @@ -614,59 +666,6 @@ def build_meta(
meta.abstract = True
return meta

def __new__(cls, name: str, bases: tuple[Type, ...], attrs: dict[str, Any]) -> "ModelMeta":
fields_db_projection: dict[str, str] = {}
meta_class: "Model.Meta" = attrs.get("Meta", type("Meta", (), {}))
pk_attr: str = "id"

# Start searching for fields in the base classes.
inherited_attrs: dict = {}
for base in bases:
_search_for_field_attributes(base, inherited_attrs)
if inherited_attrs:
# Ensure that the inherited fields are before the defined ones.
attrs = {**inherited_attrs, **attrs}
is_abstract = getattr(meta_class, "abstract", False)
if name != "Model":
attrs, pk_attr = cls.parse_custom_pk(attrs, pk_attr, name, is_abstract)
fields_map, filters, fk_fields, m2m_fields, o2o_fields = cls.dispatch_fields(
attrs, fields_db_projection, is_abstract
)

# Clean the class attributes
for slot in fields_map:
attrs.pop(slot, None)
attrs["_meta"] = meta = cls.build_meta(
meta_class,
fields_map,
fields_db_projection,
filters,
fk_fields,
o2o_fields,
m2m_fields,
pk_attr,
)

new_class = super().__new__(cls, name, bases, attrs)
for field in meta.fields_map.values():
field.model = new_class # type: ignore

for fname, comment in _get_comments(new_class).items(): # type: ignore
if fname in fields_map:
fields_map[fname].docstring = comment
if fields_map[fname].description is None:
fields_map[fname].description = comment.split("\n")[0]

if new_class.__doc__ and not meta.table_description:
meta.table_description = inspect.cleandoc(new_class.__doc__).split("\n")[0]
for value in attrs.values():
if isinstance(value, Manager):
value._model = new_class
meta._model = new_class # type: ignore
meta.manager._model = new_class
meta.finalise_fields()
return new_class

def __getitem__(cls: Type[MODEL], key: Any) -> QuerySetSingle[MODEL]: # type: ignore
return cls._getbypk(key) # type: ignore

Expand Down

0 comments on commit 1be0587

Please sign in to comment.