Merge pull request #3339 from marco-buttu/master

Added pyversion option for doctest (issue 3303)
This commit is contained in:
Takeshi KOMIYA 2017-01-28 20:34:30 +09:00 committed by GitHub
commit 0aa6ff3681
6 changed files with 99 additions and 5 deletions

View File

@ -21,6 +21,7 @@ Other contributors, listed alphabetically, are:
* Henrique Bastos -- SVG support for graphviz extension * Henrique Bastos -- SVG support for graphviz extension
* Daniel Bültmann -- todo extension * Daniel Bültmann -- todo extension
* Jean-François Burnol -- LaTeX improvements * Jean-François Burnol -- LaTeX improvements
* Marco Buttu -- doctest extension (pyversion option)
* Etienne Desautels -- apidoc module * Etienne Desautels -- apidoc module
* Michael Droettboom -- inheritance_diagram extension * Michael Droettboom -- inheritance_diagram extension
* Charles Duffy -- original graphviz extension * Charles Duffy -- original graphviz extension

View File

@ -43,6 +43,7 @@ Features added
* C++, add ``:tparam-line-spec:`` option to templated declarations. * C++, add ``:tparam-line-spec:`` option to templated declarations.
When specified, each template parameter will be rendered on a separate line. When specified, each template parameter will be rendered on a separate line.
* #3359: Allow sphinx.js in a user locale dir to override sphinx.js from Sphinx * #3359: Allow sphinx.js in a user locale dir to override sphinx.js from Sphinx
* #3303: Add ``:pyversion:`` option to the doctest directive.
Bugs fixed Bugs fixed
---------- ----------

View File

@ -63,7 +63,7 @@ a comma-separated list of group names.
default set of flags is specified by the :confval:`doctest_default_flags` default set of flags is specified by the :confval:`doctest_default_flags`
configuration variable. configuration variable.
This directive supports two options: This directive supports three options:
* ``hide``, a flag option, hides the doctest block in other builders. By * ``hide``, a flag option, hides the doctest block in other builders. By
default it is shown as a highlighted doctest block. default it is shown as a highlighted doctest block.
@ -73,6 +73,19 @@ a comma-separated list of group names.
explicit flags per example, with doctest comments, but they will show up in explicit flags per example, with doctest comments, but they will show up in
other builders too.) other builders too.)
* ``pyversion``, a string option, can be used to specify the required Python
version for the example to be tested. For instance, in the following case
the example will be tested only for Python versions greather than 3.3::
.. doctest::
:pyversion: > 3.3
The supported operands are ``<``, ``<=``, ``==``, ``>=``, ``>``, and
comparison is performed by `distutils.version.LooseVersion
<https://www.python.org/dev/peps/pep-0386/#distutils>`__.
.. versionadded:: 1.6
Note that like with standard doctests, you have to use ``<BLANKLINE>`` to Note that like with standard doctests, you have to use ``<BLANKLINE>`` to
signal a blank line in the expected output. The ``<BLANKLINE>`` is removed signal a blank line in the expected output. The ``<BLANKLINE>`` is removed
when building presentation output (HTML, LaTeX etc.). when building presentation output (HTML, LaTeX etc.).

View File

@ -15,10 +15,12 @@ import re
import sys import sys
import time import time
import codecs import codecs
import platform
from os import path from os import path
import doctest import doctest
from six import itervalues, StringIO, binary_type, text_type, PY2 from six import itervalues, StringIO, binary_type, text_type, PY2
from distutils.version import LooseVersion
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import Directive, directives from docutils.parsers.rst import Directive, directives
@ -29,6 +31,7 @@ from sphinx.util import force_decode, logging
from sphinx.util.nodes import set_source_info from sphinx.util.nodes import set_source_info
from sphinx.util.console import bold # type: ignore from sphinx.util.console import bold # type: ignore
from sphinx.util.osutil import fs_encoding from sphinx.util.osutil import fs_encoding
from sphinx.locale import _
if False: if False:
# For type annotation # For type annotation
@ -54,6 +57,29 @@ else:
return text return text
def compare_version(ver1, ver2, operand):
"""Compare `ver1` to `ver2`, relying on `operand`.
Some examples:
>>> compare_version('3.3', '3.5', '<=')
True
>>> compare_version('3.3', '3.2', '<=')
False
>>> compare_version('3.3a0', '3.3', '<=')
True
"""
if operand not in ('<=', '<', '==', '>=', '>'):
raise ValueError("'%s' is not a valid operand.")
v1 = LooseVersion(ver1)
v2 = LooseVersion(ver2)
return ((operand == '<=' and (v1 <= v2)) or
(operand == '<' and (v1 < v2)) or
(operand == '==' and (v1 == v2)) or
(operand == '>=' and (v1 >= v2)) or
(operand == '>' and (v1 > v2)))
# set up the necessary directives # set up the necessary directives
class TestDirective(Directive): class TestDirective(Directive):
@ -101,12 +127,32 @@ class TestDirective(Directive):
# parse doctest-like output comparison flags # parse doctest-like output comparison flags
option_strings = self.options['options'].replace(',', ' ').split() option_strings = self.options['options'].replace(',', ' ').split()
for option in option_strings: for option in option_strings:
if (option[0] not in '+-' or option[1:] not in prefix, option_name = option[0], option[1:]
doctest.OPTIONFLAGS_BY_NAME): # type: ignore if prefix not in '+-': # type: ignore
# XXX warn? self.state.document.reporter.warning(
_("missing '+' or '-' in '%s' option.") % option,
line=self.lineno)
continue
if option_name not in doctest.OPTIONFLAGS_BY_NAME: # type: ignore
self.state.document.reporter.warning(
_("'%s' is not a valid option.") % option_name,
line=self.lineno)
continue continue
flag = doctest.OPTIONFLAGS_BY_NAME[option[1:]] # type: ignore flag = doctest.OPTIONFLAGS_BY_NAME[option[1:]] # type: ignore
node['options'][flag] = (option[0] == '+') node['options'][flag] = (option[0] == '+')
if self.name == 'doctest' and 'pyversion' in self.options:
try:
option = self.options['pyversion']
# :pyversion: >= 3.6 --> operand='>=', option_version='3.6'
operand, option_version = [item.strip() for item in option.split()]
running_version = platform.python_version()
if not compare_version(running_version, option_version, operand):
flag = doctest.OPTIONFLAGS_BY_NAME['SKIP']
node['options'][flag] = True # Skip the test
except ValueError:
self.state.document.reporter.warning(
_("'%s' is not a valid pyversion option") % option,
line=self.lineno)
return [node] return [node]
@ -122,12 +168,14 @@ class DoctestDirective(TestDirective):
option_spec = { option_spec = {
'hide': directives.flag, 'hide': directives.flag,
'options': directives.unchanged, 'options': directives.unchanged,
'pyversion': directives.unchanged_required,
} }
class TestcodeDirective(TestDirective): class TestcodeDirective(TestDirective):
option_spec = { option_spec = {
'hide': directives.flag, 'hide': directives.flag,
'pyversion': directives.unchanged_required,
} }
@ -135,6 +183,7 @@ class TestoutputDirective(TestDirective):
option_spec = { option_spec = {
'hide': directives.flag, 'hide': directives.flag,
'options': directives.unchanged, 'options': directives.unchanged,
'pyversion': directives.unchanged_required,
} }

View File

@ -69,7 +69,7 @@ Special directives
>>> squared(2) >>> squared(2)
4 4
* options for testcode/testoutput blocks * options for doctest/testcode/testoutput blocks
.. testcode:: .. testcode::
:hide: :hide:
@ -82,6 +82,20 @@ Special directives
Output text. Output text.
.. doctest::
:pyversion: >= 2.0
>>> a = 3
>>> a
3
.. doctest::
:pyversion: < 2.0
>>> a = 3
>>> a
4
* grouping * grouping
.. testsetup:: group1 .. testsetup:: group1

View File

@ -9,6 +9,7 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import pytest import pytest
from sphinx.ext.doctest import compare_version
cleanup_called = 0 cleanup_called = 0
@ -25,6 +26,21 @@ def test_build(app, status, warning):
assert cleanup_called == 3, 'testcleanup did not get executed enough times' assert cleanup_called == 3, 'testcleanup did not get executed enough times'
def test_compare_version():
assert compare_version('3.3', '3.4', '<') is True
assert compare_version('3.3', '3.2', '<') is False
assert compare_version('3.3', '3.4', '<=') is True
assert compare_version('3.3', '3.2', '<=') is False
assert compare_version('3.3', '3.3', '==') is True
assert compare_version('3.3', '3.4', '==') is False
assert compare_version('3.3', '3.2', '>=') is True
assert compare_version('3.3', '3.4', '>=') is False
assert compare_version('3.3', '3.2', '>') is True
assert compare_version('3.3', '3.4', '>') is False
with pytest.raises(ValueError):
compare_version('3.3', '3.4', '+')
def cleanup_call(): def cleanup_call():
global cleanup_called global cleanup_called
cleanup_called += 1 cleanup_called += 1