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 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..1f187aed3 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] @@ -344,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 @@ -361,11 +362,17 @@ 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 + 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) + ): 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 @@ -604,7 +611,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) 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"