Repository: proteanhq/protean

Issue:
Demarcate `state` and `meta` attributes into their own variables

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9afbe369..de3de61b 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -52,3 +52,17 @@ Changelog
 * Change Fields to be full-fledged Descriptors to control getting/setting values
 * Introduction of Support for References and Associations (HasOne and HasMany)
 * Remove Pylint from static code analysis and use Flake8
+
+0.0.9 (2019-03-08)
+------------------
+
+* Minor fixes for issues found while migrating SQLAlchemy plugin to 0.0.8 version
+* `delete` method should query by value of `id_field` instead of hardcoded `id`
+
+0.0.10 (2019-)
+--------------
+
+* Support for chained `update` and `delete` methods on Queryset
+* Support for `update_all` method for mass updates on objects
+* Support for `delete_all` method for mass deletion of objects
+* Documentation Structure refinement
\ No newline at end of file
diff --git a/docs/index.rst b/docs/index.rst
index f2ab773d..c6a5b945 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -68,7 +68,7 @@ Protean officially supports Python 3.6+.
    :maxdepth: 1
    :caption: Introduction
 
-   intro/index
+   intro/install
 
 
 .. toctree::
diff --git a/docs/intro/index.rst b/docs/intro/index.rst
deleted file mode 100644
index 3da246ab..00000000
--- a/docs/intro/index.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-.. toctree::
-   :maxdepth: 1
-
-   install
\ No newline at end of file
diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py
index 9def2851..6c0a0d81 100644
--- a/src/protean/core/entity.py
+++ b/src/protean/core/entity.py
@@ -20,11 +20,11 @@ logger = logging.getLogger('protean.core.entity')
 class EntityBase(type):
     """
     This base metaclass processes the class declaration and constructs a meta object that can
-    be used to introspect the Entity class later. Specifically, it sets up a `_meta` attribute on
+    be used to introspect the Entity class later. Specifically, it sets up a `meta_` attribute on
     the Entity to an instance of Meta, either the default of one that is defined in the
     Entity class.
 
-    `_meta` is setup with these attributes:
+    `meta_` is setup with these attributes:
         * `declared_fields`: A dictionary that gives a list of any instances of `Field`
             included as attributes on either the class or on any of its superclasses
         * `id_field`: The Primary identifier attribute of the Entity
@@ -44,7 +44,7 @@ class EntityBase(type):
         # Gather `Meta` class/object if defined
         attr_meta = attrs.pop('Meta', None)
         meta = attr_meta or getattr(new_class, 'Meta', None)
-        setattr(new_class, '_meta', EntityMeta(meta))
+        setattr(new_class, 'meta_', EntityMeta(meta))
 
         # Load declared fields
         new_class._load_fields(attrs)
@@ -80,11 +80,11 @@ class EntityBase(type):
         This is necessary in order to maintain the correct order of fields.
         """
         for base in reversed(bases):
-            if hasattr(base, '_meta') and \
-                    hasattr(base._meta, 'declared_fields'):
+            if hasattr(base, 'meta_') and \
+                    hasattr(base.meta_, 'declared_fields'):
                 base_class_fields = {
                     field_name: field_obj for (field_name, field_obj)
-                    in base._meta.declared_fields.items()
+                    in base.meta_.declared_fields.items()
                     if field_name not in attrs and not field_obj.identifier
                 }
                 new_class._load_fields(base_class_fields)
@@ -99,12 +99,12 @@ class EntityBase(type):
         for attr_name, attr_obj in attrs.items():
             if isinstance(attr_obj, (Field, Reference)):
                 setattr(new_class, attr_name, attr_obj)
-                new_class._meta.declared_fields[attr_name] = attr_obj
+                new_class.meta_.declared_fields[attr_name] = attr_obj
 
     def _set_up_reference_fields(new_class):
         """Walk through relation fields and setup shadow attributes"""
-        if new_class._meta.declared_fields:
-            for _, field in new_class._meta.declared_fields.items():
+        if new_class.meta_.declared_fields:
+            for _, field in new_class.meta_.declared_fields.items():
                 if isinstance(field, Reference):
                     shadow_field_name, shadow_field = field.get_shadow_field()
                     setattr(new_class, shadow_field_name, shadow_field)
@@ -114,10 +114,10 @@ class EntityBase(type):
         """Lookup the id field for this entity and assign"""
         # FIXME What does it mean when there are no declared fields?
         #   Does it translate to an abstract entity?
-        if new_class._meta.declared_fields:
+        if new_class.meta_.declared_fields:
             try:
-                new_class._meta.id_field = next(
-                    field for _, field in new_class._meta.declared_fields.items()
+                new_class.meta_.id_field = next(
+                    field for _, field in new_class.meta_.declared_fields.items()
                     if field.identifier)
             except StopIteration:
                 # If no id field is declared then create one
@@ -131,13 +131,13 @@ class EntityBase(type):
         id_field.__set_name__(new_class, 'id')
 
         # Ensure ID field is updated properly in Meta attribute
-        new_class._meta.declared_fields['id'] = id_field
-        new_class._meta.id_field = id_field
+        new_class.meta_.declared_fields['id'] = id_field
+        new_class.meta_.id_field = id_field
 
     def _load_attributes(new_class):
         """Load list of attributes from declared fields"""
-        for field_name, field_obj in new_class._meta.declared_fields.items():
-            new_class._meta.attributes[field_obj.get_attribute_name()] = field_obj
+        for field_name, field_obj in new_class.meta_.declared_fields.items():
+            new_class.meta_.attributes[field_obj.get_attribute_name()] = field_obj
 
 
 class EntityMeta:
@@ -304,7 +304,7 @@ class QuerySet:
         entity_items = []
         for item in results.items:
             entity = model_cls.to_entity(item)
-            entity._state.mark_retrieved()
+            entity.state_.mark_retrieved()
             entity_items.append(entity)
         results.items = entity_items
 
@@ -496,15 +496,19 @@ class EntityState:
         self._changed = False
         self._destroyed = False
 
+    @property
     def is_new(self):
         return self._new
 
+    @property
     def is_persisted(self):
         return not self._new
 
+    @property
     def is_changed(self):
         return self._changed
 
+    @property
     def is_destroyed(self):
         return self._destroyed
 
@@ -568,7 +572,7 @@ class Entity(metaclass=EntityBase):
         self.errors = {}
 
         # Set up the storage for instance state
-        self._state = EntityState()
+        self.state_ = EntityState()
 
         # Load the attributes based on the template
         loaded_fields = []
@@ -590,7 +594,7 @@ class Entity(metaclass=EntityBase):
 
         # Now load the remaining fields with a None value, which will fail
         # for required fields
-        for field_name, field_obj in self._meta.declared_fields.items():
+        for field_name, field_obj in self.meta_.declared_fields.items():
             if field_name not in loaded_fields:
                 if not isinstance(field_obj, (Reference, ReferenceField)):
                     setattr(self, field_name, None)
@@ -631,7 +635,7 @@ class Entity(metaclass=EntityBase):
     def to_dict(self):
         """ Return entity data as a dictionary """
         return {field_name: getattr(self, field_name, None)
-                for field_name in self._meta.declared_fields}
+                for field_name in self.meta_.declared_fields}
 
     @classmethod
     def _retrieve_model(cls):
@@ -647,58 +651,10 @@ class Entity(metaclass=EntityBase):
     def clone(self):
         """Deepclone the entity, but reset state"""
         clone_copy = copy.deepcopy(self)
-        clone_copy._state = EntityState()
+        clone_copy.state_ = EntityState()
 
         return clone_copy
 
-    ################
-    # Meta methods #
-    ################
-
-    @classproperty
-    def declared_fields(cls):
-        """Pass through method to retrieve declared fields defined for entity"""
-        return cls._meta.declared_fields
-
-    @classproperty
-    def attributes(cls):
-        """Pass through method to retrieve attributes defined for entity"""
-        return cls._meta.attributes
-
-    @classproperty
-    def auto_fields(cls):
-        """Pass through method to retrieve `Auto` fields defined for entity"""
-        return cls._meta.auto_fields
-
-    @classproperty
-    def id_field(cls):
-        """Pass through method to retrieve Identifier field defined for entity"""
-        return cls._meta.id_field
-
-    #################
-    # State methods #
-    #################
-
-    @property
-    def is_new(self):
-        """Pass through method to check if Entity is not persisted"""
-        return self._state.is_new()
-
-    @property
-    def is_persisted(self):
-        """Pass through method to check if Entity is persisted"""
-        return self._state.is_persisted()
-
-    @property
-    def is_changed(self):
-        """Pass through method to check if Entity has changed since last persistence"""
-        return self._state.is_changed()
-
-    @property
-    def is_destroyed(self):
-        """Pass through method to check if Entity has been destroyed"""
-        return self._state.is_destroyed()
-
     ######################
     # Life-cycle methods #
     ######################
@@ -712,7 +668,7 @@ class Entity(metaclass=EntityBase):
         logger.debug(f'Lookup `{cls.__name__}` object with identifier {identifier}')
         # Get the ID field for the entity
         filters = {
-            cls._meta.id_field.field_name: identifier
+            cls.meta_.id_field.field_name: identifier
         }
 
         # Find this item in the repository or raise Error
@@ -785,7 +741,7 @@ class Entity(metaclass=EntityBase):
             model_obj = adapter._create_object(model_cls.from_entity(entity))
 
             # Update the auto fields of the entity
-            for field_name, field_obj in entity._meta.declared_fields.items():
+            for field_name, field_obj in entity.meta_.declared_fields.items():
                 if isinstance(field_obj, Auto):
                     if isinstance(model_obj, dict):
                         field_val = model_obj[field_name]
@@ -794,7 +750,7 @@ class Entity(metaclass=EntityBase):
                     setattr(entity, field_name, field_val)
 
             # Set Entity status to saved
-            entity._state.mark_saved()
+            entity.state_.mark_saved()
 
             return entity
         except ValidationError as exc:
@@ -820,7 +776,7 @@ class Entity(metaclass=EntityBase):
             model_obj = adapter._create_object(model_cls.from_entity(self))
 
             # Update the auto fields of the entity
-            for field_name, field_obj in self._meta.declared_fields.items():
+            for field_name, field_obj in self.meta_.declared_fields.items():
                 if isinstance(field_obj, Auto):
                     if isinstance(model_obj, dict):
                         field_val = model_obj[field_name]
@@ -829,7 +785,7 @@ class Entity(metaclass=EntityBase):
                     setattr(self, field_name, field_val)
 
             # Set Entity status to saved
-            self._state.mark_saved()
+            self.state_.mark_saved()
 
             return self
         except Exception as exc:
@@ -864,7 +820,7 @@ class Entity(metaclass=EntityBase):
             adapter._update_object(model_cls.from_entity(self))
 
             # Set Entity status to saved
-            self._state.mark_saved()
+            self.state_.mark_saved()
 
             return self
         except Exception as exc:
@@ -879,7 +835,7 @@ class Entity(metaclass=EntityBase):
         # Build the filters from the unique constraints
         filters, excludes = {}, {}
 
-        for field_name, field_obj in self._meta.unique_fields:
+        for field_name, field_obj in self.meta_.unique_fields:
             lookup_value = getattr(self, field_name, None)
             # Ignore empty lookup values
             if lookup_value in Field.empty_values:
@@ -893,7 +849,7 @@ class Entity(metaclass=EntityBase):
         # Lookup the objects by the filters and raise error on results
         for filter_key, lookup_value in filters.items():
             if self.exists(excludes, **{filter_key: lookup_value}):
-                field_obj = self._meta.declared_fields[filter_key]
+                field_obj = self.meta_.declared_fields[filter_key]
                 field_obj.fail('unique',
                                model_name=model_cls.opts_.model_name,
                                field_name=filter_key)
@@ -909,12 +865,12 @@ class Entity(metaclass=EntityBase):
         model_cls, adapter = self.__class__._retrieve_model()
 
         try:
-            if not self.is_destroyed:
+            if not self.state_.is_destroyed:
                 # Update entity's data attributes
                 adapter._delete_object(model_cls.from_entity(self))
 
                 # Set Entity status to saved
-                self._state.mark_destroyed()
+                self.state_.mark_destroyed()
 
             return self
         except Exception as exc:
diff --git a/src/protean/core/field/association.py b/src/protean/core/field/association.py
index 24514119..b374c087 100644
--- a/src/protean/core/field/association.py
+++ b/src/protean/core/field/association.py
@@ -90,7 +90,7 @@ class Reference(FieldCacheMixin, Field):
         if isinstance(self.to_cls, str):
             return 'id'
         else:
-            return self.via or self.to_cls.id_field.attribute_name
+            return self.via or self.to_cls.meta_.id_field.attribute_name
 
     def __get__(self, instance, owner):
         """Retrieve associated objects"""
@@ -105,7 +105,7 @@ class Reference(FieldCacheMixin, Field):
             self.attribute_name = self.get_attribute_name()
 
         reference_obj = None
-        if hasattr(instance, '_state'):
+        if hasattr(instance, 'state_'):
             try:
                 reference_obj = self.get_cached_value(instance)
             except KeyError:
@@ -213,7 +213,7 @@ class Association(FieldDescriptorMixin, FieldCacheMixin):
             reference_obj = self.get_cached_value(instance)
         except KeyError:
             # Fetch target object by own Identifier
-            id_value = getattr(instance, instance.id_field.field_name)
+            id_value = getattr(instance, instance.meta_.id_field.field_name)
             reference_obj = self._fetch_objects(self._linked_attribute(owner), id_value)
             if reference_obj:
                 self._set_own_value(instance, reference_obj)
diff --git a/src/protean/core/field/base.py b/src/protean/core/field/base.py
index e96aa2fa..b907dbc3 100644
--- a/src/protean/core/field/base.py
+++ b/src/protean/core/field/base.py
@@ -102,7 +102,7 @@ class Field(FieldDescriptorMixin, metaclass=ABCMeta):
         instance.__dict__[self.field_name] = value
 
         # Mark Entity as Dirty
-        instance._state.mark_changed()
+        instance.state_.mark_changed()
 
     def __delete__(self, instance):
         instance.__dict__.pop(self.field_name, None)
diff --git a/src/protean/core/field/mixins.py b/src/protean/core/field/mixins.py
index 5e314bfd..2418d8d1 100644
--- a/src/protean/core/field/mixins.py
+++ b/src/protean/core/field/mixins.py
@@ -10,21 +10,21 @@ class FieldCacheMixin:
     def get_cached_value(self, instance, default=NOT_PROVIDED):
         cache_name = self.get_cache_name()
         try:
-            return instance._state.fields_cache[cache_name]
+            return instance.state_.fields_cache[cache_name]
         except KeyError:
             if default is NOT_PROVIDED:
                 raise
             return default
 
     def is_cached(self, instance):
-        return self.get_cache_name() in instance._state.fields_cache
+        return self.get_cache_name() in instance.state_.fields_cache
 
     def set_cached_value(self, instance, value):
-        instance._state.fields_cache[self.get_cache_name()] = value
+        instance.state_.fields_cache[self.get_cache_name()] = value
 
     def delete_cached_value(self, instance):
-        if self.get_cache_name() in instance._state.fields_cache:
-            del instance._state.fields_cache[self.get_cache_name()]
+        if self.get_cache_name() in instance.state_.fields_cache:
+            del instance.state_.fields_cache[self.get_cache_name()]
 
 
 class FieldDescriptorMixin:
diff --git a/src/protean/impl/repository/dict_repo.py b/src/protean/impl/repository/dict_repo.py
index e0d431fc..e51cac6c 100644
--- a/src/protean/impl/repository/dict_repo.py
+++ b/src/protean/impl/repository/dict_repo.py
@@ -25,7 +25,7 @@ class Adapter(BaseAdapter):
     def _set_auto_fields(self, model_obj):
         """ Set the values of the auto field using counter"""
         for field_name, field_obj in \
-                self.entity_cls.auto_fields:
+                self.entity_cls.meta_.auto_fields:
             counter_key = f'{self.model_name}_{field_name}'
             if not (field_name in model_obj and model_obj[field_name] is not None):
                 # Increment the counter and it should start from 1
@@ -42,7 +42,7 @@ class Adapter(BaseAdapter):
         model_obj = self._set_auto_fields(model_obj)
 
         # Add the entity to the repository
-        identifier = model_obj[self.entity_cls.id_field.field_name]
+        identifier = model_obj[self.entity_cls.meta_.id_field.field_name]
         with self.conn['lock']:
             self.conn['data'][self.model_name][identifier] = model_obj
 
@@ -128,7 +128,7 @@ class Adapter(BaseAdapter):
 
     def _update_object(self, model_obj):
         """ Update the entity record in the dictionary """
-        identifier = model_obj[self.entity_cls.id_field.field_name]
+        identifier = model_obj[self.entity_cls.meta_.id_field.field_name]
         with self.conn['lock']:
             # Check if object is present
             if identifier not in self.conn['data'][self.model_name]:
@@ -156,7 +156,7 @@ class Adapter(BaseAdapter):
 
     def _delete_object(self, model_obj):
         """ Delete the entity record in the dictionary """
-        identifier = model_obj[self.entity_cls.id_field.field_name]
+        identifier = model_obj[self.entity_cls.meta_.id_field.field_name]
         with self.conn['lock']:
             # Check if object is present
             if identifier not in self.conn['data'][self.model_name]:
@@ -317,7 +317,7 @@ class DictModel(BaseModel):
     def from_entity(cls, entity):
         """ Convert the entity to a dictionary record """
         dict_obj = {}
-        for field_name in entity.__class__.attributes:
+        for field_name in entity.__class__.meta_.attributes:
             dict_obj[field_name] = getattr(entity, field_name)
         return dict_obj
 


Test patch:
diff --git a/tests/core/test_entity.py b/tests/core/test_entity.py
index 59d055c4..3bbb3aec 100644
--- a/tests/core/test_entity.py
+++ b/tests/core/test_entity.py
@@ -549,7 +549,7 @@ class TestEntity:
         dog = Dog.create(id=3, name='Johnny', owner='Carey')
         deleted_dog = dog.delete()
         assert deleted_dog is not None
-        assert deleted_dog.is_destroyed is True
+        assert deleted_dog.state_.is_destroyed is True
 
         with pytest.raises(ObjectNotFoundError):
             Dog.get(3)
@@ -620,13 +620,13 @@ class TestEntityMetaAttributes:
     def test_meta_on_init(self):
         """Test that `meta` attribute is available after initialization"""
         dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert hasattr(dog, '_meta')
+        assert hasattr(dog, 'meta_')
 
     def test_declared_fields_normal(self):
         """Test declared fields on an entity without references"""
         dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
 
-        attribute_keys = list(OrderedDict(sorted(dog.attributes.items())).keys())
+        attribute_keys = list(OrderedDict(sorted(dog.meta_.attributes.items())).keys())
         assert attribute_keys == ['age', 'id', 'name', 'owner']
 
     def test_declared_fields_with_reference(self):
@@ -635,7 +635,7 @@ class TestEntityMetaAttributes:
                              email='jeff.kennedy@presidents.com')
         dog = RelatedDog(id=1, name='John Doe', age=10, owner=human)
 
-        attribute_keys = list(OrderedDict(sorted(dog.attributes.items())).keys())
+        attribute_keys = list(OrderedDict(sorted(dog.meta_.attributes.items())).keys())
         assert attribute_keys == ['age', 'id', 'name', 'owner_id']
 
     def test_declared_fields_with_hasone_association(self):
@@ -644,5 +644,5 @@ class TestEntityMetaAttributes:
                                     email='jeff.kennedy@presidents.com')
         dog = HasOneDog1.create(id=1, name='John Doe', age=10, has_one_human1=human)
 
-        assert all(key in dog.attributes for key in ['age', 'has_one_human1_id', 'id', 'name'])
-        assert all(key in human.attributes for key in ['first_name', 'id', 'last_name', 'email'])
+        assert all(key in dog.meta_.attributes for key in ['age', 'has_one_human1_id', 'id', 'name'])
+        assert all(key in human.meta_.attributes for key in ['first_name', 'id', 'last_name', 'email'])
diff --git a/tests/core/test_entity_state.py b/tests/core/test_entity_state.py
index 4ff85ce7..4babc252 100644
--- a/tests/core/test_entity_state.py
+++ b/tests/core/test_entity_state.py
@@ -10,25 +10,24 @@ class TestState:
     def test_default_state(self):
         """Test that a default state is available when the entity is instantiated"""
         dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert dog._state is not None
-        assert dog._state._new
-        assert dog._state.is_new()
-        assert dog.is_new
-        assert not dog.is_persisted
+        assert dog.state_ is not None
+        assert dog.state_._new
+        assert dog.state_.is_new
+        assert not dog.state_.is_persisted
 
     def test_state_on_retrieved_objects(self):
         """Test that retrieved objects are not marked as new"""
         dog = Dog.create(name='John Doe', age=10, owner='Jimmy')
         dog_dup = Dog.get(dog.id)
 
-        assert not dog_dup.is_new
+        assert not dog_dup.state_.is_new
 
     def test_persisted_after_save(self):
         """Test that the entity is marked as saved after successfull save"""
         dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert dog.is_new
+        assert dog.state_.is_new
         dog.save()
-        assert dog.is_persisted
+        assert dog.state_.is_persisted
 
     def test_not_persisted_if_save_failed(self):
         """Test that the entity still shows as new if save failed"""
@@ -37,46 +36,46 @@ class TestState:
             del dog.name
             dog.save()
         except ValidationError as exc:
-            assert dog.is_new
+            assert dog.state_.is_new
 
     def test_persisted_after_create(self):
         """Test that the entity is marked as saved after successfull create"""
         dog = Dog.create(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert not dog.is_new
+        assert not dog.state_.is_new
 
     def test_copy_resets_state(self):
         """Test that a default state is available when the entity is instantiated"""
         dog1 = Dog.create(id=1, name='John Doe', age=10, owner='Jimmy')
         dog2 = dog1.clone()
 
-        assert dog2.is_new
+        assert dog2.state_.is_new
 
     def test_changed(self):
         """Test that entity is marked as changed if attributes are updated"""
         dog = Dog.create(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert not dog.is_changed
+        assert not dog.state_.is_changed
         dog.name = 'Jane Doe'
-        assert dog.is_changed
+        assert dog.state_.is_changed
 
     def test_not_changed_if_still_new(self):
         """Test that entity is not marked as changed upon attribute change
         if its still new"""
         dog = Dog(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert not dog.is_changed
+        assert not dog.state_.is_changed
         dog.name = 'Jane Doe'
-        assert not dog.is_changed
+        assert not dog.state_.is_changed
 
     def test_not_changed_after_save(self):
         """Test that entity is marked as not changed after save"""
         dog = Dog.create(id=1, name='John Doe', age=10, owner='Jimmy')
         dog.name = 'Jane Doe'
-        assert dog.is_changed
+        assert dog.state_.is_changed
         dog.save()
-        assert not dog.is_changed
+        assert not dog.state_.is_changed
 
     def test_destroyed(self):
         """Test that a entity is marked as destroyed after delete"""
         dog = Dog.create(id=1, name='John Doe', age=10, owner='Jimmy')
-        assert not dog.is_destroyed
+        assert not dog.state_.is_destroyed
         dog.delete()
-        assert dog.is_destroyed
+        assert dog.state_.is_destroyed
diff --git a/tests/core/test_repository.py b/tests/core/test_repository.py
index ad7e35a4..5f99a09b 100644
--- a/tests/core/test_repository.py
+++ b/tests/core/test_repository.py
@@ -105,7 +105,7 @@ class TestRepository:
         dog = Dog.create(id=3, name='Johnny', owner='Carey')
         deleted_dog = dog.delete()
         assert deleted_dog is not None
-        assert deleted_dog.is_destroyed is True
+        assert deleted_dog.state_.is_destroyed is True
 
         with pytest.raises(ObjectNotFoundError):
             Dog.get(3)