src/pyams_utils/container.py
changeset 289 c8e21d7dd685
child 292 b338586588ad
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_utils/container.py	Wed Dec 05 12:45:56 2018 +0100
@@ -0,0 +1,137 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.container.interfaces import IContainer, IContained
+from zope.lifecycleevent.interfaces import IObjectMovedEvent
+from zope.location.interfaces import ISublocations
+
+# import packages
+from BTrees.OOBTree import OOBTree
+from persistent.list import PersistentList
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyramid.threadlocal import get_current_registry
+from zope.container.ordered import OrderedContainer
+
+
+class BTreeOrderedContainer(OrderedContainer):
+    """BTree based ordered container
+
+    This container maintain a manual order of it's contents
+    """
+
+    def __init__(self):
+        self._data = OOBTree()
+        self._order = PersistentList()
+
+
+class ParentSelector(object):
+    """Interface based parent selector
+
+    This selector can be used as a subscriber predicate on IObjectAddedEvent to define
+    an interface that the new parent must support for the event to be applied::
+
+    .. code-block:: python
+
+        from pyams_utils.interfaces.site import ISiteRoot
+
+        @subscriber(IObjectAddedEvent, parent_selector=ISiteRoot)
+        def siteroot_object_added_event_handler(event):
+            '''This is an event handler for an ISiteRoot object added event'''
+    """
+
+    def __init__(self, ifaces, config):
+        if not isinstance(ifaces, (list, tuple, set)):
+            ifaces = (ifaces,)
+        self.interfaces = ifaces
+
+    def text(self):
+        return 'parent_selector = %s' % str(self.interfaces)
+
+    phash = text
+
+    def __call__(self, event):
+        if not IObjectMovedEvent.providedBy(event):
+            return False
+        for intf in self.interfaces:
+            try:
+                if intf.providedBy(event.newParent):
+                    return True
+            except (AttributeError, TypeError):
+                if isinstance(event.newParent, intf):
+                    return True
+        return False
+
+
+@adapter_config(context=IContained, provides=ISublocations)
+class ContainerSublocationsAdapter(ContextAdapter):
+    """Contained object sub-locations adapter
+
+    This adapter checks for custom ISublocations interface adapters which can
+    be defined by any component to get access to inner locations, defined for
+    example via annotations.
+    """
+
+    def sublocations(self):
+        """See `zope.location.interfaces.ISublocations` interface"""
+        context = self.context
+        # Check for adapted sub-locations first...
+        registry = get_current_registry()
+        for name, adapter in registry.getAdapters((context,), ISublocations):
+            if not name:  # don't reuse default adapter!!
+                continue
+            yield from adapter.sublocations()
+        # then yield container items
+        if IContainer.providedBy(context):
+            yield from context.values()
+
+
+def find_objects_matching(root, condition, ignore_root=False):
+    """Find all objects in root that match the condition
+
+    The condition is a Python callable object that takes an object as
+    argument and must return a boolean result.
+
+    All sub-objects of the root will also be searched recursively.
+
+    :param object root: the parent object from which search is started
+    :param callable condition: a callable object which may return true for a given
+        object to be selected
+    :param boolean ignore_root: if *True*, the root object will not be returned, even if it matches
+        the given condition
+    :return: an iterator for all root's sub-objects matching condition
+    """
+    if (not ignore_root) and condition(root):
+        yield root
+    locations = ISublocations(root, None)
+    if locations is not None:
+        for location in locations.sublocations():
+            if condition(location):
+                yield location
+            yield from find_objects_matching(location, condition, ignore_root=True)
+
+
+def find_objects_providing(root, interface):
+    """Find all objects in root that provide the specified interface
+
+    All sub-objects of the root will also be searched recursively.
+
+    :param object root: object; the parent object from which search is started
+    :param Interface interface: interface; an interface that sub-objects should provide
+    :return: an iterator for all root's sub-objects that provide the given interface
+    """
+    yield from find_objects_matching(root, interface.providedBy)