Repository: proteanhq/protean

Issue:
Change Fields to be Descriptors
By having fields as descriptors, we can control values that are set into them, as well as elegantly load/unload values.

Discussion:

Patch:
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..da1fe54b
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,426 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code
+extension-pkg-whitelist=lxml
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS, manage.py
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint.
+jobs=1
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once).You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use"--disable=all --enable=classes
+# --disable=W"
+# disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call
+disable=R,too-many-function-args,no-self-use,redefined-builtin,invalid-name,too-few-public-methods,broad-except,unexpected-keyword-arg,eval-used,arguments-differ,protected-access,bad-classmethod-argument,duplicate-code,import-error
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=
+
+
+[REPORTS]
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note). You have access to the variables errors warning, statement which
+# respectively contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio).You can also give a reporter class, eg
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+
+[BASIC]
+
+# Naming hint for argument names
+argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct argument names
+argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for attribute names
+attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct attribute names
+attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# Naming hint for class attribute names
+class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Regular expression matching correct class attribute names
+class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
+
+# Naming hint for class names
+class-name-hint=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression matching correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Naming hint for constant names
+const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Regular expression matching correct constant names
+const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming hint for function names
+function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct function names
+function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_,id,Base,faker,cv
+
+# Include a hint for the correct naming format with invalid-name
+include-naming-hint=yes
+
+# Naming hint for inline iteration names
+inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
+
+# Regular expression matching correct inline iteration names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Naming hint for method names
+method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct method names
+method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Naming hint for module names
+module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression matching correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+property-classes=abc.abstractproperty
+
+# Naming hint for variable names
+variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+# Regular expression matching correct variable names
+variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging  or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module
+max-module-lines=1500
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[LOGGING]
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=XXX,TODO
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Spelling dictionary name. Available dictionaries: none. To make it working
+# install python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to indicated private dictionary in
+# --spelling-private-dict-file option instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=fake.*
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local,Profile
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis. It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,_cb
+
+# A regular expression matching the name of dummy variables (i.e. expectedly
+# not used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,future.builtins
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,_fields,_replace,_source,_make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=mcs
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=15
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=20
+
+# Maximum number of boolean expressions in a if statement
+max-bool-expr=5
+
+# Maximum number of branch for function / method body
+max-branches=20
+
+# Maximum number of locals for function / method body
+max-locals=35
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled)
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled)
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "Exception"
+overgeneral-exceptions=Exception
diff --git a/docs/conf.py b/docs/conf.py
index 70c6f6cf..9b6f8ebd 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -47,7 +47,7 @@ html_use_smartypants = True
 html_last_updated_fmt = '%b %d, %Y'
 html_split_index = False
 html_sidebars = {
-    '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'],
+   '**': ['searchbox.html', 'globaltoc.html', 'sourcelink.html'],
 }
 html_short_title = '%s-%s' % (project, version)
 
diff --git a/requirements/dev.txt b/requirements/dev.txt
index b54cf866..788947f9 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -2,7 +2,7 @@ check-manifest==0.37
 coverage==4.5.1
 docutils==0.14
 flake8==3.5.0
-pytest-flake8==1.0.4
+pylint==2.0.0
 sphinx==1.8.3
 tox==3.1.2
 twine==1.11.0
diff --git a/src/protean/__main__.py b/src/protean/__main__.py
index e20c8814..befd64dc 100644
--- a/src/protean/__main__.py
+++ b/src/protean/__main__.py
@@ -11,4 +11,4 @@ Why does this file exist, and why __main__? For more info, read:
 from protean.cli import main
 
 if __name__ == "__main__":
-    main()
+    main()  # pylint: disable=E1120
diff --git a/src/protean/core/entity.py b/src/protean/core/entity.py
index 92fa88c4..ce75b3c8 100644
--- a/src/protean/core/entity.py
+++ b/src/protean/core/entity.py
@@ -1,5 +1,4 @@
 """Entity Functionality and Classes"""
-import inspect
 import logging
 from typing import Any, Union
 
@@ -12,8 +11,6 @@ from protean.utils.query import Q
 
 logger = logging.getLogger('protean.core.entity')
 
-ENTITY_META_ATTRIBUTE_NAMES = ('ordering')
-
 
 class EntityBase(type):
     """
@@ -29,7 +26,7 @@ class EntityBase(type):
     """
 
     def __new__(mcs, name, bases, attrs, **kwargs):
-        """Initialize Entity Classes"""
+        """Initialize Entity MetaClass and load attributes"""
 
         # Ensure initialization is only performed for subclasses of Entity
         # (excluding Entity class itself).
@@ -37,72 +34,72 @@ class EntityBase(type):
         if not parents:
             return super().__new__(mcs, name, bases, attrs)
 
-        # Create the class object
-        module = attrs.pop('__module__')
-        new_attrs = {'__module__': module}
-        # Ensure we pass __classcell__ to type.__new__
-        #   Refer to https://stackoverflow.com/a/41347038/1858466 for more information
-        classcell = attrs.pop('__classcell__', None)
-        if classcell is not None:
-            new_attrs['__classcell__'] = classcell
-
-        new_class = super().__new__(mcs, name, bases, new_attrs, **kwargs)
+        new_class = super().__new__(mcs, name, bases, attrs, **kwargs)
 
         # Gather `Meta` class/object if defined
         attr_meta = attrs.pop('Meta', None)
         meta = attr_meta or getattr(new_class, 'Meta', None)
-        new_class.add_to_class('_meta', EntityMeta(meta))
+        setattr(new_class, '_meta', EntityMeta(meta))
 
-        # Add declared fields
-        declared_fields = []
-        for attr_name, attr_obj in attrs.items():
-            new_class.add_to_class(attr_name, attr_obj)
-            if isinstance(attr_obj, Field):
-                declared_fields.append((attr_name, attr_obj))
+        # Load declared fields
+        new_class._load_fields(attrs)
+
+        # Load declared fields from Base class, in case this Entity is subclassing another
+        new_class._load_base_class_fields(bases, attrs)
+
+        # Lookup an already defined ID field or create an `Auto` field
+        new_class._set_id_field()
+
+        # Construct an empty QuerySet associated with this Entity class
+        new_class.query = QuerySet(name)
+
+        return new_class
 
-        # If this class is subclassing another Entity, add that Entity's
-        # fields.  Note that we loop over the bases in *reverse*.
-        # This is necessary in order to maintain the correct order of fields.
+    def _load_base_class_fields(new_class, bases, attrs):
+        """If this class is subclassing another Entity, add that Entity's
+        fields.  Note that we loop over the bases in *reverse*.
+        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'):
-                for field_name, field_obj in base._meta.declared_fields.items():
-                    if field_name not in attrs and not field_obj.identifier:
-                        new_class.add_to_class(field_name, field_obj)
-                        declared_fields.append((field_name, field_obj))
-
-        # Lookup the id field for this entity
-        if declared_fields:
+                base_class_fields = {
+                    field_name: field_obj for (field_name, field_obj)
+                    in base._meta.declared_fields.items()
+                    if field_name not in attrs and not field_obj.identifier
+                }
+                new_class._load_fields(base_class_fields)
+
+    def _load_fields(new_class, attrs):
+        """Load field items into Metaclass"""
+        for attr_name, attr_obj in attrs.items():
+            if isinstance(attr_obj, Field):
+                setattr(new_class, attr_name, attr_obj)
+                new_class._meta.declared_fields[attr_name] = attr_obj
+
+    def _set_id_field(new_class):
+        """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:
             try:
                 new_class._meta.id_field = next(
-                    field for _, field in declared_fields
+                    field for _, field in new_class._meta.declared_fields.items()
                     if field.identifier)
             except StopIteration:
                 # If no id field is declared then create one
-                id_field = Auto(identifier=True)
-                new_class.add_to_class('id', id_field)
-                new_class._meta.id_field = id_field
+                new_class._create_id_field()
 
-        # Construct an empty QuerySet associated with this Entity class
-        new_class.query = QuerySet(name)
-
-        return new_class
-
-    def add_to_class(cls, name, value):
-        """Add an attribute to the class
+    def _create_id_field(new_class):
+        """Create and return a default ID field that is Auto generated"""
+        id_field = Auto(identifier=True)
 
-           Will check if `bind_to_entity` is defined on the attribute. If defined, it will
-           be used to add the field to the meta class and bind the entity class to the field.
-        """
-        # We should call the contribute_to_class method only if it's bound
-        if not inspect.isclass(value) and hasattr(value, 'bind_to_entity'):
-            value.bind_to_entity(cls, name)
-        else:
-            setattr(cls, name, value)
+        setattr(new_class, 'id', id_field)
+        id_field.__set_name__(new_class, 'id')
 
-    def add_field(self, field_name, field):
-        """Add field to the Entity class meta attribute"""
-        self._meta.declared_fields[field_name] = field
+        # Ensure ID field is updated properly in Meta attribute
+        new_class._meta.declared_fields['id'] = id_field
+        new_class._meta.id_field = id_field
 
 
 class EntityMeta:
@@ -129,30 +126,6 @@ class EntityMeta:
                 for field_name, field_obj in self.declared_fields.items()
                 if isinstance(field_obj, Auto)]
 
-    def bind_to_entity(self, entity_cls, name):
-        """Placeholder method for additional processing when meta info is added to Entity"""
-        entity_cls._meta = self
-        self.entity_cls = entity_cls
-
-        # Next, apply any overridden values from 'class Meta'.
-        if self.meta:
-            meta_attrs = self.meta.__dict__.copy()
-            for name in self.meta.__dict__:
-                # Ignore any private attributes
-                if name.startswith('_'):
-                    del meta_attrs[name]
-            for attr_name in ENTITY_META_ATTRIBUTE_NAMES:
-                if attr_name in meta_attrs:
-                    setattr(self, attr_name, meta_attrs.pop(attr_name))
-                elif hasattr(self.meta, attr_name):
-                    setattr(self, attr_name, getattr(self.meta, attr_name))
-
-            # Any leftover attributes must be invalid.
-            if meta_attrs != {}:
-                raise TypeError("'class Meta' got invalid attribute(s): %s" % ','.join(meta_attrs))
-
-        del self.meta
-
 
 class QuerySet:
     """A chainable class to gather a bunch of criteria and preferences (page size, order etc.)
@@ -170,7 +143,7 @@ class QuerySet:
         page: The current page number of the records to be pulled
         per_page: The size of each page of the records to be pulled
         order_by: The list of parameters to be used for ordering the results.
-            Use a `-` before the parameter name to sort in descending order
+            Use a `-` before the parameter name to sort in descending order 
             and if not ascending order.
         excludes_: Objects with these properties will be excluded from the results
         filters: Filter criteria
@@ -407,7 +380,7 @@ class Entity(metaclass=EntityBase):
         Acts as a placeholder for generated entity fields like:
 
             :declared_fields: dict
-                Any instances of `Field` included as attributes on either the class
+                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
@@ -435,39 +408,24 @@ class Entity(metaclass=EntityBase):
                     f'values.'
                 )
             for field_name, val in dictionary.items():
-                field_obj = self._meta.declared_fields.get(field_name, None)
-                if field_obj:
-                    loaded_fields.append(field_name)
-                    self._setattr(field_name, field_obj, val)
+                loaded_fields.append(field_name)
+                setattr(self, field_name, val)
 
         # Now load against the keyword arguments
         for field_name, val in kwargs.items():
-            field_obj = self._meta.declared_fields.get(field_name, None)
-            if field_obj:
-                loaded_fields.append(field_name)
-                self._setattr(field_name, field_obj, val)
+            loaded_fields.append(field_name)
+            setattr(self, field_name, val)
 
         # 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():
             if field_name not in loaded_fields:
-                self._setattr(field_name, field_obj, None)
+                setattr(self, field_name, None)
 
         # Raise any errors found during load
         if self.errors:
             raise ValidationError(self.errors)
 
-    def _setattr(self, field_name, field_obj, value):
-        """
-        Load the value for the field, set it if passes and if not
-        add to the error list.
-        """
-        try:
-            valid_value = field_obj.load(value)
-            setattr(self, field_name, valid_value)
-        except ValidationError as err:
-            self.errors[field_name] = err.messages
-
     def _update_data(self, *data_dict, **kwargs):
         """
         A private method to process and update entity values correctly.
@@ -487,15 +445,11 @@ class Entity(metaclass=EntityBase):
                     f'values.'
                 )
             for field_name, val in data.items():
-                field_obj = self._meta.declared_fields.get(field_name, None)
-                if field_obj:
-                    self._setattr(field_name, field_obj, val)
+                setattr(self, field_name, val)
 
         # Now load against the keyword arguments
         for field_name, val in kwargs.items():
-            field_obj = self._meta.declared_fields.get(field_name, None)
-            if field_obj:
-                self._setattr(field_name, field_obj, val)
+            setattr(self, field_name, val)
 
         # Raise any errors found during update
         if self.errors:
@@ -705,7 +659,7 @@ class Entity(metaclass=EntityBase):
 
         Throws ObjectNotFoundError if the object was not found in the repository
         """
-        # FIXME: Return True or False to indicate an object was deleted,
+        # FIXME: Return True or False to indicate an object was deleted, 
         #   rather than the count of records deleted
 
         # FIXME: Ensure Adapter throws ObjectNotFoundError
diff --git a/src/protean/core/field/base.py b/src/protean/core/field/base.py
index a262b5c4..9375569d 100644
--- a/src/protean/core/field/base.py
+++ b/src/protean/core/field/base.py
@@ -48,7 +48,7 @@ class Field(metaclass=ABCMeta):
     def __init__(self, identifier: bool = False, default: Any = None,
                  required: bool = False, unique: bool = False,
                  label: str = None, choices: enum.Enum = None,
-                 validators: Iterable = (), error_messages: dict = None):
+                 validators: Iterable = (), value=None, error_messages: dict = None):
 
         self.identifier = identifier
         self.default = default
@@ -73,9 +73,11 @@ class Field(metaclass=ABCMeta):
         self.label = label
         self._validators = validators
 
-        # These are set up by `.bind_to_entity()` when the field is added to
-        # the entity
-        self.entity_cls = None
+        # Value holder
+        self._value = value
+
+        # These are set up when the owner (Entity class) adds the field to itself
+        self.name = None
         self.field_name = None
         self.attribute_name = None
 
@@ -86,36 +88,41 @@ class Field(metaclass=ABCMeta):
         messages.update(error_messages or {})
         self.error_messages = messages
 
-    def set_attributes_from_name(self, field_name):
-        """Set attributes of field from name"""
-        self.field_name = self.field_name or field_name
+    def __set_name__(self, entity_cls, name):
+        self.name = name + "__raw"
+        self.field_name = name
         self.attribute_name = self.get_attribute_name()
 
         # `self.label` should default to being based on the field name.
         if self.label is None:
-            self.label = field_name.replace('_', ' ').capitalize()
+            self.label = self.field_name.replace('_', ' ').capitalize()
+
+    def __get__(self, instance, owner):
+        return getattr(instance, self.name, self.value)
+
+    def __set__(self, instance, value):
+        value = self._load(value)
+        setattr(instance, self.name, value)
+
+    def __delete__(self, instance):
+        raise AttributeError("Can't delete attribute")
+
+    @property
+    def value(self):
+        return self._value
+
+    @value.setter
+    def value(self, value):
+        self._value = value if value else self.type()
 
     def get_attribute_name(self):
         """Return Attribute name for the attribute.
 
         Defaults to the field name in this base class, but can be overridden.
-        Handy when defining complex objects with backing attributes, like Foreign keys.
+        Handy when defining complex objects with shadow attributes, like Foreign keys.
         """
         return self.field_name
 
-    def bind_to_entity(self, entity_cls, field_name):
-        """
-        Initializes the field name for the field instance.
-        Called when a field is added to the parent entity instance.
-
-        :param entity_cls: Entity class to which there fields are being bound to
-        :param field_name: Name of the field in the binding Entity
-        """
-
-        self.entity_cls = entity_cls
-        self.set_attributes_from_name(field_name)
-        entity_cls.add_field(field_name, self)
-
     def fail(self, key, **kwargs):
         """A helper method that simply raises a `ValidationError`.
         """
@@ -168,7 +175,7 @@ class Field(metaclass=ABCMeta):
         if errors:
             raise exceptions.ValidationError(errors)
 
-    def load(self, value: Any):
+    def _load(self, value: Any):
         """
         Load the value for the field, run validators and return the value.
         Subclasses can override this to provide custom load logic.
diff --git a/src/protean/core/usecase/base.py b/src/protean/core/usecase/base.py
index fa4d045b..c5411456 100644
--- a/src/protean/core/usecase/base.py
+++ b/src/protean/core/usecase/base.py
@@ -34,7 +34,7 @@ class UseCase(metaclass=ABCMeta):
             return ResponseFailure.build_not_found(
                 {'identifier': 'Object with this ID does not exist.'})
 
-        except Exception as exc:
+        except Exception as exc:  # pylint: disable=W0703
             logger.error(
                 f'{self.__class__.__name__} execution failed due to error {exc}',
                 exc_info=True)
diff --git a/src/protean/impl/repository/dict_repo.py b/src/protean/impl/repository/dict_repo.py
index 66fe818b..c43047e7 100644
--- a/src/protean/impl/repository/dict_repo.py
+++ b/src/protean/impl/repository/dict_repo.py
@@ -68,7 +68,7 @@ class Adapter(BaseAdapter):
 
         return results
 
-    def _filter(self, criteria: Q, db):
+    def _filter(self, criteria:Q, db):
         """Recursive function to filter items from dictionary"""
         # Filter the dictionary objects based on the filters
         negated = criteria.negated
@@ -166,7 +166,6 @@ class Adapter(BaseAdapter):
             if self.model_name in self.conn['data']:
                 del self.conn['data'][self.model_name]
 
-
 operators = {
     'exact': '==',
     'iexact': '==',
@@ -179,7 +178,6 @@ operators = {
     'in': 'in'
 }
 
-
 class DefaultLookup(Lookup):
     """Base class with default implementation of expression construction"""
     def process_source(self):
diff --git a/src/protean/utils/log.py b/src/protean/utils/log.py
index 6082e49f..f7e8569c 100644
--- a/src/protean/utils/log.py
+++ b/src/protean/utils/log.py
@@ -9,4 +9,4 @@ def configure_logging():
     """ Function to configure the logging for the Protean App"""
     # Load the logger using the logging config
     logging.config.dictConfig(
-        active_config.LOGGING_CONFIG)
+        active_config.LOGGING_CONFIG)  # pylint: disable=E1101


Test patch:
diff --git a/tests/core/test_entity.py b/tests/core/test_entity.py
index 112d3225..fcf34f86 100644
--- a/tests/core/test_entity.py
+++ b/tests/core/test_entity.py
@@ -234,7 +234,7 @@ class TestEntity:
         """ Update an existing entity in the repository"""
         dog = Dog.create(id=2, name='Johnny', owner='Carey', age=2)
 
-        dog.update({'age': 10})
+        dog.update({'age':10})
         u_dog = Dog.get(2)
         assert u_dog is not None
         assert u_dog.age == 10
@@ -452,7 +452,7 @@ class TestEntity:
 
         # Filter by the Owner
         with pytest.raises(NotImplementedError):
-            Dog.query.filter(age__notexact=3).all()
+            dogs_notexact = Dog.query.filter(age__notexact=3).all()
 
     def test_pagination(self):
         """ Test the pagination of the filter results"""
@@ -997,7 +997,7 @@ class TestCriteriaConstruction:
         assert q3._criteria.children[0].connector == Q.AND
 
 
-class TestQ:
+class TestQ: 
     """Class that holds tests for Q Objects"""
 
     def test_deconstruct(self):
diff --git a/tests/core/test_ext_field_types.py b/tests/core/test_ext_field_types.py
index 9065a811..8c86edd9 100644
--- a/tests/core/test_ext_field_types.py
+++ b/tests/core/test_ext_field_types.py
@@ -20,9 +20,9 @@ class TestStringShortField:
 
         # Check that it raises validation
         with pytest.raises(ValidationError):
-            name.load('D' * 16)
+            name._load('D' * 16)
 
-        assert name.load('dummy') == 'dummy'
+        assert name._load('dummy') == 'dummy'
 
 
 class TestStringMediumField:
@@ -40,9 +40,9 @@ class TestStringMediumField:
 
         # Check that it raises validation
         with pytest.raises(ValidationError):
-            name.load('D' * 51)
+            name._load('D' * 51)
 
-        assert name.load('dummy') == 'dummy'
+        assert name._load('dummy') == 'dummy'
 
 
 class TestStringLongField:
@@ -60,6 +60,6 @@ class TestStringLongField:
 
         # Check that it raises validation
         with pytest.raises(ValidationError):
-            name.load('D' * 256)
+            name._load('D' * 256)
 
-        assert name.load('dummy') == 'dummy'
+        assert name._load('dummy') == 'dummy'
diff --git a/tests/core/test_field.py b/tests/core/test_field.py
index dd120e05..e542a4fd 100644
--- a/tests/core/test_field.py
+++ b/tests/core/test_field.py
@@ -44,23 +44,23 @@ class TestField:
 
         with pytest.raises(ValidationError):
             name = String(required=True)
-            name.load(None)
+            name._load(None)
 
     def test_defaults(self):
         """ Test default value is set when no value is supplied"""
         # Test with default value as constant
         name = String(default='dummy')
-        assert name.load('') == 'dummy'
+        assert name._load('') == 'dummy'
 
         # Test with default value as callable
         name = String(default=lambda: 'dummy')
-        assert name.load('') == 'dummy'
+        assert name._load('') == 'dummy'
 
     def test_type_validation(self):
         """ Test type checking validation for the Field"""
         with pytest.raises(ValidationError):
             name = String()
-            name.load(1)
+            name._load(1)
 
     def test_validators(self):
         """ Test custom validators defined for the field"""
@@ -68,7 +68,7 @@ class TestField:
         with pytest.raises(ValidationError):
             name = String(
                 validators=[MinLengthValidator(min_length=5)])
-            name.load('Dum')
+            name._load('Dum')
 
     def test_error_message(self):
         """ Test that proper error message is generated"""
@@ -76,7 +76,7 @@ class TestField:
         # Test the basic error message
         try:
             name = String(required=True)
-            name.load(None)
+            name._load(None)
         except ValidationError as err:
             assert err.normalized_messages == {
                 '_entity': [name.error_messages['required']]}
@@ -84,7 +84,7 @@ class TestField:
         # Test overriding of error message
         try:
             name = String()
-            name.load(1)
+            name._load(1)
         except ValidationError as err:
             assert err.normalized_messages == {
                 '_entity': ['Field value must be of str type.']}
@@ -94,7 +94,7 @@ class TestField:
             name = String(
                 validators=[MinLengthValidator(min_length=5),
                             MinLengthValidator(min_length=5)])
-            name.load('Dum')
+            name._load('Dum')
         except ValidationError as err:
             assert err.normalized_messages == {
                 '_entity': ['Ensure this value has at least 5 character.',
@@ -110,4 +110,4 @@ class TestField:
         String.default_validators = [medium_string_validator]
         with pytest.raises(ValidationError):
             name = String()
-            name.load('Dummy Dummy Dummy')
+            name._load('Dummy Dummy Dummy')
diff --git a/tests/core/test_field_types.py b/tests/core/test_field_types.py
index 7b694bbe..381073a6 100644
--- a/tests/core/test_field_types.py
+++ b/tests/core/test_field_types.py
@@ -21,21 +21,21 @@ class TestStringField:
     def test_type_validation(self):
         """ Test type checking validation for the Field"""
         name = field.String(max_length=10)
-        assert name.load(1) == '1'
+        assert name._load(1) == '1'
 
     def test_min_length(self):
         """ Test minimum length validation for the string field"""
 
         with pytest.raises(ValidationError):
             name = field.String(min_length=5, max_length=10)
-            name.load('Dum')
+            name._load('Dum')
 
     def test_max_length(self):
         """ Test maximum length validation for the string field"""
 
         with pytest.raises(ValidationError):
             name = field.String(max_length=5)
-            name.load('Dummy Dummy')
+            name._load('Dummy Dummy')
 
     def test_choice(self):
         """ Test choices validations for the string field """
@@ -50,9 +50,9 @@ class TestStringField:
         assert status is not None
 
         # Test loading of values to the status field
-        assert status.load('Pending') == 'Pending'
+        assert status._load('Pending') == 'Pending'
         with pytest.raises(ValidationError) as e_info:
-            status.load('Failure')
+            status._load('Failure')
         assert e_info.value.normalized_messages == {
             '_entity': ["Value `'Failure'` is not a valid choice. "
                         "Must be one of ['Pending', 'Success', 'Error']"]}
@@ -71,21 +71,21 @@ class TestIntegerField:
         """ Test type checking validation for the Field"""
         with pytest.raises(ValidationError):
             age = field.Integer()
-            age.load('x')
+            age._load('x')
 
     def test_min_value(self):
         """ Test minimum value validation for the integer field"""
 
         with pytest.raises(ValidationError):
             age = field.Integer(min_value=5)
-            age.load(3)
+            age._load(3)
 
     def test_max_value(self):
         """ Test maximum value validation for the integer field"""
 
         with pytest.raises(ValidationError):
             age = field.Integer(max_value=5)
-            age.load(6)
+            age._load(6)
 
     def test_choice(self):
         """ Test choices validations for the Integer field """
@@ -100,9 +100,9 @@ class TestIntegerField:
         assert status is not None
 
         # Test loading of values to the status field
-        assert status.load(0) == 0
+        assert status._load(0) == 0
         with pytest.raises(ValidationError) as e_info:
-            status.load(4)
+            status._load(4)
         assert e_info.value.normalized_messages == {
             '_entity': ["Value `4` is not a valid choice. "
                         "Must be one of [0, 1, 2]"]}
@@ -121,21 +121,21 @@ class TestFloatField:
         """ Test type checking validation for the Field"""
         with pytest.raises(ValidationError):
             score = field.Float()
-            score.load('x')
+            score._load('x')
 
     def test_min_value(self):
         """ Test minimum value validation for the float field"""
 
         with pytest.raises(ValidationError):
             score = field.Float(min_value=5.4)
-            score.load(5.3)
+            score._load(5.3)
 
     def test_max_value(self):
         """ Test maximum value validation for the float field"""
 
         with pytest.raises(ValidationError):
             score = field.Float(max_value=5.5)
-            score.load(5.6)
+            score._load(5.6)
 
 
 class TestBooleanField:
@@ -146,13 +146,13 @@ class TestBooleanField:
 
         married = field.Boolean()
         assert married is not None
-        assert married.load(True) is True
+        assert married._load(True) is True
 
     def test_type_validation(self):
         """ Test type checking validation for the Field"""
         with pytest.raises(ValidationError):
             married = field.Boolean()
-            married.load('x')
+            married._load('x')
 
 
 class TestListField:
@@ -164,13 +164,13 @@ class TestListField:
         tags = field.List()
         assert tags is not None
 
-        assert tags.load(['x', 'y', 'z']) == ['x', 'y', 'z']
+        assert tags._load(['x', 'y', 'z']) == ['x', 'y', 'z']
 
     def test_type_validation(self):
         """ Test type checking validation for the Field"""
         with pytest.raises(ValidationError):
             tags = field.Boolean()
-            tags.load('x')
+            tags._load('x')
 
     def test_choice(self):
         """ Test choices validations for the list field """
@@ -185,9 +185,9 @@ class TestListField:
         assert status is not None
 
         # Test loading of values to the status field
-        assert status.load(['Pending']) == ['Pending']
+        assert status._load(['Pending']) == ['Pending']
         with pytest.raises(ValidationError) as e_info:
-            status.load(['Pending', 'Failure'])
+            status._load(['Pending', 'Failure'])
         assert e_info.value.normalized_messages == {
             '_entity': ["Value `'Failure'` is not a valid choice. "
                         "Must be one of ['Pending', 'Success', 'Error']"]}
@@ -202,14 +202,14 @@ class TestDictField:
         add_info = field.Dict()
         assert add_info is not None
 
-        value = add_info.load({'available': 'weekdays'})
+        value = add_info._load({'available': 'weekdays'})
         assert value == {'available': 'weekdays'}
 
     def test_type_validation(self):
         """ Test type checking validation for the Field"""
         with pytest.raises(ValidationError):
             add_info = field.Dict()
-            add_info.load('x')
+            add_info._load('x')
 
 
 class TestAutoField:
@@ -221,13 +221,13 @@ class TestAutoField:
         add_info = field.Auto()
         assert add_info is not None
 
-        value = add_info.load(1)
+        value = add_info._load(1)
         assert value == 1
 
     def test_validation(self):
         """ Test validation for the Auto Field"""
         add_info = field.Auto(required=True)
-        add_info.load(None)
+        add_info._load(None)
 
 
 class TestDateField:
@@ -239,7 +239,7 @@ class TestDateField:
         age = field.Date()
         assert age is not None
 
-        value = age.load(datetime.now().date())
+        value = age._load(datetime.now().date())
         assert value == datetime.now().date()
 
     def test_type_casting(self):
@@ -248,17 +248,17 @@ class TestDateField:
         age = field.Date()
 
         # Test datetime being passed as value
-        assert age.load(datetime.now()) == datetime.now().date()
+        assert age._load(datetime.now()) == datetime.now().date()
 
         # Test string dates being passed as value
         expected = datetime(2018, 3, 16).date()
-        assert age.load('2018-03-16') == expected
-        assert age.load('2018-03-16 10:23:32') == expected
-        assert age.load('16th March 2018') == expected
+        assert age._load('2018-03-16') == expected
+        assert age._load('2018-03-16 10:23:32') == expected
+        assert age._load('16th March 2018') == expected
 
         # Test for invalid date
         with pytest.raises(ValidationError):
-            assert age.load('15 Marchs')
+            assert age._load('15 Marchs')
 
 
 class TestDateTimeField:
@@ -271,7 +271,7 @@ class TestDateTimeField:
         assert created_at is not None
 
         value = datetime.now()
-        assert value == created_at.load(value)
+        assert value == created_at._load(value)
 
     def test_type_casting(self):
         """ Test type casting and validation for the Field"""
@@ -279,17 +279,17 @@ class TestDateTimeField:
         created_at = field.DateTime()
         today = datetime.now()
         # Test date being passed as value
-        assert created_at.load(today.date()) == datetime(
+        assert created_at._load(today.date()) == datetime(
             today.year, today.month, today.day)
 
         # Test string dates being passed as value
-        assert created_at.load('2018-03-16') == datetime(2018, 3, 16)
-        assert created_at.load('2018-03-16 10:23:32') == datetime(
+        assert created_at._load('2018-03-16') == datetime(2018, 3, 16)
+        assert created_at._load('2018-03-16 10:23:32') == datetime(
             2018, 3, 16, 10, 23, 32)
 
         # Test for invalid datetime
         with pytest.raises(ValidationError):
-            assert created_at.load('2018-03-16 10 23 32')
+            assert created_at._load('2018-03-16 10 23 32')
 
 
 class TestTextField:
@@ -304,5 +304,5 @@ class TestTextField:
     def test_type_validation(self):
         """ Test type checking validation for the Field"""
         address = field.Text()
-        value = address.load('My home address')
+        value = address._load('My home address')
         assert value == 'My home address'
diff --git a/tests/core/test_repository.py b/tests/core/test_repository.py
index 52711c5a..7b4fee0c 100644
--- a/tests/core/test_repository.py
+++ b/tests/core/test_repository.py
@@ -128,7 +128,6 @@ class TestLookup:
     class SampleLookup(Lookup):
         """A simple implementation of lookup class"""
         lookup_name = "sample"
-
         def as_expression(self):
             return '%s %s %s' % (self.process_source(),
                                  '<<<>>>',
diff --git a/tests/test_config.py b/tests/test_config.py
index 7189f16c..c08ca9ca 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -18,5 +18,5 @@ def test_config_module():
     # Set the config file and make sure values get loaded
     os.environ['PROTEAN_CONFIG'] = 'tests.support.sample_config'
     config = Config()
-    assert config.TESTING
-    assert config.SECRET_KEY == 'abcdefghijklmn'
+    assert config.TESTING  # pylint: disable=E1101
+    assert config.SECRET_KEY == 'abcdefghijklmn'  # pylint: disable=E1101