Fix #4183: doctest: `:pyversion:` option also follows PEP-440 specification

This commit is contained in:
Takeshi KOMIYA 2018-01-08 11:59:50 +09:00
parent e015ce2a0f
commit 7ba54500fc
5 changed files with 63 additions and 41 deletions

View File

@ -1,6 +1,11 @@
Release 1.7 (in development) Release 1.7 (in development)
============================ ============================
Dependencies
------------
* Add ``packaging`` package
Incompatible changes Incompatible changes
-------------------- --------------------
@ -57,7 +62,7 @@ Features added
* #947: autodoc now supports ignore-module-all to ignore a module's ``__all__`` * #947: autodoc now supports ignore-module-all to ignore a module's ``__all__``
* #4332: Let LaTeX obey :confval:`math_numfig` for equation numbering * #4332: Let LaTeX obey :confval:`math_numfig` for equation numbering
* #4093: sphinx-build creates empty directories for unknown targets/builders * #4093: sphinx-build creates empty directories for unknown targets/builders
* #4183: doctest: ``:pyversion:`` option also follows PEP-440 specification
Features removed Features removed
---------------- ----------------

View File

@ -80,12 +80,24 @@ a comma-separated list of group names.
.. doctest:: .. doctest::
:pyversion: > 3.3 :pyversion: > 3.3
The supported operands are ``<``, ``<=``, ``==``, ``>=``, ``>``, and The following operands are supported:
comparison is performed by `distutils.version.LooseVersion
<https://www.python.org/dev/peps/pep-0386/#distutils>`__. * ``~=``: Compatible release clause
* ``==``: Version matching clause
* ``!=``: Version exclusion clause
* ``<=``, ``>=``: Inclusive ordered comparison clause
* ``<``, ``>``: Exclusive ordered comparison clause
* ``===``: Arbitrary equality clause.
``pyversion`` option is followed `PEP-440: Version Specifiers
<https://www.python.org/dev/peps/pep-0440/#version-specifiers>`__.
.. versionadded:: 1.6 .. versionadded:: 1.6
.. versionchanged:: 1.7
Supported PEP-440 operands and notations
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

@ -26,6 +26,7 @@ requires = [
'imagesize', 'imagesize',
'requests>=2.0.0', 'requests>=2.0.0',
'setuptools', 'setuptools',
'packaging',
'sphinxcontrib-websupport', 'sphinxcontrib-websupport',
] ]

View File

@ -20,7 +20,8 @@ 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 packaging.specifiers import SpecifierSet, InvalidSpecifier
from packaging.version import Version
from docutils import nodes from docutils import nodes
from docutils.parsers.rst import Directive, directives from docutils.parsers.rst import Directive, directives
@ -57,28 +58,23 @@ else:
return text return text
def compare_version(ver1, ver2, operand): def is_allowed_version(spec, version):
# type: (unicode, unicode, unicode) -> bool # type: (unicode, unicode) -> bool
"""Compare `ver1` to `ver2`, relying on `operand`. """Check `spec` satisfies `version` or not.
This obeys PEP-440 specifiers:
https://www.python.org/dev/peps/pep-0440/#version-specifiers
Some examples: Some examples:
>>> compare_version('3.3', '3.5', '<=') >>> is_allowed_version('3.3', '<=3.5')
True True
>>> compare_version('3.3', '3.2', '<=') >>> is_allowed_version('3.3', '<=3.2')
False False
>>> compare_version('3.3a0', '3.3', '<=') >>> is_allowed_version('3.3', '>3.2, <4.0')
True True
""" """
if operand not in ('<=', '<', '==', '>=', '>'): return Version(version) in SpecifierSet(spec)
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
@ -143,16 +139,13 @@ class TestDirective(Directive):
node['options'][flag] = (option[0] == '+') node['options'][flag] = (option[0] == '+')
if self.name == 'doctest' and 'pyversion' in self.options: if self.name == 'doctest' and 'pyversion' in self.options:
try: try:
option = self.options['pyversion'] spec = self.options['pyversion']
# :pyversion: >= 3.6 --> operand='>=', option_version='3.6' if not is_allowed_version(spec, platform.python_version()):
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'] flag = doctest.OPTIONFLAGS_BY_NAME['SKIP']
node['options'][flag] = True # Skip the test node['options'][flag] = True # Skip the test
except ValueError: except InvalidSpecifier:
self.state.document.reporter.warning( self.state.document.reporter.warning(
_("'%s' is not a valid pyversion option") % option, _("'%s' is not a valid pyversion option") % spec,
line=self.lineno) line=self.lineno)
return [node] return [node]

View File

@ -9,7 +9,9 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import pytest import pytest
from sphinx.ext.doctest import compare_version from sphinx.ext.doctest import is_allowed_version
from packaging.version import InvalidVersion
from packaging.specifiers import InvalidSpecifier
cleanup_called = 0 cleanup_called = 0
@ -26,19 +28,28 @@ 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(): def test_is_allowed_version():
assert compare_version('3.3', '3.4', '<') is True assert is_allowed_version('<3.4', '3.3') is True
assert compare_version('3.3', '3.2', '<') is False assert is_allowed_version('<3.4', '3.3') is True
assert compare_version('3.3', '3.4', '<=') is True assert is_allowed_version('<3.2', '3.3') is False
assert compare_version('3.3', '3.2', '<=') is False assert is_allowed_version('<=3.4', '3.3') is True
assert compare_version('3.3', '3.3', '==') is True assert is_allowed_version('<=3.2', '3.3') is False
assert compare_version('3.3', '3.4', '==') is False assert is_allowed_version('==3.3', '3.3') is True
assert compare_version('3.3', '3.2', '>=') is True assert is_allowed_version('==3.4', '3.3') is False
assert compare_version('3.3', '3.4', '>=') is False assert is_allowed_version('>=3.2', '3.3') is True
assert compare_version('3.3', '3.2', '>') is True assert is_allowed_version('>=3.4', '3.3') is False
assert compare_version('3.3', '3.4', '>') is False assert is_allowed_version('>3.2', '3.3') is True
with pytest.raises(ValueError): assert is_allowed_version('>3.4', '3.3') is False
compare_version('3.3', '3.4', '+') assert is_allowed_version('~=3.4', '3.4.5') is True
assert is_allowed_version('~=3.4', '3.5.0') is True
# invalid spec
with pytest.raises(InvalidSpecifier):
is_allowed_version('&3.4', '3.5')
# invalid version
with pytest.raises(InvalidVersion):
is_allowed_version('>3.4', 'Sphinx')
def cleanup_call(): def cleanup_call():