Merge branch 'master' into 3570_drop_typing_from_typehints

This commit is contained in:
Takeshi KOMIYA 2018-01-13 14:43:31 +09:00 committed by GitHub
commit c50b21af0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 141 additions and 44 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
-------------------- --------------------
@ -67,6 +72,8 @@ Features added
* #4093: sphinx-build creates empty directories for unknown targets/builders * #4093: sphinx-build creates empty directories for unknown targets/builders
* Add ``top-classes`` option for the ``sphinx.ext.inheritance_diagram`` * Add ``top-classes`` option for the ``sphinx.ext.inheritance_diagram``
extension to limit the scope of inheritance graphs. extension to limit the scope of inheritance graphs.
* #4183: doctest: ``:pyversion:`` option also follows PEP-440 specification
* #4235: html: Add :confval:`manpages_url` to make manpage roles to hyperlinks
* #3570: autodoc: Do not display 'typing.' module for type hints * #3570: autodoc: Do not display 'typing.' module for type hints
Features removed Features removed

View File

@ -293,6 +293,24 @@ General configuration
.. versionadded:: 1.3 .. versionadded:: 1.3
.. confval:: manpages_url
A URL to cross-reference :rst:role:`manpage` directives. If this is
defined to ``https://manpages.debian.org/{path}``, the
:literal:`:manpage:`man(1)`` role will like to
<https://manpages.debian.org/man(1)>. The patterns available are:
* ``page`` - the manual page (``man``)
* ``section`` - the manual section (``1``)
* ``path`` - the original manual page and section specified (``man(1)``)
This also supports manpages specified as ``man.1``.
.. note:: This currently affects only HTML writers but could be
expanded in the future.
.. versionadded:: 1.7
.. confval:: nitpicky .. confval:: nitpicky
If true, Sphinx will warn about *all* references where the target cannot be If true, Sphinx will warn about *all* references where the target cannot be

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

@ -355,7 +355,8 @@ in a different style:
.. rst:role:: manpage .. rst:role:: manpage
A reference to a Unix manual page including the section, A reference to a Unix manual page including the section,
e.g. ``:manpage:`ls(1)```. e.g. ``:manpage:`ls(1)```. Creates a hyperlink to an external site
rendering the manpage if :confval:`manpages_url` is defined.
.. rst:role:: menuselection .. rst:role:: menuselection

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

@ -125,6 +125,7 @@ class Config(object):
primary_domain = ('py', 'env', [NoneType]), primary_domain = ('py', 'env', [NoneType]),
needs_sphinx = (None, None, string_classes), needs_sphinx = (None, None, string_classes),
needs_extensions = ({}, None), needs_extensions = ({}, None),
manpages_url = (None, 'env'),
nitpicky = (False, None), nitpicky = (False, None),
nitpick_ignore = ([], None), nitpick_ignore = ([], None),
numfig = (False, 'env'), numfig = (False, 'env'),

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

@ -23,7 +23,7 @@ from sphinx.transforms import (
ApplySourceWorkaround, ExtraTranslatableNodes, CitationReferences, ApplySourceWorkaround, ExtraTranslatableNodes, CitationReferences,
DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, SortIds, DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, SortIds,
AutoNumbering, AutoIndexUpgrader, FilterSystemMessages, AutoNumbering, AutoIndexUpgrader, FilterSystemMessages,
UnreferencedFootnotesDetector, SphinxSmartQuotes UnreferencedFootnotesDetector, SphinxSmartQuotes, ManpageLink
) )
from sphinx.transforms.compact_bullet_list import RefOnlyBulletListTransform from sphinx.transforms.compact_bullet_list import RefOnlyBulletListTransform
from sphinx.transforms.i18n import ( from sphinx.transforms.i18n import (
@ -80,7 +80,7 @@ class SphinxStandaloneReader(SphinxBaseReader):
Locale, CitationReferences, DefaultSubstitutions, MoveModuleTargets, Locale, CitationReferences, DefaultSubstitutions, MoveModuleTargets,
HandleCodeBlocks, AutoNumbering, AutoIndexUpgrader, SortIds, HandleCodeBlocks, AutoNumbering, AutoIndexUpgrader, SortIds,
RemoveTranslatableInline, PreserveTranslatableMessages, FilterSystemMessages, RemoveTranslatableInline, PreserveTranslatableMessages, FilterSystemMessages,
RefOnlyBulletListTransform, UnreferencedFootnotesDetector RefOnlyBulletListTransform, UnreferencedFootnotesDetector, ManpageLink
] # type: List[Transform] ] # type: List[Transform]
def __init__(self, app, *args, **kwargs): def __init__(self, app, *args, **kwargs):
@ -110,7 +110,7 @@ class SphinxI18nReader(SphinxBaseReader):
DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks, DefaultSubstitutions, MoveModuleTargets, HandleCodeBlocks,
AutoNumbering, SortIds, RemoveTranslatableInline, AutoNumbering, SortIds, RemoveTranslatableInline,
FilterSystemMessages, RefOnlyBulletListTransform, FilterSystemMessages, RefOnlyBulletListTransform,
UnreferencedFootnotesDetector] UnreferencedFootnotesDetector, ManpageLink]
def set_lineno_for_reporter(self, lineno): def set_lineno_for_reporter(self, lineno):
# type: (int) -> None # type: (int) -> None

View File

@ -9,6 +9,8 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import re
from docutils import nodes from docutils import nodes
from docutils.transforms import Transform, Transformer from docutils.transforms import Transform, Transformer
from docutils.transforms.parts import ContentsFilter from docutils.transforms.parts import ContentsFilter
@ -348,3 +350,21 @@ class SphinxSmartQuotes(SmartQuotes):
for txtnode in txtnodes: for txtnode in txtnodes:
notsmartquotable = not is_smartquotable(txtnode) notsmartquotable = not is_smartquotable(txtnode)
yield (texttype[notsmartquotable], txtnode.astext()) yield (texttype[notsmartquotable], txtnode.astext())
class ManpageLink(SphinxTransform):
"""Find manpage section numbers and names"""
default_priority = 999
def apply(self):
for node in self.document.traverse(addnodes.manpage):
manpage = ' '.join([str(x) for x in node.children
if isinstance(x, nodes.Text)])
pattern = r'^(?P<path>(?P<page>.+)[\(\.](?P<section>[1-9]\w*)?\)?)$' # noqa
info = {'path': manpage,
'page': manpage,
'section': ''}
r = re.match(pattern, manpage)
if r:
info = r.groupdict()
node.attributes.update(info)

View File

@ -79,6 +79,7 @@ class HTMLTranslator(BaseTranslator):
self.highlightopts = builder.config.highlight_options self.highlightopts = builder.config.highlight_options
self.highlightlinenothreshold = sys.maxsize self.highlightlinenothreshold = sys.maxsize
self.docnames = [builder.current_docname] # for singlehtml builder self.docnames = [builder.current_docname] # for singlehtml builder
self.manpages_url = builder.config.manpages_url
self.protect_literal_text = 0 self.protect_literal_text = 0
self.permalink_text = builder.config.html_add_permalinks self.permalink_text = builder.config.html_add_permalinks
# support backwards-compatible setting to a bool # support backwards-compatible setting to a bool
@ -816,9 +817,14 @@ class HTMLTranslator(BaseTranslator):
def visit_manpage(self, node): def visit_manpage(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
self.visit_literal_emphasis(node) self.visit_literal_emphasis(node)
if self.manpages_url:
node['refuri'] = self.manpages_url.format(**node.attributes)
self.visit_reference(node)
def depart_manpage(self, node): def depart_manpage(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
if self.manpages_url:
self.depart_reference(node)
self.depart_literal_emphasis(node) self.depart_literal_emphasis(node)
# overwritten to add even/odd classes # overwritten to add even/odd classes

View File

@ -49,6 +49,7 @@ class HTML5Translator(BaseTranslator):
self.highlightopts = builder.config.highlight_options self.highlightopts = builder.config.highlight_options
self.highlightlinenothreshold = sys.maxsize self.highlightlinenothreshold = sys.maxsize
self.docnames = [builder.current_docname] # for singlehtml builder self.docnames = [builder.current_docname] # for singlehtml builder
self.manpages_url = builder.config.manpages_url
self.protect_literal_text = 0 self.protect_literal_text = 0
self.permalink_text = builder.config.html_add_permalinks self.permalink_text = builder.config.html_add_permalinks
# support backwards-compatible setting to a bool # support backwards-compatible setting to a bool
@ -758,9 +759,14 @@ class HTML5Translator(BaseTranslator):
def visit_manpage(self, node): def visit_manpage(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
self.visit_literal_emphasis(node) self.visit_literal_emphasis(node)
if self.manpages_url:
node['refuri'] = self.manpages_url.format(**dict(node))
self.visit_reference(node)
def depart_manpage(self, node): def depart_manpage(self, node):
# type: (nodes.Node) -> None # type: (nodes.Node) -> None
if self.manpages_url:
self.depart_reference(node)
self.depart_literal_emphasis(node) self.depart_literal_emphasis(node)
# overwritten to add even/odd classes # overwritten to add even/odd classes

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
master_doc = 'index'
html_theme = 'classic'
exclude_patterns = ['_build']

View File

@ -0,0 +1,3 @@
* :manpage:`man(1)`
* :manpage:`ls.1`
* :manpage:`sphinx`

View File

@ -1243,3 +1243,16 @@ def test_html_sidebar(app, status, warning):
assert '<h3>Related Topics</h3>' not in result assert '<h3>Related Topics</h3>' not in result
assert '<h3>This Page</h3>' not in result assert '<h3>This Page</h3>' not in result
assert '<h3>Quick search</h3>' not in result assert '<h3>Quick search</h3>' not in result
@pytest.mark.parametrize('fname,expect', flat_dict({
'index.html': [(".//em/a[@href='https://example.com/man.1']", "", True),
(".//em/a[@href='https://example.com/ls.1']", "", True),
(".//em/a[@href='https://example.com/sphinx.']", "", True)]
}))
@pytest.mark.sphinx('html', testroot='manpage_url', confoverrides={
'manpages_url': 'https://example.com/{page}.{section}'})
@pytest.mark.test_params(shared_result='test_build_html_manpage_url')
def test_html_manpage(app, cached_etree_parse, fname, expect):
app.build()
check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect)

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():