codecov-io: # [Codecov](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=h1) Report
> Merging [#138](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=desc) into [master](https://codecov.io/gh/proteanhq/protean/commit/42384231376f6c2a732321021b60302242d27618?src=pr&el=desc) will **increase** coverage by `0.07%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/proteanhq/protean/pull/138/graphs/tree.svg?width=650&token=0sFuFdLBOx&height=150&src=pr)](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #138      +/-   ##
==========================================
+ Coverage    82.6%   82.68%   +0.07%     
==========================================
  Files          46       46              
  Lines        2185     2194       +9     
  Branches      320      324       +4     
==========================================
+ Hits         1805     1814       +9     
  Misses        259      259              
  Partials      121      121
```


| [Impacted Files](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/protean/core/repository/factory.py](https://codecov.io/gh/proteanhq/protean/pull/138/diff?src=pr&el=tree#diff-c3JjL3Byb3RlYW4vY29yZS9yZXBvc2l0b3J5L2ZhY3RvcnkucHk=) | `91.04% <100%> (+0.41%)` | :arrow_up: |
| [src/protean/core/entity.py](https://codecov.io/gh/proteanhq/protean/pull/138/diff?src=pr&el=tree#diff-c3JjL3Byb3RlYW4vY29yZS9lbnRpdHkucHk=) | `91.53% <100%> (+0.2%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=footer). Last update [4238423...0737cfd](https://codecov.io/gh/proteanhq/protean/pull/138?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py
index e3c23e18..82d80926 100644
--- a/src/protean/core/entity.py
+++ b/src/protean/core/entity.py
@@ -3,6 +3,7 @@ import copy
 import logging
 from typing import Any
 
+from protean.core.exceptions import NotSupportedError
 from protean.core.exceptions import ObjectNotFoundError
 from protean.core.exceptions import ValidationError
 from protean.core.field import Auto
@@ -38,6 +39,11 @@ class EntityBase(type):
         if not parents:
             return super().__new__(mcs, name, bases, attrs)
 
+        # Remove `abstract` in base classes if defined
+        for base in bases:
+            if hasattr(base.Meta, 'abstract'):
+                delattr(base.Meta, 'abstract')
+
         new_class = super().__new__(mcs, name, bases, attrs, **kwargs)
 
         # Gather `Meta` class/object if defined
@@ -149,6 +155,14 @@ class EntityMeta:
     - ``provider``: the name of the datasource associated with this
         entity, default value is `default`.
     - ``order_by``: default ordering of objects returned by filter queries.
+
+    Also acts as a placeholder for generated entity fields like:
+
+        :declared_fields: dict
+            Any instances of `Field` included as attributes on either the class
+            or on any of its superclasses will be include in this dictionary.
+        :id_field: protean.core.Field
+            An instance of the field that will serve as the unique identifier for the entity
     """
 
     def __init__(self, entity_name, meta):
@@ -253,13 +267,7 @@ class Entity(metaclass=EntityBase):
     class Meta:
         """Options object for an Entity.
 
-        Acts as a placeholder for generated entity fields like:
-
-            :declared_fields: dict
-                Any instances of `Field` included as attributes on either the class
-                or on any of its superclasses will be include in this dictionary.
-            :id_field: protean.core.Field
-                An instance of the field that will serve as the unique identifier for the entity
+        Check ``EntityMeta`` class for full documentation.
         """
 
     def __init__(self, *template, **kwargs):
@@ -272,6 +280,11 @@ class Entity(metaclass=EntityBase):
             can even use a template for initial data.
         """
 
+        if self.meta_.abstract is True:
+            raise NotSupportedError(
+                f'{self.__class__.__name__} class has been marked abstract'
+                f' and cannot be instantiated')
+
         self.errors = {}
 
         # Set up the storage for instance state
diff --git a/src/protean/core/repository/factory.py b/src/protean/core/repository/factory.py
index b98e007f..5b543035 100644
--- a/src/protean/core/repository/factory.py
+++ b/src/protean/core/repository/factory.py
@@ -4,6 +4,7 @@ from collections import namedtuple
 from threading import local
 
 from protean.core.exceptions import ConfigurationError
+from protean.core.exceptions import NotSupportedError
 from protean.core.provider import providers
 from protean.utils.generic import fully_qualified_name
 
@@ -32,9 +33,8 @@ class RepositoryFactory:
 
     def register(self, entity_cls, provider_name=None):
         """ Register the given model with the factory
-        :param model_cls: class of the model to be registered
-        :param adapter_cls: Optional adapter class to use if not the
-        `Adapter` defined by the provider is user
+        :param entity_cls: Entity class to be registered
+        :param provider: Optional provider to associate with Entity class
         """
         self._validate_entity_cls(entity_cls)
 
@@ -104,7 +104,12 @@ class RepositoryFactory:
 
         if not issubclass(entity_cls, Entity):
             raise AssertionError(
-                f'Entity {entity_cls} must be subclass of `Entity`')
+                f'Entity {entity_cls.__name__} must be subclass of `Entity`')
+
+        if entity_cls.meta_.abstract is True:
+            raise NotSupportedError(
+                f'{entity_cls.__name__} class has been marked abstract'
+                f' and cannot be instantiated')
 
     def get_model(self, entity_cls):
         """Retrieve Model class connected to Entity"""

Add ability to define Abstract Entities
Such abstract entities will then serve as templates/parents of concrete Entities.
diff --git a/tests/core/test_entity.py b/tests/core/test_entity.py
index a4f79fd0..0e56cca3 100644
--- a/tests/core/test_entity.py
+++ b/tests/core/test_entity.py
@@ -1,6 +1,7 @@
 """Tests for Entity Functionality and Base Classes"""
 
 from collections import OrderedDict
+from datetime import datetime
 
 import pytest
 from tests.support.dog import Dog
@@ -11,6 +12,7 @@ from tests.support.human import Human
 
 from protean.core import field
 from protean.core.entity import Entity
+from protean.core.exceptions import NotSupportedError
 from protean.core.exceptions import ObjectNotFoundError
 from protean.core.exceptions import ValidationError
 from protean.core.queryset import QuerySet
@@ -661,6 +663,78 @@ class TestEntity:
         with pytest.raises(SystemError):
             immortal_dog.delete()
 
+    def test_abstract(self):
+        """Test that abstract entities cannot be initialized"""
+        class AbstractDog(Entity):
+            """A Dog that cannot Live!"""
+            name = field.String(required=True, unique=True, max_length=50)
+            age = field.Integer(default=5)
+            owner = field.String(required=True, max_length=15)
+
+            class Meta:
+                abstract = True
+
+        with pytest.raises(NotSupportedError) as exc1:
+            from protean.core.repository import repo_factory
+            repo_factory.register(AbstractDog)
+        assert exc1.value.args[0] == ('AbstractDog class has been marked abstract'
+                                      ' and cannot be instantiated')
+
+        with pytest.raises(NotSupportedError) as exc2:
+            AbstractDog(name='Titan', age=10001, owner='God')
+        assert exc2.value.args[0] == ('AbstractDog class has been marked abstract'
+                                      ' and cannot be instantiated')
+
+    def test_abstract_inheritance(self):
+        """Test that abstract entities cannot be initialized"""
+        class AbstractDog(Entity):
+            """A Dog that cannot Live!"""
+            age = field.Integer(default=5)
+
+            class Meta:
+                abstract = True
+
+        class ConcreteDog(AbstractDog):
+            """A Dog that inherits aging and death"""
+            name = field.String(required=True, unique=True, max_length=50)
+            owner = field.String(required=True, max_length=15)
+
+        immortal_dog = ConcreteDog(name='Titan', owner='God')
+        assert immortal_dog is not None
+        assert immortal_dog.age == 5
+
+    def test_two_level_abstract_inheritance(self):
+        """Test that abstract entities cannot be initialized"""
+        class AbstractDog(Entity):
+            """A Dog that cannot Live!"""
+            age = field.Integer(default=5)
+
+            class Meta:
+                abstract = True
+
+        class DogWithRecords(AbstractDog):
+            """A Dog that has medical records"""
+            born_at = field.DateTime(default=datetime.now())
+
+            class Meta:
+                abstract = True
+
+        class ConcreteDog(DogWithRecords):
+            """A Dog that inherits aging and death, with medical records"""
+            name = field.String(required=True, unique=True, max_length=50)
+            owner = field.String(required=True, max_length=15)
+
+        ordinary_dog = ConcreteDog(name='Titan', owner='God')
+        assert ordinary_dog is not None
+        assert ordinary_dog.age == 5
+        assert ordinary_dog.born_at is not None
+
+        with pytest.raises(NotSupportedError) as exc1:
+            from protean.core.repository import repo_factory
+            repo_factory.register(DogWithRecords)
+        assert exc1.value.args[0] == ('DogWithRecords class has been marked abstract'
+                                      ' and cannot be instantiated')
+
 
 class TestEntityMetaAttributes:
     """Class that holds testcases for Entity's meta attributes"""
diff --git a/tests/core/test_entity_associations.py b/tests/core/test_entity_associations.py
index 62fc8bbe..f52cf975 100644
--- a/tests/core/test_entity_associations.py
+++ b/tests/core/test_entity_associations.py
@@ -21,11 +21,7 @@ from tests.support.human import Human
 
 from protean.core.exceptions import ValidationError
 from protean.core.queryset import QuerySet
-<<<<<<< HEAD
-from protean.core.repository import Pagination
-=======
 from protean.core.repository import ResultSet
->>>>>>> 930ae3e64b2af179fecacd853dccd2f45fa916c4
 
 
 class TestReference:
@@ -306,11 +302,7 @@ class TestHasMany:
         assert 'dogs' in human.__dict__  # Avaiable after access
 
         assert isinstance(human.dogs, QuerySet)
-<<<<<<< HEAD
-        assert isinstance(human.dogs.all(), Pagination)
-=======
         assert isinstance(human.dogs.all(), ResultSet)
->>>>>>> 930ae3e64b2af179fecacd853dccd2f45fa916c4
         assert all(dog.id in [101, 102] for dog in human.dogs)  # `__iter__` magic here
 
     def test_init_with_via(self):

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-mock mock
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.16
babel==2.17.0
certifi==2025.1.31
charset-normalizer==3.4.1
check-manifest==0.37
Click==7.0
coverage==4.5.1
distlib==0.3.9
docutils==0.14
exceptiongroup==1.2.2
filelock==3.18.0
flake8==7.2.0
idna==3.10
imagesize==1.4.1
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mccabe==0.7.0
mock==5.2.0
packaging==24.2
pkginfo==1.12.1.2
platformdirs==4.3.7
pluggy==0.13.1
-e git+https://github.com/proteanhq/protean.git@42384231376f6c2a732321021b60302242d27618#egg=protean
py==1.11.0
pycodestyle==2.13.0
pyflakes==3.3.1
Pygments==2.19.1
pytest==7.4.4
pytest-flake8==1.0.4
pytest-mock==3.14.0
python-dateutil==2.7.3
requests==2.32.3
requests-toolbelt==1.0.0
six==1.17.0
snowballstemmer==2.2.0
Sphinx==1.8.3
sphinxcontrib-serializinghtml==2.0.0
sphinxcontrib-websupport==1.2.4
tomli==2.2.1
tox==3.1.2
tqdm==4.67.1
twine==1.11.0
urllib3==2.3.0
virtualenv==20.29.3

name: protean
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.16
      - babel==2.17.0
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - check-manifest==0.37
      - click==7.0
      - coverage==4.5.1
      - distlib==0.3.9
      - docutils==0.14
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - flake8==7.2.0
      - idna==3.10
      - imagesize==1.4.1
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - mock==5.2.0
      - packaging==24.2
      - pkginfo==1.12.1.2
      - platformdirs==4.3.7
      - pluggy==0.13.1
      - py==1.11.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pygments==2.19.1
      - pytest==7.4.4
      - pytest-flake8==1.0.4
      - pytest-mock==3.14.0
      - python-dateutil==2.7.3
      - requests==2.32.3
      - requests-toolbelt==1.0.0
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==1.8.3
      - sphinxcontrib-serializinghtml==2.0.0
      - sphinxcontrib-websupport==1.2.4
      - tomli==2.2.1
      - tox==3.1.2
      - tqdm==4.67.1
      - twine==1.11.0
      - urllib3==2.3.0
      - virtualenv==20.29.3
prefix: /opt/conda/envs/protean

tests/core/test_entity.py::TestEntity::test_abstract tests/core/test_entity.py::TestEntity::test_two_level_abstract_inheritance
tests/core/test_entity.py::TestEntity::test_init tests/core/test_entity.py::TestEntity::test_individuality tests/core/test_entity.py::TestEntity::test_required_fields tests/core/test_entity.py::TestEntity::test_defaults tests/core/test_entity.py::TestEntity::test_validate_string_length tests/core/test_entity.py::TestEntity::test_validate_data_value_against_type tests/core/test_entity.py::TestEntity::test_template_init tests/core/test_entity.py::TestEntity::test_error_messages tests/core/test_entity.py::TestEntity::test_entity_inheritance tests/core/test_entity.py::TestEntity::test_default_id tests/core/test_entity.py::TestEntity::test_to_dict tests/core/test_entity.py::TestEntity::test_get tests/core/test_entity.py::TestEntity::test_get_object_not_found_error tests/core/test_entity.py::TestEntity::test_find_by tests/core/test_entity.py::TestEntity::test_find_by_object_not_found_error tests/core/test_entity.py::TestEntity::test_find_by_multiple_attributes tests/core/test_entity.py::TestEntity::test_find_by_multiple_attributes_object_not_found_error tests/core/test_entity.py::TestEntity::test_create_error tests/core/test_entity.py::TestEntity::test_create tests/core/test_entity.py::TestEntity::test_save tests/core/test_entity.py::TestEntity::test_save_validation_error tests/core/test_entity.py::TestEntity::test_save_invalid_value tests/core/test_entity.py::TestEntity::test_update_with_invalid_id tests/core/test_entity.py::TestEntity::test_update_with_dict tests/core/test_entity.py::TestEntity::test_update_with_kwargs tests/core/test_entity.py::TestEntity::test_update_with_dict_and_kwargs tests/core/test_entity.py::TestEntity::test_that_update_runs_validations tests/core/test_entity.py::TestEntity::test_update_by tests/core/test_entity.py::TestEntity::test_update_all_with_args tests/core/test_entity.py::TestEntity::test_update_all_with_kwargs tests/core/test_entity.py::TestEntity::test_unique tests/core/test_entity.py::TestEntity::test_query_init tests/core/test_entity.py::TestEntity::test_filter_chain_initialization_from_entity tests/core/test_entity.py::TestEntity::test_filter_chaining tests/core/test_entity.py::TestEntity::test_filter_chain_results_1 tests/core/test_entity.py::TestEntity::test_filter_chain_results_2 tests/core/test_entity.py::TestEntity::test_filter_chain_results_3 tests/core/test_entity.py::TestEntity::test_filter_norm tests/core/test_entity.py::TestEntity::test_exclude tests/core/test_entity.py::TestEntity::test_exclude_multiple tests/core/test_entity.py::TestEntity::test_comparisons tests/core/test_entity.py::TestEntity::test_invalid_comparison_on_query_evaluation tests/core/test_entity.py::TestEntity::test_result_traversal tests/core/test_entity.py::TestEntity::test_delete tests/core/test_entity.py::TestEntity::test_delete_all tests/core/test_entity.py::TestEntity::test_delete_all_by_filter tests/core/test_entity.py::TestEntity::test_delete_by tests/core/test_entity.py::TestEntity::test_filter_returns_q_object tests/core/test_entity.py::TestEntity::test_escaped_quotes_in_values tests/core/test_entity.py::TestEntity::test_override tests/core/test_entity.py::TestEntity::test_abstract_inheritance tests/core/test_entity.py::TestEntityMetaAttributes::test_entity_structure tests/core/test_entity.py::TestEntityMetaAttributes::test_meta_overriding_abstract tests/core/test_entity.py::TestEntityMetaAttributes::test_meta_overriding_schema_name tests/core/test_entity.py::TestEntityMetaAttributes::test_meta_overriding_provider tests/core/test_entity.py::TestEntityMetaAttributes::test_meta_overriding_order_by tests/core/test_entity.py::TestEntityMetaAttributes::test_meta_on_init tests/core/test_entity.py::TestEntityMetaAttributes::test_declared_fields_normal tests/core/test_entity.py::TestEntityMetaAttributes::test_declared_fields_with_reference tests/core/test_entity.py::TestEntityMetaAttributes::test_declared_fields_with_hasone_association tests/core/test_entity_associations.py::TestReference::test_init tests/core/test_entity_associations.py::TestReference::test_init_with_string_reference tests/core/test_entity_associations.py::TestReference::test_save tests/core/test_entity_associations.py::TestReference::test_unsaved_entity_init tests/core/test_entity_associations.py::TestReference::test_unsaved_entity_assign tests/core/test_entity_associations.py::TestReference::test_invalid_entity_type tests/core/test_entity_associations.py::TestReference::test_shadow_attribute tests/core/test_entity_associations.py::TestReference::test_save_after_assign tests/core/test_entity_associations.py::TestReference::test_fetch_after_save tests/core/test_entity_associations.py::TestReference::test_shadow_attribute_init tests/core/test_entity_associations.py::TestReference::test_shadow_attribute_assign tests/core/test_entity_associations.py::TestReference::test_reference_reset_association_to_None tests/core/test_entity_associations.py::TestReference::test_reference_reset_shadow_field_to_None tests/core/test_entity_associations.py::TestReference::test_reference_reset_association_by_del tests/core/test_entity_associations.py::TestReference::test_reference_reset_shadow_field_by_del tests/core/test_entity_associations.py::TestReference::test_via tests/core/test_entity_associations.py::TestReference::test_via_with_shadow_attribute_assign tests/core/test_entity_associations.py::TestReference::test_caching tests/core/test_entity_associations.py::TestHasOne::test_init tests/core/test_entity_associations.py::TestHasOne::test_init_with_via tests/core/test_entity_associations.py::TestHasOne::test_init_with_no_reference tests/core/test_entity_associations.py::TestHasOne::test_caching tests/core/test_entity_associations.py::TestHasMany::test_init tests/core/test_entity_associations.py::TestHasMany::test_init_with_via tests/core/test_entity_associations.py::TestHasMany::test_init_with_no_reference tests/core/test_entity_associations.py::TestHasMany::test_caching
src/protean/core/entity.py src/protean/core/repository/factory.py
src/protean/core/entity.py src/protean/core/repository/factory.py