From 3b9c8b323b77160f99c7e3df39575d0f46a81e78 Mon Sep 17 00:00:00 2001 From: collerek Date: Tue, 1 Jun 2021 18:51:06 +0200 Subject: [PATCH] add exclude_parent_fields param and first test --- ormar/models/helpers/__init__.py | 2 + ormar/models/helpers/models.py | 2 + ormar/models/helpers/pydantic.py | 14 ++++ ormar/models/metaclass.py | 12 +++- ...est_excluding_parent_fields_inheritance.py | 67 +++++++++++++++++++ .../test_inheritance_concrete.py | 11 +-- 6 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py diff --git a/ormar/models/helpers/__init__.py b/ormar/models/helpers/__init__.py index af84d9e..146aab4 100644 --- a/ormar/models/helpers/__init__.py +++ b/ormar/models/helpers/__init__.py @@ -8,6 +8,7 @@ from ormar.models.helpers.pydantic import ( get_potential_fields, get_pydantic_base_orm_config, get_pydantic_field, + remove_excluded_parent_fields, ) from ormar.models.helpers.relations import ( alias_manager, @@ -36,4 +37,5 @@ __all__ = [ "sqlalchemy_columns_from_model_fields", "populate_choices_validators", "meta_field_not_set", + "remove_excluded_parent_fields", ] diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py index 865a7d6..bc56e1a 100644 --- a/ormar/models/helpers/models.py +++ b/ormar/models/helpers/models.py @@ -54,6 +54,8 @@ def populate_default_options_values( new_model.Meta.abstract = False if not hasattr(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( is_field_an_forward_ref(field) for field in new_model.Meta.model_fields.values() diff --git a/ormar/models/helpers/pydantic.py b/ormar/models/helpers/pydantic.py index e844246..c010652 100644 --- a/ormar/models/helpers/pydantic.py +++ b/ormar/models/helpers/pydantic.py @@ -117,3 +117,17 @@ def get_potential_fields(attrs: Dict) -> Dict: for k, v in attrs.items() 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 + } diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 854434e..c0d752f 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -44,6 +44,7 @@ from ormar.models.helpers import ( populate_meta_sqlalchemy_table_if_required, populate_meta_tablename_columns_and_pk, register_relation_in_alias_manager, + remove_excluded_parent_fields, sqlalchemy_columns_from_model_fields, ) from ormar.models.quick_access_views import quick_access_set @@ -80,6 +81,7 @@ class ModelMeta: abstract: bool requires_ref_update: bool orders_by: List[str] + exclude_parent_fields: List[str] 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]], ) -> 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. Only abstract classes can be subclassed. @@ -351,6 +353,11 @@ def copy_data_from_parent_model( # noqa: CCR001 else attrs.get("__name__", "").lower() + "s" ) 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: field = cast(ManyToManyField, field) 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]], ) -> 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 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) + remove_excluded_parent_fields(new_model) return new_model diff --git a/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py b/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py new file mode 100644 index 0000000..b1f0882 --- /dev/null +++ b/tests/test_inheritance_and_pydantic_generation/test_excluding_parent_fields_inheritance.py @@ -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 diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py index ac059e4..41eac11 100644 --- a/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_concrete.py @@ -121,13 +121,6 @@ class Bus(Car): max_persons: int = ormar.Integer() -# class PersonsCar(ormar.Model): -# class Meta: -# tablename = "cars_x_persons" -# metadata = metadata -# database = db - - class Car2(ormar.Model): class Meta: abstract = True @@ -138,9 +131,7 @@ class Car2(ormar.Model): name: str = ormar.String(max_length=50) owner: Person = ormar.ForeignKey(Person, related_name="owned") co_owners: List[Person] = ormar.ManyToMany( - Person, - # through=PersonsCar, - related_name="coowned", + Person, related_name="coowned", ) created_date: datetime.datetime = ormar.DateTime(default=datetime.datetime.now)