src/pyams_utils/inherit.py
branchdev-tf
changeset 408 cf2304af0fab
parent 367 2c95d34496f5
--- a/src/pyams_utils/inherit.py	Wed Nov 20 19:26:23 2019 +0100
+++ b/src/pyams_utils/inherit.py	Fri Nov 22 18:51:37 2019 +0100
@@ -15,9 +15,114 @@
 This module is used to manage a generic inheritance between a content and
 it's parent container. It also defines a custom InheritedFieldProperty which
 allows to automatically manage inherited properties.
-"""
+
+This PyAMS module is used to handle inheritance between a parent object and a child which can
+"inherit" from some of it's properties, as long as they share the same "target" interface.
+
+    >>> from zope.interface import implementer, Interface, Attribute
+    >>> from zope.schema import TextLine
+    >>> from zope.schema.fieldproperty import FieldProperty
+
+    >>> from pyams_utils.adapter import adapter_config
+    >>> from pyams_utils.interfaces.inherit import IInheritInfo
+    >>> from pyams_utils.inherit import BaseInheritInfo, InheritedFieldProperty
+    >>> from pyams_utils.registry import get_global_registry
+
+Let's start by creating a "content" interface, and a marker interface for objects for which we
+want to provide this interface:
+
+    >>> class IMyInfoInterface(IInheritInfo):
+    ...     '''Custom interface'''
+    ...     value = TextLine(title="Custom attribute")
+
+    >>> class IMyTargetInterface(Interface):
+    ...     '''Target interface'''
+
+    >>> @implementer(IMyInfoInterface)
+    ... class MyInfo(BaseInheritInfo):
+    ...     target_interface = IMyTargetInterface
+    ...     adapted_interface = IMyInfoInterface
+    ...
+    ...     _value = FieldProperty(IMyInfoInterface['value'])
+    ...     value = InheritedFieldProperty(IMyInfoInterface['value'])
+
+Please note that for each field of the interface which can be inherited, you must define to
+properties: one using "InheritedFieldProperty" with the name of the field, and one using a classic
+"FieldProperty" with the same name prefixed by "_"; this property is used to store the "local"
+property value, when inheritance is unset.
+
+The adapter is created to adapt an object providing IMyTargetInterface to IMyInfoInterface;
+please note that the adapter *must* attach the created object to it's parent by setting
+__parent__ attribute:
+
+    >>> @adapter_config(context=IMyTargetInterface, provides=IMyInfoInterface)
+    ... def my_info_factory(context):
+    ...     info = getattr(context, '__info__', None)
+    ...     if info is None:
+    ...         info = context.__info__ = MyInfo()
+    ...         info.__parent__ = context
+    ...     return info
+
+Adapter registration is here only for testing; the "adapter_config" decorator may do the job in
+a normal application context:
+
+    >>> registry = get_global_registry()
+    >>> registry.registerAdapter(my_info_factory, (IMyTargetInterface, ), IMyInfoInterface)
 
-__docformat__ = 'restructuredtext'
+We can then create classes which will be adapted to support inheritance:
+
+    >>> @implementer(IMyTargetInterface)
+    ... class MyTarget:
+    ...     '''Target class'''
+    ...     __parent__ = None
+    ...     __info__ = None
+
+    >>> parent = MyTarget()
+    >>> parent_info = IMyInfoInterface(parent)
+    >>> parent.__info__
+    <pyams_utils.tests.test_utils...MyInfo object at ...>
+    >>> parent_info.value = 'parent'
+    >>> parent_info.value
+    'parent'
+    >>> parent_info.can_inherit
+    False
+
+As soon as a parent is defined, the child object can inherit from it's parent:
+
+    >>> child = MyTarget()
+    >>> child.__parent__ = parent
+    >>> child_info = IMyInfoInterface(child)
+    >>> child.__info__
+    <pyams_utils.tests.test_utils...MyInfo object at ...>
+
+    >>> child_info.can_inherit
+    True
+    >>> child_info.inherit
+    True
+    >>> child_info.value
+    'parent'
+
+Setting child value while inheritance is enabled donesn't have any effect:
+
+    >>> child_info.value = 'child'
+    >>> child_info.value
+    'parent'
+    >>> child_info.inherit_from == parent
+    True
+
+You can disable inheritance and define your own value:
+
+    >>> child_info.inherit = False
+    >>> child_info.value = 'child'
+    >>> child_info.value
+    'child'
+    >>> child_info.inherit_from == child
+    True
+
+Please note that parent and child in this example share the same class, but this is not a
+requirement; they just have to implement the same marker interface, to be adapted to the same
+content interface.
+"""
 
 from zope.interface import Interface, implementer
 from zope.location import Location
@@ -28,9 +133,17 @@
 from pyams_utils.zodb import volatile_property
 
 
+__docformat__ = 'restructuredtext'
+
+
 @implementer(IInheritInfo)
 class BaseInheritInfo(Location):
-    """Base inherit class"""
+    """Base inherit class
+
+    Subclasses may generaly override target_interface and adapted_interface to
+    correctly handle inheritance (see example in doctests).
+    Please note also that adapters to this interface must correctly 'locate'
+    """
 
     target_interface = Interface
     adapted_interface = Interface
@@ -73,14 +186,14 @@
     def inherit_from(self):
         """Get current parent from which we inherit"""
         if not self.inherit:
-            return self
+            return self.__parent__
         parent = self.parent
         while self.adapted_interface(parent).inherit:
-            parent = parent.parent
+            parent = parent.parent  # pylint: disable=no-member
         return parent
 
 
-class InheritedFieldProperty(object):
+class InheritedFieldProperty:
     """Inherited field property"""
 
     def __init__(self, field, name=None):
@@ -94,10 +207,10 @@
         if inst is None:
             return self
         inherit_info = IInheritInfo(inst)
-        if inherit_info.inherit:
+        if inherit_info.inherit and (inherit_info.parent is not None):
+            # pylint: disable=not-callable
             return getattr(inherit_info.adapted_interface(inherit_info.parent), self.__name)
-        else:
-            return getattr(inst, '_{0}'.format(self.__name))
+        return getattr(inst, '_{0}'.format(self.__name))
 
     def __set__(self, inst, value):
         inherit_info = IInheritInfo(inst)