add exclude_parent_fields param and first test

This commit is contained in:
collerek
2021-06-01 18:51:06 +02:00
parent 7a84577613
commit 3b9c8b323b
6 changed files with 96 additions and 12 deletions

View File

@ -8,6 +8,7 @@ from ormar.models.helpers.pydantic import (
get_potential_fields, get_potential_fields,
get_pydantic_base_orm_config, get_pydantic_base_orm_config,
get_pydantic_field, get_pydantic_field,
remove_excluded_parent_fields,
) )
from ormar.models.helpers.relations import ( from ormar.models.helpers.relations import (
alias_manager, alias_manager,
@ -36,4 +37,5 @@ __all__ = [
"sqlalchemy_columns_from_model_fields", "sqlalchemy_columns_from_model_fields",
"populate_choices_validators", "populate_choices_validators",
"meta_field_not_set", "meta_field_not_set",
"remove_excluded_parent_fields",
] ]

View File

@ -54,6 +54,8 @@ def populate_default_options_values(
new_model.Meta.abstract = False new_model.Meta.abstract = False
if not hasattr(new_model.Meta, "orders_by"): if not hasattr(new_model.Meta, "orders_by"):
new_model.Meta.orders_by = [] new_model.Meta.orders_by = []
if not hasattr(new_model.Meta, "exclude_parent_fields"):
new_model.Meta.exclude_parent_fields = []
if any( if any(
is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values() is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values()

View File

@ -117,3 +117,17 @@ def get_potential_fields(attrs: Dict) -> Dict:
for k, v in attrs.items() for k, v in attrs.items()
if (lenient_issubclass(v, BaseField) or isinstance(v, BaseField)) if (lenient_issubclass(v, BaseField) or isinstance(v, BaseField))
} }
def remove_excluded_parent_fields(model: Type["Model"]):
"""
Removes pydantic fields that should be excluded from parent models
:param model:
:type model: Type["Model"]
"""
excludes = {*model.Meta.exclude_parent_fields} - {*model.Meta.model_fields.keys()}
if excludes:
model.__fields__ = {
k: v for k, v in model.__fields__.items() if k not in excludes
}

View File

@ -44,6 +44,7 @@ from ormar.models.helpers import (
populate_meta_sqlalchemy_table_if_required, populate_meta_sqlalchemy_table_if_required,
populate_meta_tablename_columns_and_pk, populate_meta_tablename_columns_and_pk,
register_relation_in_alias_manager, register_relation_in_alias_manager,
remove_excluded_parent_fields,
sqlalchemy_columns_from_model_fields, sqlalchemy_columns_from_model_fields,
) )
from ormar.models.quick_access_views import quick_access_set from ormar.models.quick_access_views import quick_access_set
@ -80,6 +81,7 @@ class ModelMeta:
abstract: bool abstract: bool
requires_ref_update: bool requires_ref_update: bool
orders_by: List[str] orders_by: List[str]
exclude_parent_fields: List[str]
def add_cached_properties(new_model: Type["Model"]) -> None: def add_cached_properties(new_model: Type["Model"]) -> None:
@ -308,7 +310,7 @@ def copy_data_from_parent_model( # noqa: CCR001
model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]], model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]],
) -> Tuple[Dict, Dict]: ) -> Tuple[Dict, Dict]:
""" """
Copy the key parameters [databse, metadata, property_fields and constraints] Copy the key parameters [database, metadata, property_fields and constraints]
and fields from parent models. Overwrites them if needed. and fields from parent models. Overwrites them if needed.
Only abstract classes can be subclassed. Only abstract classes can be subclassed.
@ -351,6 +353,11 @@ def copy_data_from_parent_model( # noqa: CCR001
else attrs.get("__name__", "").lower() + "s" else attrs.get("__name__", "").lower() + "s"
) )
for field_name, field in base_class.Meta.model_fields.items(): for field_name, field in base_class.Meta.model_fields.items():
if (
hasattr(meta, "exclude_parent_fields")
and field_name in meta.exclude_parent_fields
):
continue
if field.is_multi: if field.is_multi:
field = cast(ManyToManyField, field) field = cast(ManyToManyField, field)
copy_and_replace_m2m_through_model( copy_and_replace_m2m_through_model(
@ -386,7 +393,7 @@ def extract_from_parents_definition( # noqa: CCR001
model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]], model_fields: Dict[str, Union[BaseField, ForeignKeyField, ManyToManyField]],
) -> Tuple[Dict, Dict]: ) -> Tuple[Dict, Dict]:
""" """
Extracts fields from base classes if they have valid oramr fields. Extracts fields from base classes if they have valid ormar fields.
If model was already parsed -> fields definitions need to be removed from class If model was already parsed -> fields definitions need to be removed from class
cause pydantic complains about field re-definition so after first child cause pydantic complains about field re-definition so after first child
@ -595,6 +602,7 @@ class ModelMetaclass(pydantic.main.ModelMetaclass):
) )
new_model.pk = PkDescriptor(name=new_model.Meta.pkname) new_model.pk = PkDescriptor(name=new_model.Meta.pkname)
remove_excluded_parent_fields(new_model)
return new_model return new_model

View File

@ -0,0 +1,67 @@
import datetime
from typing import List, Optional
import databases
import pytest
import sqlalchemy as sa
from sqlalchemy import create_engine
import ormar
from ormar import ModelDefinitionError, property_field
from ormar.exceptions import ModelError
from tests.settings import DATABASE_URL
metadata = sa.MetaData()
db = databases.Database(DATABASE_URL)
engine = create_engine(DATABASE_URL)
class AuditModel(ormar.Model):
class Meta:
abstract = True
created_by: str = ormar.String(max_length=100)
updated_by: str = ormar.String(max_length=100, default="Sam")
class DateFieldsModel(ormar.Model):
class Meta(ormar.ModelMeta):
abstract = True
metadata = metadata
database = db
created_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="creation_date"
)
updated_date: datetime.datetime = ormar.DateTime(
default=datetime.datetime.now, name="modification_date"
)
class Category(DateFieldsModel, AuditModel):
class Meta(ormar.ModelMeta):
tablename = "categories"
exclude_parent_fields = ["updated_by", "updated_date"]
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=50, unique=True, index=True)
code: int = ormar.Integer()
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
metadata.create_all(engine)
yield
metadata.drop_all(engine)
def test_model_definition():
model_fields = Category.Meta.model_fields
sqlalchemy_columns = Category.Meta.table.c
pydantic_columns = Category.__fields__
assert "updated_by" not in model_fields
assert "updated_by" not in sqlalchemy_columns
assert "updated_by" not in pydantic_columns
assert "updated_date" not in model_fields
assert "updated_date" not in sqlalchemy_columns
assert "updated_date" not in pydantic_columns

View File

@ -121,13 +121,6 @@ class Bus(Car):
max_persons: int = ormar.Integer() max_persons: int = ormar.Integer()
# class PersonsCar(ormar.Model):
# class Meta:
# tablename = "cars_x_persons"
# metadata = metadata
# database = db
class Car2(ormar.Model): class Car2(ormar.Model):
class Meta: class Meta:
abstract = True abstract = True
@ -138,9 +131,7 @@ class Car2(ormar.Model):
name: str = ormar.String(max_length=50) name: str = ormar.String(max_length=50)
owner: Person = ormar.ForeignKey(Person, related_name="owned") owner: Person = ormar.ForeignKey(Person, related_name="owned")
co_owners: List[Person] = ormar.ManyToMany( co_owners: List[Person] = ormar.ManyToMany(
Person, Person, related_name="coowned",
# through=PersonsCar,
related_name="coowned",
) )
created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now) created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)