diff --git a/AUTHORS b/AUTHORS index 24897985e..8c225990d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,6 +21,7 @@ Other contributors, listed alphabetically, are: * Henrique Bastos -- SVG support for graphviz extension * Daniel Bültmann -- todo extension * Jean-François Burnol -- LaTeX improvements +* Marco Buttu -- doctest extension (pyversion option) * Etienne Desautels -- apidoc module * Michael Droettboom -- inheritance_diagram extension * Charles Duffy -- original graphviz extension diff --git a/CHANGES b/CHANGES index 493e35c55..2eeb0fb33 100644 --- a/CHANGES +++ b/CHANGES @@ -43,6 +43,7 @@ Features added * C++, add ``:tparam-line-spec:`` option to templated declarations. 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 +* #3303: Add ``:pyversion:`` option to the doctest directive. Bugs fixed ---------- diff --git a/doc/ext/doctest.rst b/doc/ext/doctest.rst index 818b86007..d1cb3c31d 100644 --- a/doc/ext/doctest.rst +++ b/doc/ext/doctest.rst @@ -63,7 +63,7 @@ a comma-separated list of group names. default set of flags is specified by the :confval:`doctest_default_flags` configuration variable. - This directive supports two options: + This directive supports three options: * ``hide``, a flag option, hides the doctest block in other builders. By 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 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 + `__. + + .. versionadded:: 1.6 + Note that like with standard doctests, you have to use ```` to signal a blank line in the expected output. The ```` is removed when building presentation output (HTML, LaTeX etc.). diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py index c355415f7..cd6397fb1 100644 --- a/sphinx/ext/doctest.py +++ b/sphinx/ext/doctest.py @@ -15,10 +15,12 @@ import re import sys import time import codecs +import platform from os import path import doctest from six import itervalues, StringIO, binary_type, text_type, PY2 +from distutils.version import LooseVersion from docutils import nodes 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.console import bold # type: ignore from sphinx.util.osutil import fs_encoding +from sphinx.locale import _ if False: # For type annotation @@ -54,6 +57,29 @@ else: 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 class TestDirective(Directive): @@ -101,12 +127,32 @@ class TestDirective(Directive): # parse doctest-like output comparison flags option_strings = self.options['options'].replace(',', ' ').split() for option in option_strings: - if (option[0] not in '+-' or option[1:] not in - doctest.OPTIONFLAGS_BY_NAME): # type: ignore - # XXX warn? + prefix, option_name = option[0], option[1:] + if prefix not in '+-': # type: ignore + 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 flag = doctest.OPTIONFLAGS_BY_NAME[option[1:]] # type: ignore 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] @@ -122,12 +168,14 @@ class DoctestDirective(TestDirective): option_spec = { 'hide': directives.flag, 'options': directives.unchanged, + 'pyversion': directives.unchanged_required, } class TestcodeDirective(TestDirective): option_spec = { 'hide': directives.flag, + 'pyversion': directives.unchanged_required, } @@ -135,6 +183,7 @@ class TestoutputDirective(TestDirective): option_spec = { 'hide': directives.flag, 'options': directives.unchanged, + 'pyversion': directives.unchanged_required, } diff --git a/tests/roots/test-doctest/doctest.txt b/tests/roots/test-doctest/doctest.txt index 053601f3c..e45bc2721 100644 --- a/tests/roots/test-doctest/doctest.txt +++ b/tests/roots/test-doctest/doctest.txt @@ -69,7 +69,7 @@ Special directives >>> squared(2) 4 -* options for testcode/testoutput blocks +* options for doctest/testcode/testoutput blocks .. testcode:: :hide: @@ -82,6 +82,20 @@ Special directives Output text. + .. doctest:: + :pyversion: >= 2.0 + + >>> a = 3 + >>> a + 3 + + .. doctest:: + :pyversion: < 2.0 + + >>> a = 3 + >>> a + 4 + * grouping .. testsetup:: group1 diff --git a/tests/test_ext_doctest.py b/tests/test_ext_doctest.py index 6b17f2ed7..10f51a133 100644 --- a/tests/test_ext_doctest.py +++ b/tests/test_ext_doctest.py @@ -9,6 +9,7 @@ :license: BSD, see LICENSE for details. """ import pytest +from sphinx.ext.doctest import compare_version cleanup_called = 0 @@ -25,6 +26,21 @@ def test_build(app, status, warning): 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(): global cleanup_called cleanup_called += 1