diff options
| author | Rob Ruana <rob@relentlessidiot.com> | 2014-01-19 13:40:06 -0500 |
|---|---|---|
| committer | Rob Ruana <rob@relentlessidiot.com> | 2014-01-19 13:40:06 -0500 |
| commit | 7f3ff63caae51ddca2a6247295c885a0fda7cc8d (patch) | |
| tree | 1d269fb42c680f8664e6ba66696ca9e5db508285 | |
| parent | cd7658dde7f32714f4e6013cd221a9ea6e88bb82 (diff) | |
| parent | a8b06aa17015396b9bd5accb5cca4644f69f307d (diff) | |
| download | sphinx-7f3ff63caae51ddca2a6247295c885a0fda7cc8d.tar.gz | |
Merged birkenfeld/sphinx into default
| -rw-r--r-- | AUTHORS | 1 | ||||
| -rw-r--r-- | doc/conf.py | 4 | ||||
| -rw-r--r-- | doc/ext/example_google.py | 223 | ||||
| -rw-r--r-- | doc/ext/example_google.rst | 15 | ||||
| -rw-r--r-- | doc/ext/example_numpy.py | 272 | ||||
| -rw-r--r-- | doc/ext/example_numpy.rst | 15 | ||||
| -rw-r--r-- | doc/ext/napoleon.rst | 365 | ||||
| -rw-r--r-- | doc/extensions.rst | 1 | ||||
| -rw-r--r-- | sphinx/ext/napoleon/__init__.py | 371 | ||||
| -rw-r--r-- | sphinx/ext/napoleon/docstring.py | 714 | ||||
| -rw-r--r-- | sphinx/ext/napoleon/iterators.py | 244 | ||||
| -rw-r--r-- | tests/root/conf.py | 2 | ||||
| -rw-r--r-- | tests/test_napoleon.py | 190 | ||||
| -rw-r--r-- | tests/test_napoleon_docstring.py | 259 | ||||
| -rw-r--r-- | tests/test_napoleon_iterators.py | 346 | ||||
| -rw-r--r-- | tox.ini | 1 |
16 files changed, 3021 insertions, 2 deletions
@@ -32,6 +32,7 @@ Other contributors, listed alphabetically, are: * Christopher Perkins -- autosummary integration * Benjamin Peterson -- unittests * T. Powers -- HTML output improvements +* Rob Ruana -- napoleon extension * Stefan Seefeld -- toctree improvements * Shibukawa Yoshiki -- pluggable search API and Japanese search * Antonio Valentino -- qthelp builder diff --git a/doc/conf.py b/doc/conf.py index 08149fba..b6515a38 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -5,8 +5,10 @@ import re import sphinx + extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', - 'sphinx.ext.autosummary', 'sphinx.ext.extlinks'] + 'sphinx.ext.autosummary', 'sphinx.ext.extlinks', + 'sphinx.ext.napoleon'] master_doc = 'contents' templates_path = ['_templates'] diff --git a/doc/ext/example_google.py b/doc/ext/example_google.py new file mode 100644 index 00000000..c94dcdf1 --- /dev/null +++ b/doc/ext/example_google.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +"""Example Google style docstrings. + +This module demonstrates documentation as specified by the `Google Python +Style Guide`_. Docstrings may extend over multiple lines. Sections are created +with a section header and a colon followed by a block of indented text. + +Example: + Examples can be given using either the ``Example`` or ``Examples`` + sections. Sections support any reStructuredText formatting, including + literal blocks:: + + $ python example_google.py + +Section breaks are created by simply resuming unindented text. Section breaks +are also implicitly created anytime a new section starts. + +Attributes: + module_level_variable (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + +.. _Google Python Style Guide: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html + +""" + +module_level_variable = 12345 + + +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Args`` section. The name + of each parameter is required. The type and description of each parameter + is optional, but should be included if not obvious. + + If the parameter itself is optional, it should be noted by adding + ", optional" to the type. If \*args or \*\*kwargs are accepted, they + should be listed as \*args and \*\*kwargs. + + The format for a parameter is:: + + name (type): description + The description may span multiple lines. Following + lines should be indented. + + Multiple paragraphs are supported in parameter + descriptions. + + Args: + param1 (int): The first parameter. + param2 (str, optional): The second parameter. Defaults to None. + Second line of description should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + bool: True if successful, False otherwise. + + The return type is optional and may be specified at the beginning of + the ``Returns`` section followed by a colon. + + The ``Returns`` section may span multiple lines and paragraphs. + Following lines should be indented to match the first line. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises: + AttributeError: The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError: If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError('param1 may not be equal to param2') + return True + + +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Args: + n (int): The upper limit of the range to generate, from 0 to `n` - 1 + + Yields: + int: The next number in the range of 0 to `n` - 1 + + Examples: + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print [i for i in example_generator(4)] + [0, 1, 2, 3] + + """ + for i in range(n): + yield i + + +class ExampleError(Exception): + """Exceptions are documented in the same way as classes. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + msg (str): Human readable string describing the exception. + code (int, optional): Error code, defaults to 2. + + Attributes: + msg (str): Human readable string describing the exception. + code (int): Exception error code. + + """ + def __init__(self, msg, code=2): + self.msg = msg + self.code = code + + +class ExampleClass(object): + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they should be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. + + Attributes: + attr1 (str): Description of `attr1`. + attr2 (list of str): Description of `attr2`. + attr3 (int): Description of `attr3`. + + """ + def __init__(self, param1, param2, param3=0): + """Example of docstring on the __init__ method. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1 (str): Description of `param1`. + param2 (list of str): Description of `param2`. Multiple + lines are supported. + param3 (int, optional): Description of `param3`, defaults to 0. + + """ + self.attr1 = param1 + self.attr2 = param2 + self.attr3 = param3 + + def example_method(self, param1, param2): + """Class methods are similar to regular functions. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + True if successful, False otherwise. + + """ + return True + + def __special__(self): + """By default special members with docstrings are included. + + Special members are any methods or attributes that start with and + end with a double underscore. Any special member with a docstring + will be included in the output. + + This behavior can be disabled by changing the following setting in + Sphinx's conf.py:: + + napoleon_include_special_with_doc = False + + """ + pass + + def __special_without_docstring__(self): + pass + + def _private(self): + """By default private members are not included. + + Private members are any methods or attributes that start with an + underscore and are *not* special. By default they are not included + in the output. + + This behavior can be changed such that private members *are* included + by changing the following setting in Sphinx's conf.py:: + + napoleon_include_private_with_doc = True + + """ + pass + + def _private_without_docstring(self): + pass diff --git a/doc/ext/example_google.rst b/doc/ext/example_google.rst new file mode 100644 index 00000000..06508082 --- /dev/null +++ b/doc/ext/example_google.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _example_google: + +Example Google Style Python Docstrings +====================================== + +.. seealso:: + + :ref:`example_numpy` + +Download: :download:`example_google.py <example_google.py>` + +.. literalinclude:: example_google.py + :language: python diff --git a/doc/ext/example_numpy.py b/doc/ext/example_numpy.py new file mode 100644 index 00000000..df1d20e6 --- /dev/null +++ b/doc/ext/example_numpy.py @@ -0,0 +1,272 @@ +# -*- coding: utf-8 -*- +"""Example NumPy style docstrings. + +This module demonstrates documentation as specified by the `NumPy +Documentation HOWTO`_. Docstrings may extend over multiple lines. Sections +are created with a section header followed by an underline of equal length. + +Example +------- +Examples can be given using either the ``Example`` or ``Examples`` +sections. Sections support any reStructuredText formatting, including +literal blocks:: + + $ python example_numpy.py + + +Section breaks are created with two blank lines. Section breaks are also +implicitly created anytime a new section starts. Section bodies *may* be +indented: + +Notes +----- + This is an example of an indented section. It's like any other section, + but the body is indented to help it stand out from surrounding text. + +If a section is indented, then a section break is created simply by +resuming unindented text. + +Attributes +---------- +module_level_variable : int + Module level variables may be documented in either the ``Attributes`` + section of the module docstring, or in an inline docstring immediately + following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + +.. _NumPy Documentation HOWTO: + https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt + +""" + +module_level_variable = 12345 + + +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Parameters`` section. + The name of each parameter is required. The type and description of each + parameter is optional, but should be included if not obvious. + + If the parameter itself is optional, it should be noted by adding + ", optional" to the type. If \*args or \*\*kwargs are accepted, they + should be listed as \*args and \*\*kwargs. + + The format for a parameter is:: + + name : type + description + + The description may span multiple lines. Following lines + should be indented to match the first line of the description. + + Multiple paragraphs are supported in parameter + descriptions. + + Parameters + ---------- + param1 : int + The first parameter. + param2 : str, optional + The second parameter, defaults to None. + *args + Variable length argument list. + **kwargs + Arbitrary keyword arguments. + + Returns + ------- + bool + True if successful, False otherwise. + + The return type is not optional. The ``Returns`` section may span + multiple lines and paragraphs. Following lines should be indented to + match the first line of the description. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises + ------ + AttributeError + The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError + If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError('param1 may not be equal to param2') + return True + + +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Parameters + ---------- + n : int + The upper limit of the range to generate, from 0 to `n` - 1 + + Yields + ------ + int + The next number in the range of 0 to `n` - 1 + + Examples + -------- + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print [i for i in example_generator(4)] + [0, 1, 2, 3] + + """ + for i in range(n): + yield i + + +class ExampleError(Exception): + """Exceptions are documented in the same way as classes. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note + ---- + Do not include the `self` parameter in the ``Parameters`` section. + + Parameters + ---------- + msg : str + Human readable string describing the exception. + code : int, optional + Error code, defaults to 2. + + Attributes + ---------- + msg : str + Human readable string describing the exception. + code : int + Exception error code. + + """ + def __init__(self, msg, code=2): + self.msg = msg + self.code = code + + +class ExampleClass(object): + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they should be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Parameters`` section. + + Attributes + ---------- + attr1 : str + Description of `attr1`. + attr2 : list of str + Description of `attr2`. + attr3 : int + Description of `attr3`. + + """ + def __init__(self, param1, param2, param3=0): + """Example of docstring on the __init__ method. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note + ---- + Do not include the `self` parameter in the ``Parameters`` section. + + Parameters + ---------- + param1 : str + Description of `param1`. + param2 : list of str + Description of `param2`. Multiple + lines are supported. + param3 : int, optional + Description of `param3`, defaults to 0. + + """ + self.attr1 = param1 + self.attr2 = param2 + self.attr3 = param3 + + def example_method(self, param1, param2): + """Class methods are similar to regular functions. + + Note + ---- + Do not include the `self` parameter in the ``Parameters`` section. + + Parameters + ---------- + param1 + The first parameter. + param2 + The second parameter. + + Returns + ------- + bool + True if successful, False otherwise. + + """ + return True + + def __special__(self): + """By default special members with docstrings are included. + + Special members are any methods or attributes that start with and + end with a double underscore. Any special member with a docstring + will be included in the output. + + This behavior can be disabled by changing the following setting in + Sphinx's conf.py:: + + napoleon_include_special_with_doc = False + + """ + pass + + def __special_without_docstring__(self): + pass + + def _private(self): + """By default private members are not included. + + Private members are any methods or attributes that start with an + underscore and are *not* special. By default they are not included + in the output. + + This behavior can be changed such that private members *are* included + by changing the following setting in Sphinx's conf.py:: + + napoleon_include_private_with_doc = True + + """ + pass + + def _private_without_docstring(self): + pass diff --git a/doc/ext/example_numpy.rst b/doc/ext/example_numpy.rst new file mode 100644 index 00000000..a3b41613 --- /dev/null +++ b/doc/ext/example_numpy.rst @@ -0,0 +1,15 @@ +:orphan: + +.. _example_numpy: + +Example NumPy Style Python Docstrings +====================================== + +.. seealso:: + + :ref:`example_google` + +Download: :download:`example_numpy.py <example_numpy.py>` + +.. literalinclude:: example_numpy.py + :language: python diff --git a/doc/ext/napoleon.rst b/doc/ext/napoleon.rst new file mode 100644 index 00000000..fde23c7b --- /dev/null +++ b/doc/ext/napoleon.rst @@ -0,0 +1,365 @@ +:mod:`sphinx.ext.napoleon` -- Support for NumPy and Google style docstrings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. module:: sphinx.ext.napoleon + :synopsis: Support for NumPy and Google style docstrings + +.. moduleauthor:: Rob Ruana + +.. versionadded:: 1.3 + +Napoleon - *Marching toward legible docstrings* +=============================================== + +Are you tired of writing docstrings that look like this:: + + :param path: The path of the file to wrap + :type path: str + :param field_storage: The :class:`FileStorage` instance to wrap + :type field_storage: FileStorage + :param temporary: Whether or not to delete the file when the File + instance is destructed + :type temporary: bool + :returns: A buffered writable file descriptor + :rtype: BufferedFileStorage + +`ReStructuredText`_ is great, but it creates visually dense, hard to read +`docstrings`_. Compare the jumble above to the same thing rewritten +according to the `Google Python Style Guide`_:: + + Args: + path (str): The path of the file to wrap + field_storage (FileStorage): The :class:`FileStorage` instance to wrap + temporary (bool): Whether or not to delete the file when the File + instance is destructed + + Returns: + BufferedFileStorage: A buffered writable file descriptor + +Much more legible, no? + +Napoleon is a `Sphinx extension`_ that allows you to write readable API +documentation in your source code. Napoleon understands both `NumPy`_ and +`Google`_ style docstrings - the style recommended by `Khan Academy`_. + +.. _ReStructuredText: http://docutils.sourceforge.net/rst.html +.. _docstrings: http://www.python.org/dev/peps/pep-0287/ +.. _Google Python Style Guide: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html +.. _Sphinx extension: http://sphinx-doc.org/extensions.html +.. _Google: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html#Comments +.. _NumPy: + https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt +.. _Khan Academy: + https://sites.google.com/a/khanacademy.org/forge/for-developers/styleguide/python#TOC-Docstrings + +Getting Started +--------------- + +1. After `setting up Sphinx`_ to build your docs, enable napoleon in the + Sphinx `conf.py` file:: + + # conf.py + + # Add autodoc and napoleon to the extensions list + extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] + +2. Use `sphinx-apidoc` to build your API documentation:: + + $ sphinx-apidoc -f -o docs/source projectdir + +.. _setting up Sphinx: http://sphinx-doc.org/tutorial.html + +Docstrings +---------- + +Napoleon interprets every docstring that `Sphinx autodoc`_ can find, +including docstrings on: ``modules``, ``classes``, ``attributes``, +``methods``, ``functions``, and ``variables``. Inside each docstring, +specially formatted `Sections`_ are parsed and converted to +reStructuredText. + +All standard reStructuredText formatting still works as expected. + +.. _Sphinx autodoc: http://sphinx-doc.org/ext/autodoc.html + + +.. _Sections: + +Docstring Sections +------------------ + +All of the following section headers are supported: + + * ``Args`` *(alias of Parameters)* + * ``Arguments`` *(alias of Parameters)* + * ``Attributes`` + * ``Example`` + * ``Examples`` + * ``Keyword Args`` *(alias of Keyword Arguments)* + * ``Keyword Arguments`` + * ``Methods`` + * ``Note`` + * ``Notes`` + * ``Other Parameters`` + * ``Parameters`` + * ``Return`` *(alias of Returns)* + * ``Returns`` + * ``Raises`` + * ``References`` + * ``See Also`` + * ``Warning`` + * ``Warnings`` *(alias of Warning)* + * ``Warns`` + * ``Yields`` + +Google vs NumPy +--------------- + +Napoleon supports two styles of docstrings: `Google`_ and `NumPy`_. The +main difference between the two styles is that Google uses indention to +separate sections, whereas NumPy uses underlines. + +Google style:: + + def func(arg1, arg2): + """Summary line. + + Extended description of function. + + Args: + arg1 (int): Description of arg1 + arg2 (str): Description of arg2 + + Returns: + bool: Description of return value + + """ + return True + +NumPy style:: + + def func(arg1, arg2): + """Summary line. + + Extended description of function. + + Parameters + ---------- + arg1 : int + Description of arg1 + arg2 : str + Description of arg2 + + Returns + ------- + bool + Description of return value + + """ + return True + +NumPy style tends to require more vertical space, whereas Google style +tends to use more horizontal space. Google style tends to be easier to +read for short and simple docstrings, whereas NumPy style tends be easier +to read for long and in-depth docstrings. + +The `Khan Academy`_ recommends using Google style. + +The choice between styles is largely aesthetic, but the two styles should +not be mixed. Choose one style for your project and be consistent with it. + +.. seealso:: + + For complete examples: + + * :ref:`example_google` + * :ref:`example_numpy` + + +Configuration +============= + +Listed below are all the settings used by napoleon and their default +values. These settings can be changed in the Sphinx `conf.py` file. Make +sure that both "sphinx.ext.autodoc" and "sphinx.ext.napoleon" are +enabled in `conf.py`:: + + # conf.py + + # Add any Sphinx extension module names here, as strings + extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] + + # Napoleon settings + napoleon_google_docstring = True + napoleon_numpy_docstring = True + napoleon_include_private_with_doc = False + napoleon_include_special_with_doc = True + napoleon_use_admonition_for_examples = False + napoleon_use_admonition_for_notes = False + napoleon_use_admonition_for_references = False + napoleon_use_ivar = False + napoleon_use_param = False + napoleon_use_rtype = False + +.. _Google style: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html +.. _NumPy style: + https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt + + + +**napoleon_google_docstring** : bool, defaults to True + True to parse `Google style`_ docstrings. False to disable support + for Google style docstrings. + +**napoleon_numpy_docstring** : bool, defaults to True + True to parse `NumPy style`_ docstrings. False to disable support + for NumPy style docstrings. + +**napoleon_include_private_with_doc** : bool, defaults to False + True to include private members (like ``_membername``) with docstrings + in the documentation. False to fall back to Sphinx's default behavior. + + **If True**:: + + def _included(self): + """ + This will be included in the docs because it has a docstring + """ + pass + + def _skipped(self): + # This will NOT be included in the docs + pass + +**napoleon_include_special_with_doc** : bool, defaults to True + True to include special members (like ``__membername__``) with + docstrings in the documentation. False to fall back to Sphinx's + default behavior. + + **If True**:: + + def __str__(self): + """ + This will be included in the docs because it has a docstring + """ + return unicode(self).encode('utf-8') + + def __unicode__(self): + # This will NOT be included in the docs + return unicode(self.__class__.__name__) + +**napoleon_use_admonition_for_examples** : bool, defaults to False + True to use the ``.. admonition::`` directive for the **Example** and + **Examples** sections. False to use the ``.. rubric::`` directive + instead. One may look better than the other depending on what HTML + theme is used. + + This `NumPy style`_ snippet will be converted as follows:: + + Example + ------- + This is just a quick example + + **If True**:: + + .. admonition:: Example + + This is just a quick example + + **If False**:: + + .. rubric:: Example + + This is just a quick example + +**napoleon_use_admonition_for_notes** : bool, defaults to False + True to use the ``.. admonition::`` directive for **Notes** sections. + False to use the ``.. rubric::`` directive instead. + + .. note:: The singular **Note** section will always be converted to a + ``.. note::`` directive. + + .. seealso:: + + :attr:`napoleon_use_admonition_for_examples` + +**napoleon_use_admonition_for_references** : bool, defaults to False + True to use the ``.. admonition::`` directive for **References** + sections. False to use the ``.. rubric::`` directive instead. + + .. seealso:: + + :attr:`napoleon_use_admonition_for_examples` + +**napoleon_use_ivar** : bool, defaults to False + True to use the ``:ivar:`` role for instance variables. False to use + the ``.. attribute::`` directive instead. + + This `NumPy style`_ snippet will be converted as follows:: + + Attributes + ---------- + attr1 : int + Description of `attr1` + + **If True**:: + + :ivar attr1: Description of `attr1` + :vartype attr1: int + + **If False**:: + + .. attribute:: attr1 + :annotation: int + + Description of `attr1` + +**napoleon_use_param** : bool, defaults to False + True to use a ``:param:`` role for each function parameter. False to + use a single ``:parameters:`` role for all the parameters. + + This `NumPy style`_ snippet will be converted as follows:: + + Parameters + ---------- + arg1 : str + Description of `arg1` + arg2 : int, optional + Description of `arg2`, defaults to 0 + + **If True**:: + + :param arg1: Description of `arg1` + :type arg1: str + :param arg2: Description of `arg2`, defaults to 0 + :type arg2: int, optional + + **If False**:: + + :parameters: * **arg1** (*str*) -- + Description of `arg1` + * **arg2** (*int, optional*) -- + Description of `arg2`, defaults to 0 + +**napoleon_use_rtype** : bool, defaults to False + True to use the ``:rtype:`` role for the return type. False to output + the return type inline with the description. + + This `NumPy style`_ snippet will be converted as follows:: + + Returns + ------- + bool + True if successful, False otherwise + + **If True**:: + + :returns: True if successful, False otherwise + :rtype: bool + + **If False**:: + + :returns: *bool* -- True if successful, False otherwise diff --git a/doc/extensions.rst b/doc/extensions.rst index 7597f281..5534a125 100644 --- a/doc/extensions.rst +++ b/doc/extensions.rst @@ -53,6 +53,7 @@ These extensions are built in and can be activated by respective entries in the ext/extlinks ext/viewcode ext/linkcode + ext/napoleon Third-party extensions diff --git a/sphinx/ext/napoleon/__init__.py b/sphinx/ext/napoleon/__init__.py new file mode 100644 index 00000000..168ac574 --- /dev/null +++ b/sphinx/ext/napoleon/__init__.py @@ -0,0 +1,371 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.napoleon + ~~~~~~~~~~~~~~~~~~~ + + Support for NumPy and Google style docstrings. + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import sys +from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring + + +class Config(object): + """Sphinx napoleon extension settings in `conf.py`. + + Listed below are all the settings used by napoleon and their default + values. These settings can be changed in the Sphinx `conf.py` file. Make + sure that both "sphinx.ext.autodoc" and "sphinx.ext.napoleon" are + enabled in `conf.py`:: + + # conf.py + + # Add any Sphinx extension module names here, as strings + extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon'] + + # Napoleon settings + napoleon_google_docstring = True + napoleon_numpy_docstring = True + napoleon_include_private_with_doc = False + napoleon_include_special_with_doc = True + napoleon_use_admonition_for_examples = False + napoleon_use_admonition_for_notes = False + napoleon_use_admonition_for_references = False + napoleon_use_ivar = False + napoleon_use_param = False + napoleon_use_rtype = False + + .. _Google style: + http://google-styleguide.googlecode.com/svn/trunk/pyguide.html + .. _NumPy style: + https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt + + + + **napoleon_google_docstring** : bool, defaults to True + True to parse `Google style`_ docstrings. False to disable support + for Google style docstrings. + + **napoleon_numpy_docstring** : bool, defaults to True + True to parse `NumPy style`_ docstrings. False to disable support + for NumPy style docstrings. + + **napoleon_include_private_with_doc** : bool, defaults to False + True to include private members (like ``_membername``) with docstrings + in the documentation. False to fall back to Sphinx's default behavior. + + **If True**:: + + def _included(self): + \"\"\" + This will be included in the docs because it has a docstring + \"\"\" + pass + + def _skipped(self): + # This will NOT be included in the docs + pass + + **napoleon_include_special_with_doc** : bool, defaults to True + True to include special members (like ``__membername__``) with + docstrings in the documentation. False to fall back to Sphinx's + default behavior. + + **If True**:: + + def __str__(self): + \"\"\" + This will be included in the docs because it has a docstring + \"\"\" + return unicode(self).encode('utf-8') + + def __unicode__(self): + # This will NOT be included in the docs + return unicode(self.__class__.__name__) + + **napoleon_use_admonition_for_examples** : bool, defaults to False + True to use the ``.. admonition::`` directive for the **Example** and + **Examples** sections. False to use the ``.. rubric::`` directive + instead. One may look better than the other depending on what HTML + theme is used. + + This `NumPy style`_ snippet will be converted as follows:: + + Example + ------- + This is just a quick example + + **If True**:: + + .. admonition:: Example + + This is just a quick example + + **If False**:: + + .. rubric:: Example + + This is just a quick example + + **napoleon_use_admonition_for_notes** : bool, defaults to False + True to use the ``.. admonition::`` directive for **Notes** sections. + False to use the ``.. rubric::`` directive instead. + + .. note:: The singular **Note** section will always be converted to a + ``.. note::`` directive. + + .. seealso:: :attr:`napoleon_use_admonition_for_examples` + + **napoleon_use_admonition_for_references** : bool, defaults to False + True to use the ``.. admonition::`` directive for **References** + sections. False to use the ``.. rubric::`` directive instead. + + .. seealso:: :attr:`napoleon_use_admonition_for_examples` + + **napoleon_use_ivar** : bool, defaults to False + True to use the ``:ivar:`` role for instance variables. False to use + the ``.. attribute::`` directive instead. + + This `NumPy style`_ snippet will be converted as follows:: + + Attributes + ---------- + attr1 : int + Description of `attr1` + + **If True**:: + + :ivar attr1: Description of `attr1` + :vartype attr1: int + + **If False**:: + + .. attribute:: attr1 + :annotation: int + + Description of `attr1` + + **napoleon_use_param** : bool, defaults to False + True to use a ``:param:`` role for each function parameter. False to + use a single ``:parameters:`` role for all the parameters. + + This `NumPy style`_ snippet will be converted as follows:: + + Parameters + ---------- + arg1 : str + Description of `arg1` + arg2 : int, optional + Description of `arg2`, defaults to 0 + + **If True**:: + + :param arg1: Description of `arg1` + :type arg1: str + :param arg2: Description of `arg2`, defaults to 0 + :type arg2: int, optional + + **If False**:: + + :parameters: * **arg1** (*str*) -- + Description of `arg1` + * **arg2** (*int, optional*) -- + Description of `arg2`, defaults to 0 + + **napoleon_use_rtype** : bool, defaults to False + True to use the ``:rtype:`` role for the return type. False to output + the return type inline with the description. + + This `NumPy style`_ snippet will be converted as follows:: + + Returns + ------- + bool + True if successful, False otherwise + + **If True**:: + + :returns: True if successful, False otherwise + :rtype: bool + + **If False**:: + + :returns: *bool* -- True if successful, False otherwise + + """ + _config_values = { + 'napoleon_google_docstring': (True, 'env'), + 'napoleon_numpy_docstring': (True, 'env'), + 'napoleon_include_private_with_doc': (False, 'env'), + 'napoleon_include_special_with_doc': (True, 'env'), + 'napoleon_use_admonition_for_examples': (False, 'env'), + 'napoleon_use_admonition_for_notes': (False, 'env'), + 'napoleon_use_admonition_for_references': (False, 'env'), + 'napoleon_use_ivar': (False, 'env'), + 'napoleon_use_param': (False, 'env'), + 'napoleon_use_rtype': (False, 'env'), + } + + def __init__(self, **settings): + for name, (default, rebuild) in self._config_values.iteritems(): + setattr(self, name, default) + for name, value in settings.iteritems(): + setattr(self, name, value) + + +def setup(app): + """Sphinx extension setup function. + + When the extension is loaded, Sphinx imports this module and executes + the ``setup()`` function, which in turn notifies Sphinx of everything + the extension offers. + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process + + See Also + -------- + The Sphinx documentation on `Extensions`_, the `Extension Tutorial`_, and + the `Extension API`_. + + .. _Extensions: http://sphinx-doc.org/extensions.html + .. _Extension Tutorial: http://sphinx-doc.org/ext/tutorial.html + .. _Extension API: http://sphinx-doc.org/ext/appapi.html + + """ + from sphinx.application import Sphinx + if not isinstance(app, Sphinx): + return # probably called by tests + + app.connect('autodoc-process-docstring', _process_docstring) + app.connect('autodoc-skip-member', _skip_member) + + for name, (default, rebuild) in Config._config_values.iteritems(): + app.add_config_value(name, default, rebuild) + + +def _process_docstring(app, what, name, obj, options, lines): + """Process the docstring for a given python object. + + Called when autodoc has read and processed a docstring. `lines` is a list + of docstring lines that `_process_docstring` modifies in place to change + what Sphinx outputs. + + The following settings in conf.py control what styles of docstrings will + be parsed: + + * ``napoleon_google_docstring`` -- parse Google style docstrings + * ``napoleon_numpy_docstring`` -- parse NumPy style docstrings + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process. + what : str + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str + The fully qualified name of the object. + obj : module, class, exception, function, method, or attribute + The object to which the docstring belongs. + options : sphinx.ext.autodoc.Options + The options given to the directive: an object with attributes + inherited_members, undoc_members, show_inheritance and noindex that + are True if the flag option of same name was given to the auto + directive. + lines : list of str + The lines of the docstring, see above. + + .. note:: `lines` is modified *in place* + + """ + result_lines = lines + if app.config.napoleon_numpy_docstring: + docstring = NumpyDocstring(result_lines, app.config, app, what, name, + obj, options) + result_lines = docstring.lines() + if app.config.napoleon_google_docstring: + docstring = GoogleDocstring(result_lines, app.config, app, what, name, + obj, options) + result_lines = docstring.lines() + lines[:] = result_lines[:] + + +def _skip_member(app, what, name, obj, skip, options): + """Determine if private and special class members are included in docs. + + The following settings in conf.py determine if private and special class + members are included in the generated documentation: + + * ``napoleon_include_private_with_doc`` -- + include private members if they have docstrings + * ``napoleon_include_special_with_doc`` -- + include special members if they have docstrings + + Parameters + ---------- + app : sphinx.application.Sphinx + Application object representing the Sphinx process + what : str + A string specifying the type of the object to which the member + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str + The name of the member. + obj : module, class, exception, function, method, or attribute. + For example, if the member is the __init__ method of class A, then + `obj` will be `A.__init__`. + skip : bool + A boolean indicating if autodoc will skip this member if `_skip_member` + does not override the decision + options : sphinx.ext.autodoc.Options + The options given to the directive: an object with attributes + inherited_members, undoc_members, show_inheritance and noindex that + are True if the flag option of same name was given to the auto + directive. + + Returns + ------- + bool + True if the member should be skipped during creation of the docs, + False if it should be included in the docs. + + """ + has_doc = getattr(obj, '__doc__', False) + is_member = (what == 'class' or what == 'exception' or what == 'module') + if name != '__weakref__' and name != '__init__' and has_doc and is_member: + if what == 'class' or what == 'exception': + if sys.version_info[0] < 3: + cls = getattr(obj, 'im_class', getattr(obj, '__objclass__', + None)) + cls_is_owner = (cls and hasattr(cls, name) and + name in cls.__dict__) + elif sys.version_info[1] >= 3 and hasattr(obj, '__qualname__'): + cls_path, _, _ = obj.__qualname__.rpartition('.') + if cls_path: + import importlib + import functools + + mod = importlib.import_module(obj.__module__) + cls = functools.reduce(getattr, cls_path.split('.'), mod) + cls_is_owner = (cls and hasattr(cls, name) and + name in cls.__dict__) + else: + cls_is_owner = False + else: + cls_is_owner = True + + if what == 'module' or cls_is_owner: + is_special = name.startswith('__') and name.endswith('__') + is_private = not is_special and name.startswith('_') + inc_special = app.config.napoleon_include_special_with_doc + inc_private = app.config.napoleon_include_private_with_doc + if (is_special and inc_special) or (is_private and inc_private): + return False + return skip diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py new file mode 100644 index 00000000..d6c73db8 --- /dev/null +++ b/sphinx/ext/napoleon/docstring.py @@ -0,0 +1,714 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.napoleon.docstring + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + Classes for docstring parsing and formatting. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import re +import sys +from sphinx.ext.napoleon.iterators import modify_iter + + +if sys.version_info[0] >= 3: + basestring = str + xrange = range + + +_directive_regex = re.compile(r'\.\. \S+::') +_field_parens_regex = re.compile(r'\s*(\w+)\s*\(\s*(.+?)\s*\)') + + +class GoogleDocstring(object): + """Parse Google style docstrings. + + Convert Google style docstrings to reStructuredText. + + Parameters + ---------- + docstring : str or list of str + The docstring to parse, given either as a string or split into + individual lines. + config : sphinx.ext.napoleon.Config or sphinx.config.Config, optional + The configuration settings to use. If not given, defaults to the + config object on `app`; or if `app` is not given defaults to the + a new `sphinx.ext.napoleon.Config` object. + + See Also + -------- + :class:`sphinx.ext.napoleon.Config` + + Other Parameters + ---------------- + app : sphinx.application.Sphinx, optional + Application object representing the Sphinx process. + what : str, optional + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str, optional + The fully qualified name of the object. + obj : module, class, exception, function, method, or attribute + The object to which the docstring belongs. + options : sphinx.ext.autodoc.Options, optional + The options given to the directive: an object with attributes + inherited_members, undoc_members, show_inheritance and noindex that + are True if the flag option of same name was given to the auto + directive. + + Example + ------- + >>> from sphinx.ext.napoleon import Config + >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) + >>> docstring = '''One line summary. + ... + ... Extended description. + ... + ... Args: + ... arg1(int): Description of `arg1` + ... arg2(str): Description of `arg2` + ... Returns: + ... str: Description of return value. + ... ''' + >>> print(GoogleDocstring(docstring, config)) + One line summary. + <BLANKLINE> + Extended description. + <BLANKLINE> + :param arg1: Description of `arg1` + :type arg1: int + :param arg2: Description of `arg2` + :type arg2: str + <BLANKLINE> + :returns: Description of return value. + :rtype: str + + """ + def __init__(self, docstring, config=None, app=None, what='', name='', + obj=None, options=None): + self._config = config + self._app = app + if not self._config: + from sphinx.ext.napoleon import Config + self._config = self._app and self._app.config or Config() + self._what = what + self._name = name + self._obj = obj + self._opt = options + if isinstance(docstring, basestring): + docstring = docstring.splitlines() + self._lines = docstring + self._line_iter = modify_iter(docstring, modifier=lambda s: s.rstrip()) + self._parsed_lines = [] + self._is_in_section = False + self._section_indent = 0 + if not hasattr(self, '_directive_sections'): + self._directive_sections = [] + if not hasattr(self, '_sections'): + self._sections = { + 'args': self._parse_parameters_section, + 'arguments': self._parse_parameters_section, + 'attributes': self._parse_attributes_section, + 'example': self._parse_examples_section, + 'examples': self._parse_examples_section, + 'keyword args': self._parse_keyword_arguments_section, + 'keyword arguments': self._parse_keyword_arguments_section, + 'methods': self._parse_methods_section, + 'note': self._parse_note_section, + 'notes': self._parse_notes_section, + 'other parameters': self._parse_other_parameters_section, + 'parameters': self._parse_parameters_section, + 'return': self._parse_returns_section, + 'returns': self._parse_returns_section, + 'raises': self._parse_raises_section, + 'references': self._parse_references_section, + 'see also': self._parse_see_also_section, + 'warning': self._parse_warning_section, + 'warnings': self._parse_warning_section, + 'warns': self._parse_warns_section, + 'yields': self._parse_yields_section, + } + self._parse() + + def __str__(self): + """Return the parsed docstring in reStructuredText format. + + Returns + ------- + str + UTF-8 encoded version of the docstring. + + """ + if sys.version_info[0] >= 3: + return self.__unicode__() + else: + return self.__unicode__().encode('utf8') + + def __unicode__(self): + """Return the parsed docstring in reStructuredText format. + + Returns + ------- + unicode + Unicode version of the docstring. + + """ + return u'\n'.join(self.lines()) + + def lines(self): + """Return the parsed lines of the docstring in reStructuredText format. + + Returns + ------- + list of str + The lines of the docstring in a list. + + """ + return self._parsed_lines + + def _consume_indented_block(self, indent=1): + lines = [] + line = self._line_iter.peek() + while(not self._is_section_break() + and (not line or self._is_indented(line, indent))): + lines.append(self._line_iter.next()) + line = self._line_iter.peek() + return lines + + def _consume_contiguous(self): + lines = [] + while (self._line_iter.has_next() + and self._line_iter.peek() + and not self._is_section_header()): + lines.append(self._line_iter.next()) + return lines + + def _consume_empty(self): + lines = [] + line = self._line_iter.peek() + while self._line_iter.has_next() and not line: + lines.append(self._line_iter.next()) + line = self._line_iter.peek() + return lines + + def _consume_field(self, parse_type=True, prefer_type=False): + line = self._line_iter.next() + _name, _, _desc = line.partition(':') + _name, _type, _desc = _name.strip(), '', _desc.strip() + match = _field_parens_regex.match(_name) + if parse_type and match: + _name = match.group(1) + _type = match.group(2) + if prefer_type and not _type: + _type, _name = _name, _type + indent = self._get_indent(line) + 1 + _desc = [_desc] + self._dedent(self._consume_indented_block(indent)) + _desc = self.__class__(_desc, self._config).lines() + return _name, _type, _desc + + def _consume_fields(self, parse_type=True, prefer_type=False): + self._consume_empty() + fields = [] + while not self._is_section_break(): + _name, _type, _desc = self._consume_field(parse_type, prefer_type) + if _name or _type or _desc: + fields.append((_name, _type, _desc,)) + return fields + + def _consume_returns_section(self): + lines = self._dedent(self._consume_to_next_section()) + if lines: + if ':' in lines[0]: + _type, _, _desc = lines[0].partition(':') + _name, _type, _desc = '', _type.strip(), _desc.strip() + match = _field_parens_regex.match(_type) + if match: + _name = match.group(1) + _type = match.group(2) + lines[0] = _desc + _desc = lines + else: + _name, _type, _desc = '', '', lines + _desc = self.__class__(_desc, self._config).lines() + return [(_name, _type, _desc,)] + else: + return [] + + def _consume_section_header(self): + section = self._line_iter.next() + stripped_section = section.strip(':') + if stripped_section.lower() in self._sections: + section = stripped_section + return section + + def _consume_to_next_section(self): + self._consume_empty() + lines = [] + while not self._is_section_break(): + lines.append(self._line_iter.next()) + return lines + self._consume_empty() + + def _dedent(self, lines, full=False): + if full: + return [line.lstrip() for line in lines] + else: + min_indent = self._get_min_indent(lines) + return [line[min_indent:] for line in lines] + + def _format_admonition(self, admonition, lines): + lines = self._strip_empty(lines) + if len(lines) == 1: + return ['.. %s:: %s' % (admonition, lines[0].strip()), ''] + elif lines: + lines = self._indent(self._dedent(lines), 3) + return ['.. %s::' % admonition, ''] + lines + [''] + else: + return ['.. %s::' % admonition, ''] + + def _format_block(self, prefix, lines, padding=None): + if lines: + if padding is None: + padding = ' ' * len(prefix) + result_lines = [] + for i, line in enumerate(lines): + if line: + if i == 0: + result_lines.append(prefix + line) + else: + result_lines.append(padding + line) + else: + result_lines.append('') + return result_lines + else: + return [prefix] + + def _format_field(self, _name, _type, _desc): + separator = any([s for s in _desc]) and ' --' or '' + if _name: + if _type: + field = ['**%s** (*%s*)%s' % (_name, _type, separator)] + else: + field = ['**%s**%s' % (_name, separator)] + elif _type: + field = ['*%s*%s' % (_type, separator)] + else: + field = [] + return field + _desc + + def _format_fields(self, field_type, fields): + field_type = ':%s:' % field_type.strip() + padding = ' ' * len(field_type) + multi = len(fields) > 1 + lines = [] + for _name, _type, _desc in fields: + field = self._format_field(_name, _type, _desc) + if multi: + if lines: + lines.extend(self._format_block(padding + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' ', field)) + return lines + + def _get_current_indent(self, peek_ahead=0): + line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] + while line != self._line_iter.sentinel: + if line: + return self._get_indent(line) + peek_ahead += 1 + line = self._line_iter.peek(peek_ahead + 1)[peek_ahead] + return 0 + + def _get_indent(self, line): + for i, s in enumerate(line): + if not s.isspace(): + return i + return len(line) + + def _get_min_indent(self, lines): + min_indent = None + for line in lines: + if line: + indent = self._get_indent(line) + if min_indent is None: + min_indent = indent + elif indent < min_indent: + min_indent = indent + return min_indent or 0 + + def _indent(self, lines, n=4): + return [(' ' * n) + line for line in lines] + + def _is_indented(self, line, indent=1): + for i, s in enumerate(line): + if i >= indent: + return True + elif not s.isspace(): + return False + return False + + def _is_section_header(self): + section = self._line_iter.peek().lower() + if section.strip(':') in self._sections: + header_indent = self._get_indent(section) + section_indent = self._get_current_indent(peek_ahead=1) + return section_indent > header_indent + elif self._directive_sections: + if _directive_regex.match(section): + for directive_section in self._directive_sections: + if section.startswith(directive_section): + return True + return False + + def _is_section_break(self): + line = self._line_iter.peek() + return (not self._line_iter.has_next() + or self._is_section_header() + or (self._is_in_section + and line + and not self._is_indented(line, self._section_indent))) + + def _parse(self): + self._parsed_lines = self._consume_empty() + while self._line_iter.has_next(): + if self._is_section_header(): + try: + section = self._consume_section_header() + self._is_in_section = True + self._section_indent = self._get_current_indent() + if _directive_regex.match(section): + lines = [section] + self._consume_to_next_section() + else: + lines = self._sections[section.lower()](section) + finally: + self._is_in_section = False + self._section_indent = 0 + else: + if not self._parsed_lines: + lines = self._consume_contiguous() + self._consume_empty() + else: + lines = self._consume_to_next_section() + self._parsed_lines.extend(lines) + + def _parse_attributes_section(self, section): + lines = [] + for _name, _type, _desc in self._consume_fields(): + if self._config.napoleon_use_ivar: + field = ':ivar %s: ' % _name + lines.extend(self._format_block(field, _desc)) + if _type: + lines.append(':vartype %s: %s' % (_name, _type)) + else: + lines.append('.. attribute:: ' + _name) + if _type: + lines.append(' :annotation: ' + _type) + if _desc: + lines.extend([''] + self._indent(_desc, 3)) + lines.append('') + if self._config.napoleon_use_ivar: + lines.append('') + return lines + + def _parse_examples_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_examples + return self._parse_generic_section(section, use_admonition) + + def _parse_generic_section(self, section, use_admonition): + lines = self._strip_empty(self._consume_to_next_section()) + lines = self._dedent(lines) + if use_admonition: + header = '.. admonition:: %s' % section + lines = self._indent(lines, 3) + else: + header = '.. rubric:: %s' % section + if lines: + return [header, ''] + lines + [''] + else: + return [header, ''] + + def _parse_keyword_arguments_section(self, section): + return self._format_fields('Keyword Arguments', self._consume_fields()) + + def _parse_methods_section(self, section): + lines = [] + for _name, _, _desc in self._consume_fields(parse_type=False): + lines.append('.. method:: %s' % _name) + if _desc: + lines.extend([''] + self._indent(_desc, 3)) + lines.append('') + return lines + + def _parse_note_section(self, section): + lines = self._consume_to_next_section() + return self._format_admonition('note', lines) + + def _parse_notes_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_notes + return self._parse_generic_section('Notes', use_admonition) + + def _parse_other_parameters_section(self, section): + return self._format_fields('Other Parameters', self._consume_fields()) + + def _parse_parameters_section(self, section): + fields = self._consume_fields() + if self._config.napoleon_use_param: + lines = [] + for _name, _type, _desc in fields: + field = ':param %s: ' % _name + lines.extend(self._format_block(field, _desc)) + if _type: + lines.append(':type %s: %s' % (_name, _type)) + return lines + [''] + else: + return self._format_fields('Parameters', fields) + + def _parse_raises_section(self, section): + fields = self._consume_fields() + field_type = ':raises:' + padding = ' ' * len(field_type) + multi = len(fields) > 1 + lines = [] + for _name, _type, _desc in fields: + sep = _desc and ' -- ' or '' + if _name: + if ' ' in _name: + _name = '**%s**' % _name + else: + _name = ':exc:`%s`' % _name + if _type: + field = ['%s (*%s*)%s' % (_name, _type, sep)] + else: + field = ['%s%s' % (_name, sep)] + elif _type: + field = ['*%s*%s' % (_type, sep)] + else: + field = [] + field = field + _desc + if multi: + if lines: + lines.extend(self._format_block(padding + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' * ', field)) + else: + lines.extend(self._format_block(field_type + ' ', field)) + return lines + + def _parse_references_section(self, section): + use_admonition = self._config.napoleon_use_admonition_for_references + return self._parse_generic_section('References', use_admonition) + + def _parse_returns_section(self, section): + fields = self._consume_returns_section() + multi = len(fields) > 1 + if multi: + use_rtype = False + else: + use_rtype = self._config.napoleon_use_rtype + + lines = [] + for _name, _type, _desc in fields: + if use_rtype: + field = self._format_field(_name, '', _desc) + else: + field = self._format_field(_name, _type, _desc) + + if multi: + if lines: + lines.extend(self._format_block(' * ', field)) + else: + lines.extend(self._format_block(':returns: * ', field)) + else: + lines.extend(self._format_block(':returns: ', field)) + if _type and use_rtype: + lines.append(':rtype: %s' % _type) + return lines + + def _parse_see_also_section(self, section): + lines = self._consume_to_next_section() + return self._format_admonition('seealso', lines) + + def _parse_warning_section(self, section): + lines = self._consume_to_next_section() + return self._format_admonition('warning', lines) + + def _parse_warns_section(self, section): + return self._format_fields('Warns', self._consume_fields()) + + def _parse_yields_section(self, section): + fields = self._consume_fields(prefer_type=True) + return self._format_fields('Yields', fields) + + def _strip_empty(self, lines): + if lines: + start = -1 + for i, line in enumerate(lines): + if line: + start = i + break + if start == -1: + lines = [] + end = -1 + for i in reversed(xrange(len(lines))): + line = lines[i] + if line: + end = i + break + if start > 0 or end + 1 < len(lines): + lines = lines[start:end + 1] + return lines + + +class NumpyDocstring(GoogleDocstring): + """Parse NumPy style docstrings. + + Convert NumPy style docstrings to reStructuredText. + + Parameters + ---------- + docstring : str or list of str + The docstring to parse, given either as a string or split into + individual lines. + config : sphinx.ext.napoleon.Config or sphinx.config.Config, optional + The configuration settings to use. If not given, defaults to the + config object on `app`; or if `app` is not given defaults to the + a new `sphinx.ext.napoleon.Config` object. + + See Also + -------- + :class:`sphinx.ext.napoleon.Config` + + Other Parameters + ---------------- + app : sphinx.application.Sphinx, optional + Application object representing the Sphinx process. + what : str, optional + A string specifying the type of the object to which the docstring + belongs. Valid values: "module", "class", "exception", "function", + "method", "attribute". + name : str, optional + The fully qualified name of the object. + obj : module, class, exception, function, method, or attribute + The object to which the docstring belongs. + options : sphinx.ext.autodoc.Options, optional + The options given to the directive: an object with attributes + inherited_members, undoc_members, show_inheritance and noindex that + are True if the flag option of same name was given to the auto + directive. + + Example + ------- + >>> from sphinx.ext.napoleon import Config + >>> config = Config(napoleon_use_param=True, napoleon_use_rtype=True) + >>> docstring = '''One line summary. + ... + ... Extended description. + ... + ... Parameters + ... ---------- + ... arg1 : int + ... Description of `arg1` + ... arg2 : str + ... Description of `arg2` + ... Returns + ... ------- + ... str + ... Description of return value. + ... ''' + >>> print(NumpyDocstring(docstring, config)) + One line summary. + <BLANKLINE> + Extended description. + <BLANKLINE> + :param arg1: Description of `arg1` + :type arg1: int + :param arg2: Description of `arg2` + :type arg2: str + <BLANKLINE> + :returns: Description of return value. + :rtype: str + + Methods + ------- + __str__() + Return the parsed docstring in reStructuredText format. + + Returns + ------- + str + UTF-8 encoded version of the docstring. + + __unicode__() + Return the parsed docstring in reStructuredText format. + + Returns + ------- + unicode + Unicode version of the docstring. + + lines() + Return the parsed lines of the docstring in reStructuredText format. + + Returns + ------- + list of str + The lines of the docstring in a list. + + """ + def __init__(self, docstring, config=None, app=None, what='', name='', + obj=None, options=None): + self._directive_sections = ['.. index::'] + super(NumpyDocstring, self).__init__(docstring, config, app, what, + name, obj, options) + + def _consume_field(self, parse_type=True, prefer_type=False): + line = self._line_iter.next() + if parse_type: + _name, _, _type = line.partition(':') + else: + _name, _type = line, '' + _name, _type = _name.strip(), _type.strip() + if prefer_type and not _type: + _type, _name = _name, _type + indent = self._get_indent(line) + _desc = self._dedent(self._consume_indented_block(indent + 1)) + _desc = self.__class__(_desc, self._config).lines() + return _name, _type, _desc + + def _consume_returns_section(self): + return self._consume_fields(prefer_type=True) + + def _consume_section_header(self): + section = self._line_iter.next() + if not _directive_regex.match(section): + # Consume the header underline + self._line_iter.next() + return section + + def _is_section_break(self): + line1, line2 = self._line_iter.peek(2) + return (not self._line_iter.has_next() + or self._is_section_header() + or ['', ''] == [line1, line2] + or (self._is_in_section + and line1 + and not self._is_indented(line1, self._section_indent))) + + def _is_section_header(self): + section, underline = self._line_iter.peek(2) + section = section.lower() + if section in self._sections and isinstance(underline, basestring): + pattern = r'[=\-`:\'"~^_*+#<>]{' + str(len(section)) + r'}$' + return bool(re.match(pattern, underline)) + elif self._directive_sections: + if _directive_regex.match(section): + for directive_section in self._directive_sections: + if section.startswith(directive_section): + return True + return False diff --git a/sphinx/ext/napoleon/iterators.py b/sphinx/ext/napoleon/iterators.py new file mode 100644 index 00000000..2f1904da --- /dev/null +++ b/sphinx/ext/napoleon/iterators.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.napoleon.iterators + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + + A collection of helpful iterators. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import collections +import sys + + +if sys.version_info[0] >= 3: + callable = lambda o: hasattr(o, '__call__') + + +class peek_iter(object): + """An iterator object that supports peeking ahead. + + Parameters + ---------- + o : iterable or callable + `o` is interpreted very differently depending on the presence of + `sentinel`. + + If `sentinel` is not given, then `o` must be a collection object + which supports either the iteration protocol or the sequence protocol. + + If `sentinel` is given, then `o` must be a callable object. + + sentinel : any value, optional + If given, the iterator will call `o` with no arguments for each + call to its `next` method; if the value returned is equal to + `sentinel`, :exc:`StopIteration` will be raised, otherwise the + value will be returned. + + See Also + -------- + `peek_iter` can operate as a drop in replacement for the built-in + `iter <http://docs.python.org/2/library/functions.html#iter>`_ function. + + Attributes + ---------- + sentinel + The value used to indicate the iterator is exhausted. If `sentinel` + was not given when the `peek_iter` was instantiated, then it will + be set to a new object instance: ``object()``. + + """ + def __init__(self, *args): + """__init__(o, sentinel=None)""" + self._iterable = iter(*args) + self._cache = collections.deque() + if len(args) == 2: + self.sentinel = args[1] + else: + self.sentinel = object() + + def __iter__(self): + return self + + def __next__(self, n=None): + # note: prevent 2to3 to transform self.next() in next(self) which + # causes an infinite loop ! + return getattr(self, 'next')(n) + + def _fillcache(self, n): + """Cache `n` items. If `n` is 0 or None, then 1 item is cached.""" + if not n: + n = 1 + try: + while len(self._cache) < n: + self._cache.append(self._iterable.next()) + except StopIteration: + while len(self._cache) < n: + self._cache.append(self.sentinel) + + def has_next(self): + """Determine if iterator is exhausted. + + Returns + ------- + bool + True if iterator has more items, False otherwise. + + Note + ---- + Will never raise :exc:`StopIteration`. + + """ + return self.peek() != self.sentinel + + def next(self, n=None): + """Get the next item or `n` items of the iterator. + + Parameters + ---------- + n : int or None + The number of items to retrieve. Defaults to None. + + Returns + ------- + item or list of items + The next item or `n` items of the iterator. If `n` is None, the + item itself is returned. If `n` is an int, the items will be + returned in a list. If `n` is 0, an empty list is returned. + + Raises + ------ + StopIteration + Raised if the iterator is exhausted, even if `n` is 0. + + """ + self._fillcache(n) + if not n: + if self._cache[0] == self.sentinel: + raise StopIteration + if n is None: + result = self._cache.popleft() + else: + result = [] + else: + if self._cache[n - 1] == self.sentinel: + raise StopIteration + result = [self._cache.popleft() for i in range(n)] + return result + + def peek(self, n=None): + """Preview the next item or `n` items of the iterator. + + The iterator is not advanced when peek is called. + + Returns + ------- + item or list of items + The next item or `n` items of the iterator. If `n` is None, the + item itself is returned. If `n` is an int, the items will be + returned in a list. If `n` is 0, an empty list is returned. + + If the iterator is exhausted, `peek_iter.sentinel` is returned, + or placed as the last item in the returned list. + + Note + ---- + Will never raise :exc:`StopIteration`. + + """ + self._fillcache(n) + if n is None: + result = self._cache[0] + else: + result = [self._cache[i] for i in range(n)] + return result + + +class modify_iter(peek_iter): + """An iterator object that supports modifying items as they are returned. + + Parameters + ---------- + o : iterable or callable + `o` is interpreted very differently depending on the presence of + `sentinel`. + + If `sentinel` is not given, then `o` must be a collection object + which supports either the iteration protocol or the sequence protocol. + + If `sentinel` is given, then `o` must be a callable object. + + sentinel : any value, optional + If given, the iterator will call `o` with no arguments for each + call to its `next` method; if the value returned is equal to + `sentinel`, :exc:`StopIteration` will be raised, otherwise the + value will be returned. + + modifier : callable, optional + The function that will be used to modify each item returned by the + iterator. `modifier` should take a single argument and return a + single value. Defaults to ``lambda x: x``. + + If `sentinel` is not given, `modifier` must be passed as a keyword + argument. + + Attributes + ---------- + modifier : callable + `modifier` is called with each item in `o` as it is iterated. The + return value of `modifier` is returned in lieu of the item. + + Values returned by `peek` as well as `next` are affected by + `modifier`. However, `modify_iter.sentinel` is never passed through + `modifier`; it will always be returned from `peek` unmodified. + + Example + ------- + >>> a = [" A list ", + ... " of strings ", + ... " with ", + ... " extra ", + ... " whitespace. "] + >>> modifier = lambda s: s.strip().replace('with', 'without') + >>> for s in modify_iter(a, modifier=modifier): + ... print('"%s"' % s) + "A list" + "of strings" + "without" + "extra" + "whitespace." + + """ + def __init__(self, *args, **kwargs): + """__init__(o, sentinel=None, modifier=lambda x: x)""" + if 'modifier' in kwargs: + self.modifier = kwargs['modifier'] + elif len(args) > 2: + self.modifier = args[2] + args = args[:2] + else: + self.modifier = lambda x: x + if not callable(self.modifier): + raise TypeError('modify_iter(o, modifier): ' + 'modifier must be callable') + super(modify_iter, self).__init__(*args) + + def _fillcache(self, n): + """Cache `n` modified items. If `n` is 0 or None, 1 item is cached. + + Each item returned by the iterator is passed through the + `modify_iter.modified` function before being cached. + + """ + if not n: + n = 1 + try: + while len(self._cache) < n: + self._cache.append(self.modifier(self._iterable.next())) + except StopIteration: + while len(self._cache) < n: + self._cache.append(self.sentinel) diff --git a/tests/root/conf.py b/tests/root/conf.py index f0d40148..af984e5e 100644 --- a/tests/root/conf.py +++ b/tests/root/conf.py @@ -8,7 +8,7 @@ sys.path.append(os.path.abspath('..')) extensions = ['sphinx.ext.autodoc', 'sphinx.ext.jsmath', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.autosummary', 'sphinx.ext.doctest', 'sphinx.ext.extlinks', - 'sphinx.ext.viewcode', 'ext'] + 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'ext'] jsmath_path = 'dummy.js' diff --git a/tests/test_napoleon.py b/tests/test_napoleon.py new file mode 100644 index 00000000..d8c71960 --- /dev/null +++ b/tests/test_napoleon.py @@ -0,0 +1,190 @@ +# -*- coding: utf-8 -*- +""" + test_napoleon + ~~~~~~~~~~~~~ + + Tests for :mod:`sphinx.ext.napoleon.__init__` module. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from mock import Mock +from sphinx.application import Sphinx +from sphinx.ext.napoleon import (_process_docstring, _skip_member, Config, + setup) +from unittest import TestCase + + +def _private_doc(): + """module._private_doc.DOCSTRING""" + pass + + +def _private_undoc(): + pass + + +def __special_doc__(): + """module.__special_doc__.DOCSTRING""" + pass + + +def __special_undoc__(): + pass + + +class SampleClass(object): + def _private_doc(self): + """SampleClass._private_doc.DOCSTRING""" + pass + + def _private_undoc(self): + pass + + def __special_doc__(self): + """SampleClass.__special_doc__.DOCSTRING""" + pass + + def __special_undoc__(self): + pass + + +class SampleError(Exception): + def _private_doc(self): + """SampleError._private_doc.DOCSTRING""" + pass + + def _private_undoc(self): + pass + + def __special_doc__(self): + """SampleError.__special_doc__.DOCSTRING""" + pass + + def __special_undoc__(self): + pass + + +class ProcessDocstringTest(TestCase): + def test_modify_in_place(self): + lines = ['Summary line.', + '', + 'Args:', + ' arg1: arg1 description'] + app = Mock() + app.config = Config() + _process_docstring(app, 'class', 'SampleClass', SampleClass, Mock(), + lines) + + expected = ['Summary line.', + '', + ':Parameters: **arg1** --', + ' arg1 description'] + self.assertEqual(expected, lines) + + +class SetupTest(TestCase): + def test_unknown_app_type(self): + setup(object()) + + def test_add_config_values(self): + app = Mock(Sphinx) + setup(app) + for name, (default, rebuild) in Config._config_values.items(): + has_config = False + for method_name, args, kwargs in app.method_calls: + if(method_name == 'add_config_value' and + args[0] == name): + has_config = True + if not has_config: + self.fail('Config value was not added to app %s' % name) + + has_process_docstring = False + has_skip_member = False + for method_name, args, kwargs in app.method_calls: + if method_name == 'connect': + if(args[0] == 'autodoc-process-docstring' and + args[1] == _process_docstring): + has_process_docstring = True + elif(args[0] == 'autodoc-skip-member' and + args[1] == _skip_member): + has_skip_member = True + if not has_process_docstring: + self.fail('autodoc-process-docstring never connected') + if not has_skip_member: + self.fail('autodoc-skip-member never connected') + + +class SkipMemberTest(TestCase): + def assertSkip(self, what, member, obj, expect_skip, config_name): + skip = 'default skip' + app = Mock() + app.config = Config() + setattr(app.config, config_name, True) + if expect_skip: + self.assertEqual(skip, _skip_member(app, what, member, obj, skip, + Mock())) + else: + self.assertFalse(_skip_member(app, what, member, obj, skip, + Mock())) + setattr(app.config, config_name, False) + self.assertEqual(skip, _skip_member(app, what, member, obj, skip, + Mock())) + + def test_class_private_doc(self): + self.assertSkip('class', '_private_doc', + SampleClass._private_doc, False, + 'napoleon_include_private_with_doc') + + def test_class_private_undoc(self): + self.assertSkip('class', '_private_undoc', + SampleClass._private_undoc, True, + 'napoleon_include_private_with_doc') + + def test_class_special_doc(self): + self.assertSkip('class', '__special_doc__', + SampleClass.__special_doc__, False, + 'napoleon_include_special_with_doc') + + def test_class_special_undoc(self): + self.assertSkip('class', '__special_undoc__', + SampleClass.__special_undoc__, True, + 'napoleon_include_special_with_doc') + + def test_exception_private_doc(self): + self.assertSkip('exception', '_private_doc', + SampleError._private_doc, False, + 'napoleon_include_private_with_doc') + + def test_exception_private_undoc(self): + self.assertSkip('exception', '_private_undoc', + SampleError._private_undoc, True, + 'napoleon_include_private_with_doc') + + def test_exception_special_doc(self): + self.assertSkip('exception', '__special_doc__', + SampleError.__special_doc__, False, + 'napoleon_include_special_with_doc') + + def test_exception_special_undoc(self): + self.assertSkip('exception', '__special_undoc__', + SampleError.__special_undoc__, True, + 'napoleon_include_special_with_doc') + + def test_module_private_doc(self): + self.assertSkip('module', '_private_doc', _private_doc, False, + 'napoleon_include_private_with_doc') + + def test_module_private_undoc(self): + self.assertSkip('module', '_private_undoc', _private_undoc, True, + 'napoleon_include_private_with_doc') + + def test_module_special_doc(self): + self.assertSkip('module', '__special_doc__', __special_doc__, False, + 'napoleon_include_special_with_doc') + + def test_module_special_undoc(self): + self.assertSkip('module', '__special_undoc__', __special_undoc__, True, + 'napoleon_include_special_with_doc') diff --git a/tests/test_napoleon_docstring.py b/tests/test_napoleon_docstring.py new file mode 100644 index 00000000..aa52aebe --- /dev/null +++ b/tests/test_napoleon_docstring.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +""" + test_napoleon_docstring + ~~~~~~~~~~~~~~~~~~~~~~~ + + Tests for :mod:`sphinx.ext.napoleon.docstring` module. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import textwrap +from sphinx.ext.napoleon.docstring import GoogleDocstring, NumpyDocstring +from unittest import TestCase + + +class BaseDocstringTest(TestCase): + pass + + +class GoogleDocstringTest(BaseDocstringTest): + docstrings = [( + """Single line summary""", + """Single line summary""" + ), ( + """ + Single line summary + + Extended description + + """, + """ + Single line summary + + Extended description + """ + ), ( + """ + Single line summary + + Args: + arg1(str):Extended + description of arg1 + """, + """ + Single line summary + + :Parameters: **arg1** (*str*) -- + Extended + description of arg1""" + ), ( + """ + Single line summary + + Args: + arg1(str):Extended + description of arg1 + arg2 ( int ) : Extended + description of arg2 + + Keyword Args: + kwarg1(str):Extended + description of kwarg1 + kwarg2 ( int ) : Extended + description of kwarg2""", + """ + Single line summary + + :Parameters: * **arg1** (*str*) -- + Extended + description of arg1 + * **arg2** (*int*) -- + Extended + description of arg2 + + :Keyword Arguments: * **kwarg1** (*str*) -- + Extended + description of kwarg1 + * **kwarg2** (*int*) -- + Extended + description of kwarg2""" + ), ( + """ + Single line summary + + Arguments: + arg1(str):Extended + description of arg1 + arg2 ( int ) : Extended + description of arg2 + + Keyword Arguments: + kwarg1(str):Extended + description of kwarg1 + kwarg2 ( int ) : Extended + description of kwarg2""", + """ + Single line summary + + :Parameters: * **arg1** (*str*) -- + Extended + description of arg1 + * **arg2** (*int*) -- + Extended + description of arg2 + + :Keyword Arguments: * **kwarg1** (*str*) -- + Extended + description of kwarg1 + * **kwarg2** (*int*) -- + Extended + description of kwarg2""" + ), ( + """ + Single line summary + + Return: + str:Extended + description of return value + """, + """ + Single line summary + + :returns: *str* -- + Extended + description of return value""" + ), ( + """ + Single line summary + + Returns: + str:Extended + description of return value + """, + """ + Single line summary + + :returns: *str* -- + Extended + description of return value""" + )] + + def test_docstrings(self): + for docstring, expected in self.docstrings: + actual = str(GoogleDocstring(textwrap.dedent(docstring))) + expected = textwrap.dedent(expected) + self.assertEqual(expected, actual) + + +class NumpyDocstringTest(BaseDocstringTest): + docstrings = [( + """Single line summary""", + """Single line summary""" + ), ( + """ + Single line summary + + Extended description + + """, + """ + Single line summary + + Extended description + """ + ), ( + """ + Single line summary + + Parameters + ---------- + arg1:str + Extended + description of arg1 + """, + """ + Single line summary + + :Parameters: **arg1** (*str*) -- + Extended + description of arg1""" + ), ( + """ + Single line summary + + Parameters + ---------- + arg1:str + Extended + description of arg1 + arg2 : int + Extended + description of arg2 + + Keyword Arguments + ----------------- + kwarg1:str + Extended + description of kwarg1 + kwarg2 : int + Extended + description of kwarg2 + """, + """ + Single line summary + + :Parameters: * **arg1** (*str*) -- + Extended + description of arg1 + * **arg2** (*int*) -- + Extended + description of arg2 + + :Keyword Arguments: * **kwarg1** (*str*) -- + Extended + description of kwarg1 + * **kwarg2** (*int*) -- + Extended + description of kwarg2""" + ), ( + """ + Single line summary + + Return + ------ + str + Extended + description of return value + """, + """ + Single line summary + + :returns: *str* -- + Extended + description of return value""" + ), ( + """ + Single line summary + + Returns + ------- + str + Extended + description of return value + """, + """ + Single line summary + + :returns: *str* -- + Extended + description of return value""" + )] + + def test_docstrings(self): + for docstring, expected in self.docstrings: + actual = str(NumpyDocstring(textwrap.dedent(docstring))) + expected = textwrap.dedent(expected) + self.assertEqual(expected, actual) diff --git a/tests/test_napoleon_iterators.py b/tests/test_napoleon_iterators.py new file mode 100644 index 00000000..db0be32c --- /dev/null +++ b/tests/test_napoleon_iterators.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 -*- +""" + test_napoleon_iterators + ~~~~~~~~~~~~~~~~~~~~~~~ + + Tests for :mod:`sphinx.ext.napoleon.iterators` module. + + + :copyright: Copyright 2007-2014 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +from sphinx.ext.napoleon.iterators import peek_iter, modify_iter +from unittest import TestCase + + +class BaseIteratorsTest(TestCase): + def assertEqualTwice(self, expected, func, *args): + self.assertEqual(expected, func(*args)) + self.assertEqual(expected, func(*args)) + + def assertFalseTwice(self, func, *args): + self.assertFalse(func(*args)) + self.assertFalse(func(*args)) + + def assertNext(self, it, expected, is_last): + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(expected, it.peek) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(expected, it.peek) + self.assertTrueTwice(it.has_next) + self.assertEqual(expected, it.next()) + if is_last: + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next) + else: + self.assertTrueTwice(it.has_next) + + def assertRaisesTwice(self, exc, func, *args): + self.assertRaises(exc, func, *args) + self.assertRaises(exc, func, *args) + + def assertTrueTwice(self, func, *args): + self.assertTrue(func(*args)) + self.assertTrue(func(*args)) + + +class PeekIterTest(BaseIteratorsTest): + def test_init_with_sentinel(self): + a = iter(['1', '2', 'DONE']) + sentinel = 'DONE' + self.assertRaises(TypeError, peek_iter, a, sentinel) + + def get_next(): + return next(a) + it = peek_iter(get_next, sentinel) + self.assertEqual(it.sentinel, sentinel) + self.assertNext(it, '1', is_last=False) + self.assertNext(it, '2', is_last=True) + + def test_iter(self): + a = ['1', '2', '3'] + it = peek_iter(a) + self.assertTrue(it is it.__iter__()) + + a = [] + b = [i for i in peek_iter(a)] + self.assertEqual([], b) + + a = ['1'] + b = [i for i in peek_iter(a)] + self.assertEqual(['1'], b) + + a = ['1', '2'] + b = [i for i in peek_iter(a)] + self.assertEqual(['1', '2'], b) + + a = ['1', '2', '3'] + b = [i for i in peek_iter(a)] + self.assertEqual(['1', '2', '3'], b) + + def test_next_with_multi(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 2) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 2) + self.assertTrueTwice(it.has_next) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqual(['1', '2'], it.next(2)) + self.assertFalseTwice(it.has_next) + + a = ['1', '2', '3'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqual(['1', '2'], it.next(2)) + self.assertTrueTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 2) + self.assertTrueTwice(it.has_next) + + a = ['1', '2', '3', '4'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqual(['1', '2'], it.next(2)) + self.assertTrueTwice(it.has_next) + self.assertEqual(['3', '4'], it.next(2)) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 2) + self.assertFalseTwice(it.has_next) + + def test_next_with_none(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next) + self.assertFalseTwice(it.has_next) + + a = ['1'] + it = peek_iter(a) + self.assertEqual('1', it.__next__()) + + a = ['1'] + it = peek_iter(a) + self.assertNext(it, '1', is_last=True) + + a = ['1', '2'] + it = peek_iter(a) + self.assertNext(it, '1', is_last=False) + self.assertNext(it, '2', is_last=True) + + a = ['1', '2', '3'] + it = peek_iter(a) + self.assertNext(it, '1', is_last=False) + self.assertNext(it, '2', is_last=False) + self.assertNext(it, '3', is_last=True) + + def test_next_with_one(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 1) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqual(['1'], it.next(1)) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 1) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqual(['1'], it.next(1)) + self.assertTrueTwice(it.has_next) + self.assertEqual(['2'], it.next(1)) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 1) + + def test_next_with_zero(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertRaisesTwice(StopIteration, it.next, 0) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.next, 0) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.next, 0) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.next, 0) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.next, 0) + + def test_peek_with_multi(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice([it.sentinel, it.sentinel], it.peek, 2) + self.assertFalseTwice(it.has_next) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', it.sentinel], it.peek, 2) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', it.sentinel, it.sentinel], it.peek, 3) + self.assertTrueTwice(it.has_next) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', '2'], it.peek, 2) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', '2', it.sentinel], it.peek, 3) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', '2', it.sentinel, it.sentinel], it.peek, 4) + self.assertTrueTwice(it.has_next) + + a = ['1', '2', '3'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', '2'], it.peek, 2) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', '2', '3'], it.peek, 3) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1', '2', '3', it.sentinel], it.peek, 4) + self.assertTrueTwice(it.has_next) + self.assertEqual('1', it.next()) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['2', '3'], it.peek, 2) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['2', '3', it.sentinel], it.peek, 3) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['2', '3', it.sentinel, it.sentinel], it.peek, 4) + self.assertTrueTwice(it.has_next) + + def test_peek_with_none(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice(it.sentinel, it.peek) + self.assertFalseTwice(it.has_next) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice('1', it.peek) + self.assertEqual('1', it.next()) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice(it.sentinel, it.peek) + self.assertFalseTwice(it.has_next) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice('1', it.peek) + self.assertEqual('1', it.next()) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice('2', it.peek) + self.assertEqual('2', it.next()) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice(it.sentinel, it.peek) + self.assertFalseTwice(it.has_next) + + def test_peek_with_one(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice([it.sentinel], it.peek, 1) + self.assertFalseTwice(it.has_next) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1'], it.peek, 1) + self.assertEqual('1', it.next()) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice([it.sentinel], it.peek, 1) + self.assertFalseTwice(it.has_next) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['1'], it.peek, 1) + self.assertEqual('1', it.next()) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice(['2'], it.peek, 1) + self.assertEqual('2', it.next()) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice([it.sentinel], it.peek, 1) + self.assertFalseTwice(it.has_next) + + def test_peek_with_zero(self): + a = [] + it = peek_iter(a) + self.assertFalseTwice(it.has_next) + self.assertEqualTwice([], it.peek, 0) + + a = ['1'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.peek, 0) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.peek, 0) + + a = ['1', '2'] + it = peek_iter(a) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.peek, 0) + self.assertTrueTwice(it.has_next) + self.assertEqualTwice([], it.peek, 0) + + +class ModifyIterTest(BaseIteratorsTest): + def test_init_with_sentinel_args(self): + a = iter(['1', '2', '3', 'DONE']) + sentinel = 'DONE' + + def get_next(): + return next(a) + it = modify_iter(get_next, sentinel, int) + expected = [1, 2, 3] + self.assertEqual(expected, [i for i in it]) + + def test_init_with_sentinel_kwargs(self): + a = iter([1, 2, 3, 4]) + sentinel = 4 + + def get_next(): + return next(a) + it = modify_iter(get_next, sentinel, modifier=str) + expected = ['1', '2', '3'] + self.assertEqual(expected, [i for i in it]) + + def test_modifier_default(self): + a = ['', ' ', ' a ', 'b ', ' c', ' ', ''] + it = modify_iter(a) + expected = ['', ' ', ' a ', 'b ', ' c', ' ', ''] + self.assertEqual(expected, [i for i in it]) + + def test_modifier_not_callable(self): + self.assertRaises(TypeError, modify_iter, [1], modifier='not_callable') + + def test_modifier_rstrip(self): + a = ['', ' ', ' a ', 'b ', ' c', ' ', ''] + it = modify_iter(a, modifier=lambda s: s.rstrip()) + expected = ['', '', ' a', 'b', ' c', '', ''] + self.assertEqual(expected, [i for i in it]) + + def test_modifier_rstrip_unicode(self): + a = [u'', u' ', u' a ', u'b ', u' c', u' ', u''] + it = modify_iter(a, modifier=lambda s: s.rstrip()) + expected = [u'', u'', u' a', u'b', u' c', u'', u''] + self.assertEqual(expected, [i for i in it]) @@ -3,6 +3,7 @@ envlist=py26,py27,py32,py33,pypy,du11,du10,du09,du08,du07 [testenv] deps= + mock nose sqlalchemy whoosh |
