diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | CHANGES.rst | 2 | ||||
-rw-r--r-- | README.rst | 11 | ||||
-rw-r--r-- | doc-requirements.txt | 1 | ||||
-rw-r--r-- | docs/adapters.rst | 5 | ||||
-rw-r--r-- | docs/api.rst | 5 | ||||
-rw-r--r-- | docs/browser/absoluteurl.rst | 5 | ||||
-rw-r--r-- | docs/browser/interfaces.rst | 5 | ||||
-rw-r--r-- | docs/changelog.rst | 1 | ||||
-rw-r--r-- | docs/conf.py | 283 | ||||
-rw-r--r-- | docs/index.rst | 35 | ||||
-rw-r--r-- | docs/interfaces.rst | 5 | ||||
-rw-r--r-- | docs/namespace.rst | 5 | ||||
-rw-r--r-- | docs/publicationtraverse.rst | 5 | ||||
-rw-r--r-- | setup.py | 4 | ||||
-rw-r--r-- | src/zope/traversing/adapters.py | 46 | ||||
-rw-r--r-- | src/zope/traversing/api.py | 91 | ||||
-rw-r--r-- | src/zope/traversing/browser/absoluteurl.py | 16 | ||||
-rw-r--r-- | src/zope/traversing/browser/interfaces.py | 18 | ||||
-rw-r--r-- | src/zope/traversing/interfaces.py | 103 | ||||
-rw-r--r-- | src/zope/traversing/namespace.py | 311 | ||||
-rw-r--r-- | src/zope/traversing/publicationtraverse.py | 18 | ||||
-rw-r--r-- | src/zope/traversing/tests/test_namespacetrversal.py | 9 | ||||
-rw-r--r-- | tox.ini | 11 |
24 files changed, 763 insertions, 233 deletions
@@ -11,3 +11,4 @@ eggs/ parts/ .coverage htmlcov/ +docs/_build/ diff --git a/CHANGES.rst b/CHANGES.rst index 23ecdb2..a95e93d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,7 @@ 4.2.1 (unreleased) ================== -- Nothing changed yet. +- Host documentation at https://zopetraversing.readthedocs.io/ 4.2.0 (2017-09-23) @@ -1,6 +1,6 @@ -===================== - ``zope.traversing`` -===================== +================= + zope.traversing +================= .. image:: https://img.shields.io/pypi/v/zope.traversing.svg :target: https://pypi.python.org/pypi/zope.traversing/ @@ -16,8 +16,13 @@ .. image:: https://coveralls.io/repos/github/zopefoundation/zope.traversing/badge.svg?branch=master :target: https://coveralls.io/github/zopefoundation/zope.traversing?branch=master +.. image:: https://readthedocs.org/projects/zopetraversing/badge/?version=latest + :target: https://zopetraversing.readthedocs.io/en/latest/ + :alt: Documentation Status This package provides adapters for resolving object paths by traversing an object hierarchy. This package also includes support for traversal namespaces (e.g. ``++view++``, ``++skin++``, etc.) as well as computing URLs via the ``@@absolute_url`` view. + +Documentation is hosted at https://zopetraversing.readthedocs.io/ diff --git a/doc-requirements.txt b/doc-requirements.txt new file mode 100644 index 0000000..e9704b8 --- /dev/null +++ b/doc-requirements.txt @@ -0,0 +1 @@ +.[docs] diff --git a/docs/adapters.rst b/docs/adapters.rst new file mode 100644 index 0000000..6deec59 --- /dev/null +++ b/docs/adapters.rst @@ -0,0 +1,5 @@ +==================== + Traversal Adapters +==================== + +.. automodule:: zope.traversing.adapters diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..0ebcb51 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,5 @@ +=========================== + Traversal Convenience API +=========================== + +.. automodule:: zope.traversing.api diff --git a/docs/browser/absoluteurl.rst b/docs/browser/absoluteurl.rst new file mode 100644 index 0000000..6e7d248 --- /dev/null +++ b/docs/browser/absoluteurl.rst @@ -0,0 +1,5 @@ +======================= + Browser Absolute URLs +======================= + +.. automodule:: zope.traversing.browser.absoluteurl diff --git a/docs/browser/interfaces.rst b/docs/browser/interfaces.rst new file mode 100644 index 0000000..3540b18 --- /dev/null +++ b/docs/browser/interfaces.rst @@ -0,0 +1,5 @@ +==================== + Browser Interfaces +==================== + +.. automodule:: zope.traversing.browser.interfaces diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..d9e113e --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1 @@ +.. include:: ../CHANGES.rst diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..722a25c --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,283 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# zope.traversing documentation build configuration file, created by +# sphinx-quickstart on Thu Jan 29 11:31:12 2015. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import pkg_resources +sys.path.append(os.path.abspath('../src')) +rqmt = pkg_resources.require('zope.traversing')[0] + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'repoze.sphinx.autointerface', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'zope.traversing' +copyright = '2015-2017, Zope Foundation and Contributors' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '%s.%s' % tuple(map(int, rqmt.version.split('.')[:2])) +# The full version, including alpha/beta/rc tags. +release = rqmt.version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +default_role = 'obj' + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +#html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'zopetraversingdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'zopetraversing.tex', 'zope.traversing Documentation', + 'Zope Foundation and Contributors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'zopetraversing', 'zope.traversing Documentation', + ['Zope Foundation and Contributors'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'zopetraversing', 'zope.traversing Documentation', + 'Zope Foundation and Contributors', 'zopetraversing', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'https://docs.python.org/': None, + 'https://zopelocation.readthedocs.io/en/latest/': None, + 'https://zopeinterface.readthedocs.io/en/latest/': None, + 'https://zopepublisher.readthedocs.io/en/latest/': None, + 'https://zopeconfiguration.readthedocs.io/en/latest/': None, + 'https://zopei18n.readthedocs.io/en/latest/': None, +} + +autodoc_default_flags = ['members', 'show-inheritance'] +autoclass_content = 'both' +autodoc_member_order = 'bysource' diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..67fdd2b --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,35 @@ +.. include:: ../README.rst + + +API Documentation: + +.. toctree:: + :maxdepth: 2 + + interfaces + api + adapters + namespace + publicationtraverse + +Browser Documentation: + +.. toctree:: + :maxdepth: 2 + + browser/interfaces + browser/absoluteurl + + +.. toctree:: + :maxdepth: 2 + + changelog + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/interfaces.rst b/docs/interfaces.rst new file mode 100644 index 0000000..b4974a5 --- /dev/null +++ b/docs/interfaces.rst @@ -0,0 +1,5 @@ +============ + Interfaces +============ + +.. automodule:: zope.traversing.interfaces diff --git a/docs/namespace.rst b/docs/namespace.rst new file mode 100644 index 0000000..419f7c5 --- /dev/null +++ b/docs/namespace.rst @@ -0,0 +1,5 @@ +============ + Namespaces +============ + +.. automodule:: zope.traversing.namespace diff --git a/docs/publicationtraverse.rst b/docs/publicationtraverse.rst new file mode 100644 index 0000000..d4240a5 --- /dev/null +++ b/docs/publicationtraverse.rst @@ -0,0 +1,5 @@ +======================= + Publication Traverser +======================= + +.. automodule:: zope.traversing.publicationtraverse @@ -75,6 +75,10 @@ setup( namespace_packages=['zope'], extras_require={ 'test': TESTS_REQUIRE, + 'docs': [ + 'Sphinx', + 'repoze.sphinx.autointerface', + ], }, install_requires=[ 'setuptools', diff --git a/src/zope/traversing/adapters.py b/src/zope/traversing/adapters.py index 45d6e92..69ad836 100644 --- a/src/zope/traversing/adapters.py +++ b/src/zope/traversing/adapters.py @@ -11,7 +11,8 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Adapters for the traversing mechanism +""" +Adapters for the traversing mechanism. """ import six @@ -30,7 +31,11 @@ _marker = object() # opaque marker that doesn't get security proxied @zope.interface.implementer(ITraversable) class DefaultTraversable(object): - """Traverses objects via attribute and item lookup""" + """ + Traverses objects via attribute and item lookup. + + Implements `~zope.traversing.interfaces.ITraversable`. + """ def __init__(self, subject): self._subject = subject @@ -58,7 +63,11 @@ class DefaultTraversable(object): @zope.interface.implementer(ITraverser) class Traverser(object): - """Provide traverse features""" + """ + Provide traverse features. + + Implements `~zope.traversing.interfaces.ITraverser`. + """ # This adapter can be used for any object. @@ -99,24 +108,29 @@ class Traverser(object): def traversePathElement(obj, name, further_path, default=_marker, traversable=None, request=None): - """Traverse a single step 'name' relative to the given object. - - 'name' must be a string. '.' and '..' are treated specially, as well as - names starting with '@' or '+'. Otherwise 'name' will be treated as a - single path segment. + """ + Traverse a single step *name* relative to the given object. - 'further_path' is a list of names still to be traversed. This method - is allowed to change the contents of 'further_path'. + This is used to implement + :meth:`zope.traversing.interfaces.ITraversalAPI.traverseName`. - You can explicitly pass in an ITraversable as the 'traversable' - argument. If you do not, the given object will be adapted to ITraversable. + :param str name: must be a string. '.' and '..' are treated + specially, as well as names starting with '@' or '+'. + Otherwise *name* will be treated as a single path segment. + :param list further_path: a list of names still to be traversed. + This method is allowed to change the contents of + *further_path*. - 'request' is passed in when traversing from presentation code. This - allows paths like @@foo to work. + :keyword ITraversable traversable: You can explicitly pass in + an `~zope.traversing.interfaces.ITraversable` as the + *traversable* argument. If you do not, the given object will + be adapted to ``ITraversable``. - Raises LocationError if path cannot be found and 'default' was - not provided. + :keyword request: assed in when traversing from presentation + code. This allows paths like ``@@foo`` to work. + :raises zope.location.interfaces.LocationError: if *path* cannot + be found and '*default* was not provided. """ __traceback_info__ = (obj, name) diff --git a/src/zope/traversing/api.py b/src/zope/traversing/api.py index 7a938b2..c9b0cb3 100644 --- a/src/zope/traversing/api.py +++ b/src/zope/traversing/api.py @@ -11,7 +11,10 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""Convenience functions for traversing the object tree. +""" +Convenience functions for traversing the object tree. + +This module provides :class:`zope.traversing.interfaces.ITraversalAPI` """ import six from zope.interface import moduleProvides @@ -26,22 +29,8 @@ _marker = object() def joinPath(path, *args): - """Join the given relative paths to the given path. - - Returns a unicode path. - - The path should be well-formed, and not end in a '/' unless it is - the root path. It can be either a string (ascii only) or unicode. - The positional arguments are relative paths to be added to the - path as new path segments. The path may be absolute or relative. - - A segment may not start with a '/' because that would be confused - with an absolute path. A segment may not end with a '/' because we - do not allow '/' at the end of relative paths. A segment may - consist of . or .. to mean "the same place", or "the parent path" - respectively. A '.' should be removed and a '..' should cause the - segment to the left to be removed. joinPath('/', '..') should - raise an exception. + """ + Join the given relative paths to the given path. """ if not args: @@ -58,32 +47,21 @@ def joinPath(path, *args): def getPath(obj): - """Returns a string representing the physical path to the object. + """ + Returns a string representing the physical path to the object. """ return ILocationInfo(obj).getPath() def getRoot(obj): - """Returns the root of the traversal for the given object. + """ + Returns the root of the traversal for the given object. """ return ILocationInfo(obj).getRoot() def traverse(object, path, default=_marker, request=None): - """Traverse 'path' relative to the given object. - - 'path' is a string with path segments separated by '/'. - - 'request' is passed in when traversing from presentation code. This - allows paths like @@foo to work. - - Raises LocationError if path cannot be found - - Note: calling traverse with a path argument taken from an untrusted - source, such as an HTTP request form variable, is a bad idea. - It could allow a maliciously constructed request to call - code unexpectedly. - Consider using traverseName instead. + """ """ traverser = ITraverser(object) if default is _marker: @@ -92,21 +70,8 @@ def traverse(object, path, default=_marker, request=None): def traverseName(obj, name, default=_marker, traversable=None, request=None): - """Traverse a single step 'name' relative to the given object. - - 'name' must be a string. '.' and '..' are treated specially, as well as - names starting with '@' or '+'. Otherwise 'name' will be treated as a - single path segment. - - You can explicitly pass in an ITraversable as the 'traversable' - argument. If you do not, the given object will be adapted to ITraversable. - - 'request' is passed in when traversing from presentation code. This - allows paths like @@foo to work. - - Raises LocationError if path cannot be found and 'default' was - not provided. - + """ + Traverse a single step 'name' relative to the given object. """ further_path = [] if default is _marker: @@ -122,17 +87,15 @@ def traverseName(obj, name, default=_marker, traversable=None, request=None): def getName(obj): - """Get the name an object was traversed via + """ + Get the name an object was traversed via """ return ILocationInfo(obj).getName() def getParent(obj): - """Returns the container the object was traversed via. - - Returns None if the object is a containment root. - Raises TypeError if the object doesn't have enough context to get the - parent. + """ + Returns the container the object was traversed via. """ try: location_info = ILocationInfo(obj) @@ -157,11 +120,9 @@ def getParent(obj): def getParents(obj): - """Returns a list starting with the given object's parent followed by + """ + Returns a list starting with the given object's parent followed by each of its parents. - - Raises a TypeError if the context doesn't go all the way down to - a containment root. """ return ILocationInfo(obj).getParents() @@ -194,11 +155,9 @@ def _normalizePath(path): def canonicalPath(path_or_object): - """Returns a canonical absolute unicode path for the given path or object. - - Resolves segments that are '.' or '..'. - - Raises ValueError if a badly formed path is given. + """ + Returns a canonical absolute unicode path for the given path or + object. """ if isinstance(path_or_object, six.string_types): path = path_or_object @@ -223,3 +182,9 @@ def canonicalPath(path_or_object): # import this down here to avoid circular imports from zope.traversing.adapters import traversePathElement + +# Synchronize the documentation. +for name in ITraversalAPI.names(): + if name in globals(): + globals()[name].__doc__ = ITraversalAPI[name].__doc__ +del name diff --git a/src/zope/traversing/browser/absoluteurl.py b/src/zope/traversing/browser/absoluteurl.py index 9b89d46..285b9de 100644 --- a/src/zope/traversing/browser/absoluteurl.py +++ b/src/zope/traversing/browser/absoluteurl.py @@ -11,7 +11,12 @@ # FOR A PARTICULAR PURPOSE # ############################################################################## -"""Absolute URL View components +""" +Absolute URL View components. + +These are registered as views and named views (``absolute_url``) if +you load this package's ``configure.zcml`` with +:mod:`zope.configuration.xmlconfig`. """ try: from urllib.parse import quote_from_bytes as quote @@ -52,6 +57,10 @@ class _EncodedUnicode(object): @implementer(IAbsoluteURL) class AbsoluteURL(_EncodedUnicode, BrowserView): + """ + The default implementation of + :class:`zope.traversing.browser.interfaces.IAbsoluteURL`. + """ def __str__(self): context = self.context @@ -131,6 +140,11 @@ class AbsoluteURL(_EncodedUnicode, @implementer(IAbsoluteURL) class SiteAbsoluteURL(_EncodedUnicode, BrowserView): + """ + An implementation of + :class:`zope.traversing.browser.interfaces.IAbsoluteURL` for site + root objects (:class:`zope.location.interfaces.IRoot`). + """ def __str__(self): context = self.context diff --git a/src/zope/traversing/browser/interfaces.py b/src/zope/traversing/browser/interfaces.py index 0ef4f62..df468ce 100644 --- a/src/zope/traversing/browser/interfaces.py +++ b/src/zope/traversing/browser/interfaces.py @@ -17,6 +17,12 @@ from zope.interface import Interface class IAbsoluteURL(Interface): + """ + An absolute URL. + + These are typically registered as adapters or multi-adapters + for objects. + """ def __unicode__(): """Returns the URL as a unicode string.""" @@ -39,6 +45,16 @@ class IAbsoluteURL(Interface): class IAbsoluteURLAPI(Interface): + """ + The api to compute absolute URLs of objects. + + Provided by :mod:`zope.traversing.browser.absoluteurl` + """ def absoluteURL(ob, request): - """Compute the absolute URL of an object """ + """ + Compute the absolute URL of an object. + + This should return an ASCII string by looking up an adapter + from `(ob, request)` to :class:`IAbsoluteURL` and then calling it. + """ diff --git a/src/zope/traversing/interfaces.py b/src/zope/traversing/interfaces.py index cd515ed..ad3b410 100644 --- a/src/zope/traversing/interfaces.py +++ b/src/zope/traversing/interfaces.py @@ -36,12 +36,12 @@ class ITraversable(Interface): """Get the next item on the path Should return the item corresponding to 'name' or raise - LocationError where appropriate. + :exc:`~zope.location.interfaces.LocationError` where appropriate. - 'name' is an ASCII string or Unicode object. - - 'furtherPath' is a list of names still to be traversed. This - method is allowed to change the contents of furtherPath. + :param str name: an ASCII string or Unicode object. + :param list furtherPath: is a list of names still to be + traversed. This method is allowed to change the contents + of furtherPath. """ @@ -62,31 +62,34 @@ class ITraverser(Interface): path begins with a '/', start at the root. Otherwise the path is relative to the current context. - If the object is not found, return 'default' argument. + If the object is not found, return *default* argument. """ class ITraversalAPI(Interface): - """Common API functions to ease traversal computations + """ + Common API functions to ease traversal computations. + + This is provided by :mod:`zope.traversing.api`. """ def joinPath(path, *args): - """Join the given relative paths to the given path. + """Join the given relative paths to the given *path*. - Returns a unicode path. + Returns a text (`unicode`) path. The path should be well-formed, and not end in a '/' unless it is the root path. It can be either a string (ascii only) or unicode. The positional arguments are relative paths to be added to the - path as new path segments. The path may be absolute or relative. + path as new path segments. The path may be absolute or relative. A segment may not start with a '/' because that would be confused with an absolute path. A segment may not end with a '/' because we do not allow '/' at the end of relative paths. A segment may - consist of . or .. to mean "the same place", or "the parent path" + consist of '.' or '..' to mean "the same place", or "the parent path" respectively. A '.' should be removed and a '..' should cause the - segment to the left to be removed. joinPath('/', '..') should + segment to the left to be removed. ``joinPath('/', '..')`` should raise an exception. """ @@ -99,40 +102,37 @@ class ITraversalAPI(Interface): """ def traverse(object, path, default=None, request=None): - """Traverse 'path' relative to the given object. - - 'path' is a string with path segments separated by '/'. - - 'request' is passed in when traversing from presentation code. This - allows paths like @@foo to work. - - Raises LocationError if path cannot be found - - Note: calling traverse with a path argument taken from an untrusted - source, such as an HTTP request form variable, is a bad idea. - It could allow a maliciously constructed request to call - code unexpectedly. - Consider using traverseName instead. + """Traverse *path* relative to the given object. + + :param str path: a string with path segments separated by '/'. + :keyword request: Passed in when traversing from + presentation code. This allows paths like "@@foo" to work. + :raises zope.location.interfaces.LocationError: if *path* cannot be found + + .. note:: Calling `traverse` with a path argument taken from an + untrusted source, such as an HTTP request form variable, + is a bad idea. It could allow a maliciously constructed + request to call code unexpectedly. Consider using + `traverseName` instead. """ def traverseName(obj, name, default=None, traversable=None, request=None): - """Traverse a single step 'name' relative to the given object. - - 'name' must be a string. '.' and '..' are treated specially, as well as - names starting with '@' or '+'. Otherwise 'name' will be treated as a - single path segment. + """Traverse a single step *name* relative to the given object. - You can explicitly pass in an ITraversable as the - 'traversable' argument. If you do not, the given object will - be adapted to ITraversable. + *name* must be a string. '.' and '..' are treated specially, + as well as names starting with '@' or '+'. Otherwise *name* + will be treated as a single path segment. - 'request' is passed in when traversing from presentation code. This - allows paths like @@foo to work. + You can explicitly pass in an `ITraversable` as the + *traversable* argument. If you do not, the given object will + be adapted to `ITraversable`. - Raises LocationError if path cannot be found and 'default' was - not provided. + *request* is passed in when traversing from presentation code. + This allows paths like "@@foo" to work. + :raises zope.location.interfaces.LocationError: if *path* cannot + be found and *default* was not provided. """ def getName(obj): @@ -142,25 +142,28 @@ class ITraversalAPI(Interface): def getParent(obj): """Returns the container the object was traversed via. - Returns None if the object is a containment root. - Raises TypeError if the object doesn't have enough context to get the - parent. + Returns `None` if the object is a containment root. + + :raises TypeError: if the object doesn't have enough context + to get the parent. """ def getParents(obj): - """Returns a list starting with the given object's parent followed by - each of its parents. + """ + Returns a list starting with the given object's parent + followed by each of its parents. - Raises a TypeError if the context doesn't go all the way down to - a containment root. + :raises TypeError: if the context doesn't go all the way down + to a containment root. """ def canonicalPath(path_or_object): - """Returns a canonical absolute unicode path for the path or object. + """ + Returns a canonical absolute unicode path for the path or object. Resolves segments that are '.' or '..'. - Raises ValueError if a badly formed path is given. + :raises ValueError: if a badly formed path is given. """ @@ -182,7 +185,11 @@ class IBeforeTraverseEvent(IObjectEvent): @implementer(IBeforeTraverseEvent) class BeforeTraverseEvent(ObjectEvent): - """An event which gets sent on publication traverse""" + """ + An event which gets sent on publication traverse. + + Default implementation of `IBeforeTraverseEvent`. + """ def __init__(self, ob, request): diff --git a/src/zope/traversing/namespace.py b/src/zope/traversing/namespace.py index 7ef07a4..ef296e3 100644 --- a/src/zope/traversing/namespace.py +++ b/src/zope/traversing/namespace.py @@ -11,7 +11,48 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## -"""URL Namespace Implementations +""" +URL Namespace Implementations + +A URL Namespace is usually a path segment that looks like ``++ns++name``. +(It can also look like ``@@name``, which is a +shortcut for ``++view++name``. See :func:`nsParse` for details.) + +``ns`` is the name of the namespace (a named, registered adapter that +implements `ITraversable`) and ``name`` is the name to traverse to in +that namespace. + +The function :func:`namespaceLookup` handles this process. + +If you configure this package by loading its ``configure.zcml`` using +:mod:`zope.configuration.xmlconfig`, several namespaces are registered. They +are registered both as single adapters for any object, and as +multi-adapters (views) for any object together with a +`zope.publisher.interfaces.IRequest`. Those namespaces are: + +etc + Implemented in `etc` +attribute + Implemented in `attr` +adapter + Implemented in `adapter` +item + Implemented in `item` +acquire + Implemented in `acquire` +view + Implemented in `view` +resource + Implemented in `resource` +lang + Implemented in `lang` +skin + Implemented in `skin` +vh + Implemented in `vh` +debug + Implemented in `debug` (only if the ZCML feature ``devmode`` is enabled) + and only registered as a multi-adapter. """ __docformat__ = 'restructuredtext' @@ -41,68 +82,71 @@ class ExcessiveDepth(LocationError): def namespaceLookup(ns, name, object, request=None): - """Lookup a value from a namespace + """ + Lookup a value from a namespace. - We look up a value using a view or an adapter, depending on - whether a request is passed. + We look up a value by getting an adapter from the *object* to + :class:`~zope.traversing.interfaces.ITraversable` named *ns*. If + the *request* is passed, we get a multi-adapter on the *object* + and *request* (sometimes this is called a "view"). - Let's start with adapter-based transersal: + Let's start with adapter-based traversal:: - >>> class I(zope.interface.Interface): - ... 'Test interface' - >>> @zope.interface.implementer(I) - ... class C(object): - ... pass + >>> class I(zope.interface.Interface): + ... 'Test interface' + >>> @zope.interface.implementer(I) + ... class C(object): + ... pass - We'll register a simple testing adapter: + We'll register a simple testing adapter:: - >>> class Adapter(object): - ... def __init__(self, context): - ... self.context = context - ... def traverse(self, name, remaining): - ... return name+'42' + >>> class Adapter(object): + ... def __init__(self, context): + ... self.context = context + ... def traverse(self, name, remaining): + ... return name+'42' - >>> zope.component.provideAdapter(Adapter, (I,), ITraversable, 'foo') + >>> zope.component.provideAdapter(Adapter, (I,), ITraversable, 'foo') Then given an object, we can traverse it with a - namespace-qualified name: + namespace-qualified name:: - >>> namespaceLookup('foo', 'bar', C()) - 'bar42' + >>> namespaceLookup('foo', 'bar', C()) + 'bar42' - If we give an invalid namespace, we'll get a not found error: + If we give an invalid namespace, we'll get a not found error:: - >>> namespaceLookup('fiz', 'bar', C()) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - LocationError: (<zope.traversing.namespace.C object at 0x...>, '++fiz++bar') + >>> namespaceLookup('fiz', 'bar', C()) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + LocationError: (<zope.traversing.namespace.C object at 0x...>, '++fiz++bar') - We'll get the same thing if we provide a request: + We'll get the same thing if we provide a request:: - >>> from zope.publisher.browser import TestRequest - >>> request = TestRequest() - >>> namespaceLookup('foo', 'bar', C(), request) # doctest: +ELLIPSIS - Traceback (most recent call last): - ... - LocationError: (<zope.traversing.namespace.C object at 0x...>, '++foo++bar') + >>> from zope.publisher.browser import TestRequest + >>> request = TestRequest() + >>> namespaceLookup('foo', 'bar', C(), request) # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + LocationError: (<zope.traversing.namespace.C object at 0x...>, '++foo++bar') - We need to provide a view: + We need to provide a view:: - >>> class View(object): - ... def __init__(self, context, request): - ... pass - ... def traverse(self, name, remaining): - ... return name+'fromview' - >>> from zope.traversing.testing import browserView - >>> browserView(I, 'foo', View, providing=ITraversable) + >>> class View(object): + ... def __init__(self, context, request): + ... pass + ... def traverse(self, name, remaining): + ... return name+'fromview' + >>> from zope.traversing.testing import browserView + >>> browserView(I, 'foo', View, providing=ITraversable) - >>> namespaceLookup('foo', 'bar', C(), request) - 'barfromview' + >>> namespaceLookup('foo', 'bar', C(), request) + 'barfromview' - Clean up: + Clean up:: - >>> from zope.testing.cleanup import cleanUp - >>> cleanUp() + >>> from zope.testing.cleanup import cleanUp + >>> cleanUp() """ if request is not None: traverser = zope.component.queryMultiAdapter((object, request), @@ -120,32 +164,33 @@ namespace_pattern = re.compile('[+][+]([a-zA-Z0-9_]+)[+][+]') def nsParse(name): - """Parse a namespace-qualified name into a namespace name and a - name. Returns the namespace name and a name. - - A namespace-qualified name is usually of the form ++ns++name, as in: + """ + Parse a namespace-qualified name into a namespace name and a name. + Returns the namespace name and a name. - >>> nsParse('++acquire++foo') - ('acquire', 'foo') + A namespace-qualified name is usually of the form ++ns++name, as + in:: - The part inside the +s must be an identifier, so: + >>> nsParse('++acquire++foo') + ('acquire', 'foo') - >>> nsParse('++hello world++foo') - ('', '++hello world++foo') - >>> nsParse('+++acquire+++foo') - ('', '+++acquire+++foo') + The part inside the +s must be an identifier, so:: - But it may also be a @@foo, which implies the view namespace: + >>> nsParse('++hello world++foo') + ('', '++hello world++foo') + >>> nsParse('+++acquire+++foo') + ('', '+++acquire+++foo') - >>> nsParse('@@foo') - ('view', 'foo') + But it may also be a @@foo, which implies the view namespace:: - >>> nsParse('@@@foo') - ('view', '@foo') + >>> nsParse('@@foo') + ('view', 'foo') - >>> nsParse('@foo') - ('', '@foo') + >>> nsParse('@@@foo') + ('view', '@foo') + >>> nsParse('@foo') + ('', '@foo') """ ns = '' if name.startswith('@@'): @@ -160,14 +205,14 @@ def nsParse(name): return ns, name -def getResource(site, name, request): - resource = queryResource(site, name, request) +def getResource(context, name, request): + resource = queryResource(context, name, request) if resource is None: - raise LocationError(site, name) + raise LocationError(context, name) return resource -def queryResource(site, name, request, default=None): +def queryResource(context, name, request, default=None): resource = zope.component.queryAdapter(request, name=name) if resource is None: return default @@ -176,7 +221,7 @@ def queryResource(site, name, request, default=None): # resource to do this. We still return the proxied resource. r = removeSecurityProxy(resource) - r.__parent__ = site + r.__parent__ = context r.__name__ = name return resource @@ -188,28 +233,28 @@ def queryResource(site, name, request, default=None): class SimpleHandler(object): def __init__(self, context, request=None): - """Simple handlers can be used as adapters or views - - They ignore their second constructor arg and store the first - one in their context attr: - - >>> SimpleHandler(42).context - 42 - - >>> SimpleHandler(42, 43).context - 42 + """ + It ignores its second constructor arg and stores the first + one in its ``context`` attr. """ self.context = context class acquire(SimpleHandler): - """Traversal adapter for the acquire namespace + """ + Traversal adapter for the ``acquire`` namespace. + + This namespace tries to traverse to the given *name* + starting with the context object. If it cannot be found, + it proceeds to look at each ``__parent__`` all the way + up the tree until it is found. """ def traverse(self, name, remaining): - """Acquire a name + """ + Acquire a name - Let's set up some example data: + Let's set up some example data:: >>> @zope.interface.implementer(ITraversable) ... class testcontent(object): @@ -272,6 +317,12 @@ class acquire(SimpleHandler): class attr(SimpleHandler): + """ + Traversal adapter for the ``attribute`` namespace. + + This namespace simply looks for an attribute of the given + *name*. + """ def traverse(self, name, ignored): """Attribute traversal adapter @@ -288,6 +339,12 @@ class attr(SimpleHandler): class item(SimpleHandler): + """ + Traversal adapter for the ``item`` namespace. + + This namespace simply uses ``__getitem__`` to find a + value of the given *name*. + """ def traverse(self, name, ignored): """Item traversal adapter @@ -303,6 +360,17 @@ class item(SimpleHandler): class etc(SimpleHandler): + """ + Traversal adapter for the ``etc`` namespace. + + This namespace provides for a layer of indirection. The given + **name** is used to find a utility of that name that implements + `zope.traversing.interfaces.IEtcNamespace`. + + As a special case, if there is no such utility, and the name is + "site", then we will attempt to call a method named ``getSiteManager`` + on the *context* object. + """ def traverse(self, name, ignored): utility = zope.component.queryUtility(IEtcNamespace, name) @@ -327,6 +395,15 @@ class etc(SimpleHandler): @zope.interface.implementer(ITraversable) class view(object): + """ + Traversal adapter for the ``view`` (``@@``) namespace. + + This looks for the default multi-adapter from the *context* and + *request* of the given *name*. + + :raises zope.location.interfaces.LocationError: If no such + adapter can be found. + """ def __init__(self, context, request): self.context = context @@ -342,6 +419,14 @@ class view(object): class resource(view): + """ + Traversal adapter for the ``resource`` namespace. + + Resources are default adapters of the given *name* for the + *request* (**not** the *context*). The returned object will have + its ``__parent__`` set to the *context* and its ``__name__`` will + match the *name* we traversed. + """ def traverse(self, name, ignored): # The context is important here, since it becomes the parent of the @@ -350,6 +435,17 @@ class resource(view): class lang(view): + """ + Traversal adapter for the ``lang`` namespace. + + Traversing to *name* means to adapt the request to + :class:`zope.i18n.interfaces.IModifiableUserPreferredLanguages` + and set the *name* as the only preferred language. + + This needs the *request* to support + :class:`zope.publisher.interfaces.http.IVirtualHostRequest` because + it shifts the language name to the application. + """ def traverse(self, name, ignored): self.request.shiftNameToApplication() @@ -359,6 +455,18 @@ class lang(view): class skin(view): + """ + Traversal adapter for the ``skin`` namespace. + + Traversing to *name* looks for the + :class:`zope.publisher.interfaces.browser.IBrowserSkinType` + utility having the given name, and then applies it to the + *request* with :func:`.applySkin`. + + This needs the *request* to support + :class:`zope.publisher.interfaces.http.IVirtualHostRequest` + because it shifts the skin name to the application. + """ def traverse(self, name, ignored): self.request.shiftNameToApplication() @@ -371,6 +479,16 @@ class skin(view): class vh(view): + """ + Traversal adapter for the ``vh`` namespace. + + Traversing to *name*, which must be of the form + ``protocol:host:port`` causes a call to + :meth:`zope.publisher.interfaces.http.IVirtualHostRequest.setApplicationServer`. + Segments in the request's traversal stack up to a prior ``++`` are + collected and become the application names given to + :meth:`zope.publisher.interfaces.http.IVirtualHostRequest.setVirtualHostRoot`. + """ def traverse(self, name, ignored): @@ -410,13 +528,17 @@ class vh(view): class adapter(SimpleHandler): + """ + Traversal adapter for the ``adapter`` namespace. - def traverse(self, name, ignored): - """Adapter traversal adapter + This adapter provides traversal to named adapters for the + *context* registered to provide + `zope.traversing.interfaces.IPathAdapter`. + """"" - This adapter provides traversal to named adapters registered - to provide IPathAdapter. + def traverse(self, name, ignored): + """ To demonstrate this, we need to register some adapters: >>> def adapter1(ob): @@ -454,15 +576,18 @@ class adapter(SimpleHandler): class debug(view): + """ + Traversal adapter for the ``debug`` namespace. - enable_debug = __debug__ + This adapter allows debugging flags to be set in the request. - def traverse(self, name, ignored): - """Debug traversal adapter + .. seealso:: :class:`zope.publisher.interfaces.IDebugFlags` + """ - This adapter allows debugging flags to be set in the request. - See IDebugFlags. + enable_debug = __debug__ + def traverse(self, name, ignored): + """ Setup for demonstration: >>> from zope.publisher.browser import TestRequest @@ -470,7 +595,7 @@ class debug(view): >>> ob = object() >>> adapter = debug(ob, request) - in debug mode, ++debug++source enables source annotations + in debug mode, ``++debug++source`` enables source annotations >>> request.debug.sourceAnnotations False @@ -479,7 +604,7 @@ class debug(view): >>> request.debug.sourceAnnotations True - ++debug++tal enables TAL markup in output + ``++debug++tal`` enables TAL markup in output >>> request.debug.showTAL False @@ -488,7 +613,7 @@ class debug(view): >>> request.debug.showTAL True - ++debug++errors enables tracebacks (by switching to debug skin) + ``++debug++errors`` enables tracebacks (by switching to debug skin) >>> from zope.publisher.interfaces.browser import IBrowserRequest diff --git a/src/zope/traversing/publicationtraverse.py b/src/zope/traversing/publicationtraverse.py index 603d0c6..efa4046 100644 --- a/src/zope/traversing/publicationtraverse.py +++ b/src/zope/traversing/publicationtraverse.py @@ -30,16 +30,19 @@ class PublicationTraverser(object): """Traversal used for publication. The significant differences from - zope.traversing.adapters.traversePathElement() are: + `zope.traversing.adapters.traversePathElement` are: - - Instead of adapting each traversed object to ITraversable, this - version multi-adapts (ob, request) to IPublishTraverse. + - Instead of adapting each traversed object to ITraversable, + this version multi-adapts (ob, request) to + `zope.publisher.interfaces.IPublishTraverse`. - - This version wraps a security proxy around each traversed object. + - This version wraps a security proxy around each traversed + object. - - This version raises NotFound rather than LocationError. + - This version raises `zope.publisher.interfaces.NotFound` + rather than `zope.location.interfaces.LocationError`. - - This version has a method, traverseRelativeURL(), that + - This version has a method, :meth:`traverseRelativeURL`, that supports "browserDefault" traversal. """ def proxy(self, ob): @@ -126,6 +129,9 @@ PublicationTraverse = PublicationTraverser class PublicationTraverserWithoutProxy(PublicationTraverse): + """ + A `PublicationTraverse` that does not add security proxies. + """ def proxy(self, ob): return ob diff --git a/src/zope/traversing/tests/test_namespacetrversal.py b/src/zope/traversing/tests/test_namespacetrversal.py index 0fdd4db..cfd2e31 100644 --- a/src/zope/traversing/tests/test_namespacetrversal.py +++ b/src/zope/traversing/tests/test_namespacetrversal.py @@ -27,6 +27,15 @@ from zope.component.testing import setUp, tearDown from zope.testing.renormalizing import RENormalizing +class TestSimpleHandler(unittest.TestCase): + + def test_constructor(self): + h = namespace.SimpleHandler(42) + self.assertEqual(h.context, 42) + + h = namespace.SimpleHandler(42, 43) + self.assertEqual(h.context, 42) + class TestFunctions(unittest.TestCase): def test_getResource_not_found(self): @@ -1,6 +1,6 @@ [tox] envlist = - py27,py34,py35,py36,pypy,pypy3,coverage + py27,py34,py35,py36,pypy,pypy3,coverage,docs [testenv] commands = @@ -18,3 +18,12 @@ commands = deps = {[testenv]deps} coverage + +[testenv:docs] +basepython = + python2.7 +commands = + sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html +deps = + {[testenv]deps} + .[docs] |