Source code for nti.traversal.traversal

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Generic traversal functions (and adapters).
"""

from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

import warnings

import six

from zope.interface import implementer
from zope.component import queryMultiAdapter
from zope.container.traversal import ContainerTraversable as _ContainerTraversable

from zope.location import LocationIterator

from zope.location.interfaces import IContained
from zope.location.interfaces import ILocationInfo

from zope.traversing.adapters import DefaultTraversable as _DefaultTraversable

from zope.traversing.interfaces import IPathAdapter
from zope.traversing.interfaces import ITraversable

from zope.traversing.namespace import adapter

from nti.traversal.compat import join_path_tuple

from nti.traversal.location import find_interface as _p_find_interface

logger = __import__('logging').getLogger(__name__)

__all__ = [
    'resource_path',
    'normal_resource_path',
    'is_valid_resource_path',
    'find_nearest_site',
    'find_interface',
    # TODO: The adapters should go in adapters.py
    'path_adapter',
    'adapter_request',
    'ContainerAdapterTraversable',
    'DefaultAdapterTraversable',
]

[docs]def resource_path(res): # This function is somewhat more flexible than Pyramid's, and # also more strict. It requires strings (not None, for example) # and bottoms out at an IRoot. This helps us get things right. # It is probably also a bit slower. # Could probably use a __traceback_supplement__ for this _known_parents = [] # Ask for the parents; we do this instead of getPath() and url_quote # to work properly with unicode paths through the magic of pyramid loc_info = ILocationInfo(res) try: # pylint: disable=too-many-function-args parents = loc_info.getParents() except TypeError: # "Not enough context information to get all parents" # This is a programming/design error: some object is not where it # should be _known_parents.extend(LocationIterator(res)) logger.exception("Failed to get all parents of %r; known parents: %s", res, _known_parents) raise if parents: # Take the root off, it's implicit and has a name of None parents.pop() # Put it in the order pyramid expects, root first # (root is added only to the names to avoid prepending) parents.reverse() parents.append(res) # And let pyramid construct the URL, doing proper escaping and # also caching. names = [''] # Bottom out at the root for p in parents: name = p.__name__ if name is None: __traceback_info__ = p raise TypeError("Element in the hierarchy is missing __name__") names.append(name) return join_path_tuple(tuple(names))
[docs]def normal_resource_path(res): """ :return: The result of traversing the containers of `res`, but normalized by removing double slashes. This is useful when elements in the containment hierarchy do not have a name; however, it can hide bugs when all elements are expected to have names. """ # If this starts to get complicated, we can take a dependency # on the urlnorm library result = resource_path(res) result = result.replace('//', '/') # Our LocalSiteManager is sneaking in here, which we don't want... # result = result.replace( '%2B%2Betc%2B%2Bsite/', '' ) return result
[docs]def is_valid_resource_path(target): # We really want to check if this is a valid HTTP URL path. How best to do that? # Not documented until we figure it out. return isinstance(target, six.string_types) and (target.startswith('/') or target.startswith('http://') or target.startswith('https://'))
[docs]def find_nearest_site(context, root=None, ignore=None): """ Find the nearest :class:`ISite` in the lineage of *context*. :param context: The object whose lineage to search. If this object cannot be adapted to `ILocationInfo`, then we attempt to adapt ``context.target`` and get its site; failing that, we return the *root*. :param ignore: If the `ILocationInfo` of the *context* can be retrieved, but the :meth:`.ILocationInfo.getNearestSite` cannot, then, if *ignore* is given, and *context* provides that interface, return the *root*. This makes no sense and is deprecated. :return: The nearest site. Possibly the root site. .. deprecated:: 1.0 Relying on the fallback to ``context.target`` and *root* is deprecated; the *ignore* parameter is deprecated. All of these things signal a broken resource tree. """ try: loc_info = ILocationInfo(context) except TypeError: # Not adaptable (not located). What about the target? try: # pylint: disable=too-many-function-args loc_info = ILocationInfo(context.target) warnings.warn( "Relying on ``context.target`` is deprecated. " "Register an ILocationInfo adapter for ``context`` instead.", FutureWarning, stacklevel=2 ) nearest_site = loc_info.getNearestSite() except (TypeError, AttributeError): # Nothing. Assume the main site/root nearest_site = root else: # Located. Better be able to get a site, otherwise we have a # broken chain. try: # pylint: disable=too-many-function-args nearest_site = loc_info.getNearestSite() except TypeError: # Convertible, but not located correctly. if ignore is None or not ignore.providedBy(context): raise warnings.warn( "The ignore argument is deprecated. " "Register an appropriate ILocationInfo instead.", FutureWarning, stacklevel=2 ) nearest_site = root return nearest_site
[docs]def find_interface(resource, interface, strict=True): # pylint:disable=inconsistent-return-statements """ Given an object, find the first object in its lineage providing the given interface. This is similar to :func:`nti.traversal.location.find_interface`, but, as with :func:`resource_path` requires the strict adherence to the resource tree, unless ``strict`` is set to ``False``. :keyword bool strict: Deprecated. Do not use. Non-strict lineage is broken lineage. """ if not strict: return _p_find_interface(resource, interface) # pylint: disable=too-many-function-args lineage = ILocationInfo(resource).getParents() lineage.insert(0, resource) for item in lineage: if interface.providedBy(item): return item
[docs]def path_adapter(context, request, name=''): return queryMultiAdapter((context, request), IPathAdapter, name)
[docs]class adapter_request(adapter): """ Implementation of the adapter namespace that attempts to pass the request along when getting an adapter. """ def __init__(self, context, request=None): super(adapter_request, self).__init__(context, request) self.request = request
[docs] def traverse(self, name, ignored): result = None if self.request is not None: result = path_adapter(self.context, self.request, name) if result is None: # Look for the single-adapter. Or raise location error result = super(adapter_request, self).traverse(name, ignored) # Some sanity checks on the returned object # pylint: disable=unused-variable __traceback_info__ = result, self.context, result.__parent__, result.__name__ assert IContained.providedBy(result) assert result.__parent__ is not None if result.__name__ is None: result.__name__ = name assert result.__name__ == name return result
[docs]@implementer(ITraversable) class ContainerAdapterTraversable(_ContainerTraversable): """ A :class:`ITraversable` implementation for containers that also automatically looks for named path adapters *if* the container has no matching key. You may subclass this traversable or register it in ZCML directly. It is usable both with and without a request. """ context = property(lambda self: getattr(self, "_container"), lambda self, nv: setattr(self, "_container", nv)) def __init__(self, context, request=None): super(ContainerAdapterTraversable, self).__init__(context) self.context = context self.request = request
[docs] def traverse(self, key, remaining_path): # pylint: disable=arguments-differ try: return super(ContainerAdapterTraversable, self).traverse(key, remaining_path) except KeyError: # Is there a named path adapter? adapted = adapter_request(self.context, self.request) return adapted.traverse(key, remaining_path)
[docs]@implementer(ITraversable) class DefaultAdapterTraversable(_DefaultTraversable): """ A :class:`ITraversable` implementation for ordinary objects that also automatically looks for named path adapters *if* the traversal found no matching path. You may subclass this traversable or register it in ZCML directly. It is usable both with and without a request. """ def __init__(self, context, request=None): super(DefaultAdapterTraversable, self).__init__(context) self.context = context self.request = request
[docs] def traverse(self, key, remaining_path): # pylint: disable=arguments-differ try: return super(DefaultAdapterTraversable, self).traverse(key, remaining_path) except KeyError: # Is there a named path adapter? adapted = adapter_request(self.context, self.request) return adapted.traverse(key, remaining_path)