summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2017-11-08 11:51:03 -0600
committerJason Madden <jamadden@gmail.com>2017-11-08 11:51:03 -0600
commitfbc54516fcdaa1b516606e6ac2c6b569bd25ed94 (patch)
tree7192b316e31a25f7e0b6ddde522a79087fc7868e
parent7e3ebd8032aaa081637bc2c378416da607960584 (diff)
downloadzope-traversing-fbc54516fcdaa1b516606e6ac2c6b569bd25ed94.tar.gz
Publish docs using Sphinx.rtd-docs
Some doc formatting fixes and cleanups. Add quite a bit of docs about namespaces and the various ones we supply. Fixes #5
-rw-r--r--.gitignore1
-rw-r--r--CHANGES.rst2
-rw-r--r--README.rst11
-rw-r--r--doc-requirements.txt1
-rw-r--r--docs/adapters.rst5
-rw-r--r--docs/api.rst5
-rw-r--r--docs/browser/absoluteurl.rst5
-rw-r--r--docs/browser/interfaces.rst5
-rw-r--r--docs/changelog.rst1
-rw-r--r--docs/conf.py283
-rw-r--r--docs/index.rst35
-rw-r--r--docs/interfaces.rst5
-rw-r--r--docs/namespace.rst5
-rw-r--r--docs/publicationtraverse.rst5
-rw-r--r--setup.py4
-rw-r--r--src/zope/traversing/adapters.py46
-rw-r--r--src/zope/traversing/api.py91
-rw-r--r--src/zope/traversing/browser/absoluteurl.py16
-rw-r--r--src/zope/traversing/browser/interfaces.py18
-rw-r--r--src/zope/traversing/interfaces.py103
-rw-r--r--src/zope/traversing/namespace.py311
-rw-r--r--src/zope/traversing/publicationtraverse.py18
-rw-r--r--src/zope/traversing/tests/test_namespacetrversal.py9
-rw-r--r--tox.ini11
24 files changed, 763 insertions, 233 deletions
diff --git a/.gitignore b/.gitignore
index f631ff5..9762e6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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)
diff --git a/README.rst b/README.rst
index 023985d..724c1a7 100644
--- a/README.rst
+++ b/README.rst
@@ -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
diff --git a/setup.py b/setup.py
index 5fffac5..ed864fb 100644
--- a/setup.py
+++ b/setup.py
@@ -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):
diff --git a/tox.ini b/tox.ini
index fef112b..df2a65d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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]