From fa6f4a694fa28d04c5a8d0aabd6efaf454d78dcd Mon Sep 17 00:00:00 2001 From: Sepehr Date: Mon, 25 Jul 2022 12:23:21 +0430 Subject: [PATCH 1/8] docs: added proxy to supported type inheritance --- docs/models/inheritance.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/models/inheritance.md b/docs/models/inheritance.md index d0f4be638..c395b3c04 100644 --- a/docs/models/inheritance.md +++ b/docs/models/inheritance.md @@ -1,9 +1,10 @@ # Inheritance -Out of various types of ORM models inheritance `ormar` currently supports two of them: +Out of various types of ORM models inheritance `ormar` currently supports three of them: * **Mixins** * **Concrete table inheritance** (with parents set to `abstract=True`) +* **Proxy models** (with children's set to `proxy=True`) ## Types of inheritance @@ -15,6 +16,8 @@ The short summary of different types of inheritance is: * **Concrete table inheritance [SUPPORTED]** - means that parent is marked as abstract and each child has its own table with columns from a parent and own child columns, kind of similar to Mixins but parent also is a Model +* **Proxy models [SUPPORTED]** - means that only parent has an actual table, + children just add methods, modify settings etc. * **Single table inheritance [NOT SUPPORTED]** - means that only one table is created with fields that are combination/sum of the parent and all children models but child models use only subset of column in db (all parent and own ones, skipping the other @@ -23,8 +26,6 @@ The short summary of different types of inheritance is: is saved on parent model and part is saved on child model that are connected to each other by kind of one to one relation and under the hood you operate on two models at once -* **Proxy models [NOT SUPPORTED]** - means that only parent has an actual table, - children just add methods, modify settings etc. ## Mixins From ba1e5e576ed1f0a86e5174319d23b511574374bb Mon Sep 17 00:00:00 2001 From: Sepehr Date: Wed, 27 Jul 2022 13:43:01 +0430 Subject: [PATCH 2/8] feat: added boolean proxy field in meta --- ormar/models/helpers/models.py | 1 + ormar/models/metaclass.py | 1 + 2 files changed, 2 insertions(+) diff --git a/ormar/models/helpers/models.py b/ormar/models/helpers/models.py index 5bc0a0f3d..21e079914 100644 --- a/ormar/models/helpers/models.py +++ b/ormar/models/helpers/models.py @@ -51,6 +51,7 @@ def populate_default_options_values( # noqa: CCR001 "constraints": [], "model_fields": model_fields, "abstract": False, + "proxy": False, "extra": Extra.forbid, "orders_by": [], "exclude_parent_fields": [], diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 208e0eb9a..c92a4fbc1 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -83,6 +83,7 @@ class ModelMeta: property_fields: Set signals: SignalEmitter abstract: bool + proxy: bool requires_ref_update: bool orders_by: List[str] exclude_parent_fields: List[str] From 7fa1578b42a82db3468d1cf5675d18a87b759523 Mon Sep 17 00:00:00 2001 From: Sepehr Date: Thu, 28 Jul 2022 12:15:36 +0430 Subject: [PATCH 3/8] refactor: check inherit abstract without proxy --- ormar/models/metaclass.py | 105 ++++++++++++++++++++------------------ 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index c92a4fbc1..fc50ee7a8 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -362,59 +362,64 @@ def copy_data_from_parent_model( # noqa: CCR001 :return: updated attrs and model_fields :rtype: Tuple[Dict, Dict] """ - if attrs.get("Meta"): - if model_fields and not base_class.Meta.abstract: # type: ignore - raise ModelDefinitionError( - f"{curr_class.__name__} cannot inherit " - f"from non abstract class {base_class.__name__}" - ) - update_attrs_from_base_meta( - base_class=base_class, # type: ignore - attrs=attrs, - model_fields=model_fields, + meta = attrs.get("Meta") + if not meta: # pragma: no cover + raise ModelDefinitionError( + f"Model {curr_class.__name__} declared without Meta" ) - parent_fields: Dict = dict() - meta = attrs.get("Meta") - if not meta: # pragma: no cover - raise ModelDefinitionError( - f"Model {curr_class.__name__} declared without Meta" - ) - table_name = ( - meta.tablename - if hasattr(meta, "tablename") and meta.tablename - 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( - field=field, - field_name=field_name, - table_name=table_name, - parent_fields=parent_fields, - attrs=attrs, - meta=meta, - base_class=base_class, # type: ignore - ) - elif field.is_relation and field.related_name: - Field = type( # type: ignore - field.__class__.__name__, (ForeignKeyField, BaseField), {} - ) - copy_field = Field(**dict(field.__dict__)) - related_name = field.related_name + "_" + table_name - copy_field.related_name = related_name # type: ignore - parent_fields[field_name] = copy_field - else: - parent_fields[field_name] = field + if ( # type: ignore + model_fields + and not base_class.Meta.abstract + and not getattr(meta, "proxy", False) + ): + raise ModelDefinitionError( + f"{curr_class.__name__} cannot inherit " + f"from non abstract class {base_class.__name__}" + "except for the proxy model mode without adding new model fields." + ) + update_attrs_from_base_meta( + base_class=base_class, # type: ignore + attrs=attrs, + model_fields=model_fields, + ) + parent_fields: Dict = dict() + table_name = ( + meta.tablename + if hasattr(meta, "tablename") and meta.tablename + 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( + field=field, + field_name=field_name, + table_name=table_name, + parent_fields=parent_fields, + attrs=attrs, + meta=meta, + base_class=base_class, # type: ignore + ) - parent_fields.update(model_fields) # type: ignore - model_fields = parent_fields + elif field.is_relation and field.related_name: + Field = type( # type: ignore + field.__class__.__name__, (ForeignKeyField, BaseField), {} + ) + copy_field = Field(**dict(field.__dict__)) + related_name = field.related_name + "_" + table_name + copy_field.related_name = related_name # type: ignore + parent_fields[field_name] = copy_field + else: + parent_fields[field_name] = field + + parent_fields.update(model_fields) # type: ignore + model_fields = parent_fields return attrs, model_fields From 8dc818f5d2ec8e878f458b9c89dc98e0e12afe23 Mon Sep 17 00:00:00 2001 From: Sepehr Date: Thu, 28 Jul 2022 12:37:33 +0430 Subject: [PATCH 4/8] refactor: set table for proxy from bases parent --- ormar/models/metaclass.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index fc50ee7a8..35d922dec 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -610,7 +610,9 @@ def __new__( # type: ignore # noqa: CCR001 register_signals(new_model=new_model) populate_choices_validators(new_model) - if not new_model.Meta.abstract: + if new_model.Meta.proxy: + new_model.Meta.table = attrs.get("table") + elif not new_model.Meta.abstract: new_model = populate_meta_tablename_columns_and_pk(name, new_model) populate_meta_sqlalchemy_table_if_required(new_model.Meta) expand_reverse_relationships(new_model) From 1b6e60f407dcbb69ef59b9d48ee093c59150232f Mon Sep 17 00:00:00 2001 From: Sepehr Date: Thu, 28 Jul 2022 12:39:13 +0430 Subject: [PATCH 5/8] docs: update docstring exception raised details --- ormar/models/metaclass.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 35d922dec..ca4eb8534 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -345,12 +345,12 @@ def copy_data_from_parent_model( # noqa: CCR001 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. + Only abstract or proxy classes can be subclassed. Since relation fields requires different related_name for different children - :raises ModelDefinitionError: if non abstract model is subclassed + :raises ModelDefinitionError: if non abstract model is subclassed or not proxy model :param base_class: one of the parent classes :type base_class: Model or model parent class :param curr_class: current constructed class From 43f645c08e38b2edb7bf1028897fcd0abb5ca64e Mon Sep 17 00:00:00 2001 From: Sepehr Date: Thu, 28 Jul 2022 13:13:39 +0430 Subject: [PATCH 6/8] test: write first sample tests for proxy models --- .../test_inheritance_proxy_models.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_inheritance_and_pydantic_generation/test_inheritance_proxy_models.py diff --git a/tests/test_inheritance_and_pydantic_generation/test_inheritance_proxy_models.py b/tests/test_inheritance_and_pydantic_generation/test_inheritance_proxy_models.py new file mode 100644 index 000000000..d466a4573 --- /dev/null +++ b/tests/test_inheritance_and_pydantic_generation/test_inheritance_proxy_models.py @@ -0,0 +1,54 @@ +import uuid + +import databases +import pytest +import sqlalchemy + +import ormar +from tests.settings import DATABASE_URL + +metadata = sqlalchemy.MetaData() +database = databases.Database(DATABASE_URL) + + +class MainMeta(ormar.ModelMeta): + database = database + metadata = metadata + + +class Human(ormar.Model): + class Meta(MainMeta): + pass + + id: uuid.UUID = ormar.UUID( + primary_key=True, default=uuid.uuid4, uuid_format="string" + ) + first_name: str = ormar.String(max_length=50) + last_name: str = ormar.String(max_length=50) + + +class User(Human): + class Meta(MainMeta): + proxy = True + + def full_name(self) -> str: + return f"{self.first_name} {self.last_name}" + + +@pytest.fixture(autouse=True, scope="module") +def create_test_database(): + engine = sqlalchemy.create_engine(DATABASE_URL) + metadata.drop_all(engine) + metadata.create_all(engine) + yield + metadata.drop_all(engine) + + +@pytest.mark.asyncio +async def test_method_proxy_models(): + async with database: + await Human.objects.create(first_name="foo", last_name="bar") + + users = await User.objects.all() + assert len(users) == 1 + assert users[0].full_name() == "foo bar" From 96d964dc00e183376aed28237f0ef2f5b6486d0b Mon Sep 17 00:00:00 2001 From: Sepehr Date: Thu, 28 Jul 2022 13:17:08 +0430 Subject: [PATCH 7/8] style: reformatted code with black formatter --- ormar/models/metaclass.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index ca4eb8534..19abf6769 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -364,9 +364,7 @@ def copy_data_from_parent_model( # noqa: CCR001 """ meta = attrs.get("Meta") if not meta: # pragma: no cover - raise ModelDefinitionError( - f"Model {curr_class.__name__} declared without Meta" - ) + raise ModelDefinitionError(f"Model {curr_class.__name__} declared without Meta") if ( # type: ignore model_fields From 72b4f2a9012742fa05dd674b45c3e9dddf9f81f3 Mon Sep 17 00:00:00 2001 From: Sepehr Date: Thu, 28 Jul 2022 13:36:07 +0430 Subject: [PATCH 8/8] fix: debuging check model meta in copy data func --- ormar/models/metaclass.py | 109 ++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/ormar/models/metaclass.py b/ormar/models/metaclass.py index 19abf6769..1f187aed3 100644 --- a/ormar/models/metaclass.py +++ b/ormar/models/metaclass.py @@ -362,62 +362,65 @@ def copy_data_from_parent_model( # noqa: CCR001 :return: updated attrs and model_fields :rtype: Tuple[Dict, Dict] """ - meta = attrs.get("Meta") - if not meta: # pragma: no cover - raise ModelDefinitionError(f"Model {curr_class.__name__} declared without Meta") - - if ( # type: ignore - model_fields - and not base_class.Meta.abstract - and not getattr(meta, "proxy", False) - ): - raise ModelDefinitionError( - f"{curr_class.__name__} cannot inherit " - f"from non abstract class {base_class.__name__}" - "except for the proxy model mode without adding new model fields." - ) - update_attrs_from_base_meta( - base_class=base_class, # type: ignore - attrs=attrs, - model_fields=model_fields, - ) - parent_fields: Dict = dict() - table_name = ( - meta.tablename - if hasattr(meta, "tablename") and meta.tablename - 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 + meta: Optional[ModelMeta] = attrs.get("Meta") + if meta is not None: + if ( # type: ignore + model_fields + and not base_class.Meta.abstract + and not getattr(meta, "proxy", False) ): - continue - if field.is_multi: - field = cast(ManyToManyField, field) - copy_and_replace_m2m_through_model( - field=field, - field_name=field_name, - table_name=table_name, - parent_fields=parent_fields, - attrs=attrs, - meta=meta, - base_class=base_class, # type: ignore + raise ModelDefinitionError( + f"{curr_class.__name__} cannot inherit " + f"from non abstract class {base_class.__name__}" + "except for the proxy model mode without adding new model fields." ) - - elif field.is_relation and field.related_name: - Field = type( # type: ignore - field.__class__.__name__, (ForeignKeyField, BaseField), {} + update_attrs_from_base_meta( + base_class=base_class, # type: ignore + attrs=attrs, + model_fields=model_fields, + ) + parent_fields: Dict = dict() + meta = attrs.get("Meta") + if not meta: # pragma: no cover + raise ModelDefinitionError( + f"Model {curr_class.__name__} declared without Meta" ) - copy_field = Field(**dict(field.__dict__)) - related_name = field.related_name + "_" + table_name - copy_field.related_name = related_name # type: ignore - parent_fields[field_name] = copy_field - else: - parent_fields[field_name] = field - - parent_fields.update(model_fields) # type: ignore - model_fields = parent_fields + table_name = ( + meta.tablename + if hasattr(meta, "tablename") and meta.tablename + 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( + field=field, + field_name=field_name, + table_name=table_name, + parent_fields=parent_fields, + attrs=attrs, + meta=meta, + base_class=base_class, # type: ignore + ) + + elif field.is_relation and field.related_name: + Field = type( # type: ignore + field.__class__.__name__, (ForeignKeyField, BaseField), {} + ) + copy_field = Field(**dict(field.__dict__)) + related_name = field.related_name + "_" + table_name + copy_field.related_name = related_name # type: ignore + parent_fields[field_name] = copy_field + else: + parent_fields[field_name] = field + + parent_fields.update(model_fields) # type: ignore + model_fields = parent_fields return attrs, model_fields