Merge branch '1.7'

This commit is contained in:
Takeshi KOMIYA 2018-07-17 00:37:40 +09:00
commit 3e57ea0a52
19 changed files with 196 additions and 101 deletions

23
CHANGES
View File

@ -181,7 +181,7 @@ Documentation
* #5083: Fix wrong make.bat option for internationalization.
* #5115: napoleon: add admonitions added by #4613 to the docs.
Release 1.7.6 (in development)
Release 1.7.7 (in development)
==============================
Dependencies
@ -199,6 +199,15 @@ Features added
Bugs fixed
----------
Testing
--------
Release 1.7.6 (released Jul 17, 2018)
=====================================
Bugs fixed
----------
* #5037: LaTeX ``\sphinxupquote{}`` breaks in Russian
* sphinx.testing uses deprecated pytest API; ``Node.get_marker(name)``
* #5016: crashed when recommonmark.AutoStrictify is enabled
@ -216,6 +225,7 @@ Bugs fixed
* #5091: latex: curly braces in index entries are not handled correctly
* #5070: epub: Wrong internal href fragment links
* #5104: apidoc: Interface of ``sphinx.apidoc:main()`` has changed
* #4272: PDF builds of French projects have issues with XeTeX
* #5076: napoleon raises RuntimeError with python 3.7
* #5125: sphinx-build: Interface of ``sphinx:main()`` has changed
* sphinx-build: ``sphinx.cmd.build.main()`` refers ``sys.argv`` instead of given
@ -225,9 +235,14 @@ Bugs fixed
* autosummary: warnings of autosummary indicates wrong location (refs: #5146)
* #5143: autodoc: crashed on inspecting dict like object which does not support
sorting
Testing
--------
* #5139: autodoc: Enum argument missing if it shares value with another
* #4946: py domain: rtype field could not handle "None" as a type
* #5176: LaTeX: indexing of terms containing ``@``, ``!``, or ``"`` fails
* #5161: html: crashes if copying static files are failed
* #5167: autodoc: Fix formatting type annotations for tuples with more than two
arguments
* #3329: i18n: crashed by auto-symbol footnote references
* #5158: autosummary: module summary has been broken when it starts with heading
Release 1.7.5 (released May 29, 2018)
=====================================

View File

@ -31,7 +31,7 @@ directory = sphinx/locale/
[flake8]
max-line-length = 95
ignore = E116,E241,E251,E741,I101
ignore = E116,E241,E251,E741,W504,I101
exclude = .git,.tox,.venv,tests/*,build/*,doc/_build/*,sphinx/search/*,doc/usage/extensions/example*.py
application-import-names = sphinx
import-order-style = smarkets

View File

@ -848,85 +848,94 @@ class StandaloneHTMLBuilder(Builder):
try:
copyfile(path.join(self.srcdir, src),
path.join(self.outdir, '_downloads', dest))
except Exception as err:
except EnvironmentError as err:
logger.warning(__('cannot copy downloadable file %r: %s'),
path.join(self.srcdir, src), err)
def copy_static_files(self):
# type: () -> None
# copy static files
logger.info(bold(__('copying static files... ')), nonl=True)
ensuredir(path.join(self.outdir, '_static'))
# first, create pygments style file
with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f:
f.write(self.highlighter.get_stylesheet()) # type: ignore
# then, copy translations JavaScript file
if self.config.language is not None:
jsfile = self._get_translations_js()
if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static',
'translations.js'))
try:
# copy static files
logger.info(bold(__('copying static files... ')), nonl=True)
ensuredir(path.join(self.outdir, '_static'))
# first, create pygments style file
with open(path.join(self.outdir, '_static', 'pygments.css'), 'w') as f:
f.write(self.highlighter.get_stylesheet()) # type: ignore
# then, copy translations JavaScript file
if self.config.language is not None:
jsfile = self._get_translations_js()
if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static',
'translations.js'))
# copy non-minified stemmer JavaScript file
if self.indexer is not None:
jsfile = self.indexer.get_js_stemmer_rawcode()
if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js'))
# copy non-minified stemmer JavaScript file
if self.indexer is not None:
jsfile = self.indexer.get_js_stemmer_rawcode()
if jsfile:
copyfile(jsfile, path.join(self.outdir, '_static', '_stemmer.js'))
ctx = self.globalcontext.copy()
ctx = self.globalcontext.copy()
# add context items for search function used in searchtools.js_t
if self.indexer is not None:
ctx.update(self.indexer.context_for_searchtool())
# add context items for search function used in searchtools.js_t
if self.indexer is not None:
ctx.update(self.indexer.context_for_searchtool())
# then, copy over theme-supplied static files
if self.theme:
for theme_path in self.theme.get_theme_dirs()[::-1]:
entry = path.join(theme_path, 'static')
copy_asset(entry, path.join(self.outdir, '_static'), excluded=DOTFILES,
# then, copy over theme-supplied static files
if self.theme:
for theme_path in self.theme.get_theme_dirs()[::-1]:
entry = path.join(theme_path, 'static')
copy_asset(entry, path.join(self.outdir, '_static'), excluded=DOTFILES,
context=ctx, renderer=self.templates)
# then, copy over all user-supplied static files
excluded = Matcher(self.config.exclude_patterns + ["**/.*"])
for static_path in self.config.html_static_path:
entry = path.join(self.confdir, static_path)
if not path.exists(entry):
logger.warning(__('html_static_path entry %r does not exist'), entry)
continue
copy_asset(entry, path.join(self.outdir, '_static'), excluded,
context=ctx, renderer=self.templates)
# then, copy over all user-supplied static files
excluded = Matcher(self.config.exclude_patterns + ["**/.*"])
for static_path in self.config.html_static_path:
entry = path.join(self.confdir, static_path)
if not path.exists(entry):
logger.warning(__('html_static_path entry %r does not exist'), entry)
continue
copy_asset(entry, path.join(self.outdir, '_static'), excluded,
context=ctx, renderer=self.templates)
# copy logo and favicon files if not already in static path
if self.config.html_logo:
logobase = path.basename(self.config.html_logo)
logotarget = path.join(self.outdir, '_static', logobase)
if not path.isfile(path.join(self.confdir, self.config.html_logo)):
logger.warning(__('logo file %r does not exist'), self.config.html_logo)
elif not path.isfile(logotarget):
copyfile(path.join(self.confdir, self.config.html_logo),
logotarget)
if self.config.html_favicon:
iconbase = path.basename(self.config.html_favicon)
icontarget = path.join(self.outdir, '_static', iconbase)
if not path.isfile(path.join(self.confdir, self.config.html_favicon)):
logger.warning(__('favicon file %r does not exist'), self.config.html_favicon)
elif not path.isfile(icontarget):
copyfile(path.join(self.confdir, self.config.html_favicon),
icontarget)
logger.info('done')
# copy logo and favicon files if not already in static path
if self.config.html_logo:
logobase = path.basename(self.config.html_logo)
logotarget = path.join(self.outdir, '_static', logobase)
if not path.isfile(path.join(self.confdir, self.config.html_logo)):
logger.warning(__('logo file %r does not exist'), self.config.html_logo)
elif not path.isfile(logotarget):
copyfile(path.join(self.confdir, self.config.html_logo),
logotarget)
if self.config.html_favicon:
iconbase = path.basename(self.config.html_favicon)
icontarget = path.join(self.outdir, '_static', iconbase)
if not path.isfile(path.join(self.confdir, self.config.html_favicon)):
logger.warning(__('favicon file %r does not exist'),
self.config.html_favicon)
elif not path.isfile(icontarget):
copyfile(path.join(self.confdir, self.config.html_favicon),
icontarget)
logger.info('done')
except EnvironmentError as err:
# TODO: In py3, EnvironmentError (and IOError) was merged into OSError.
# So it should be replaced by IOError on dropping py2 support
logger.warning(__('cannot copy static file %r'), err)
def copy_extra_files(self):
# type: () -> None
# copy html_extra_path files
logger.info(bold(__('copying extra files... ')), nonl=True)
excluded = Matcher(self.config.exclude_patterns)
try:
# copy html_extra_path files
logger.info(bold(__('copying extra files... ')), nonl=True)
excluded = Matcher(self.config.exclude_patterns)
for extra_path in self.config.html_extra_path:
entry = path.join(self.confdir, extra_path)
if not path.exists(entry):
logger.warning(__('html_extra_path entry %r does not exist'), entry)
continue
for extra_path in self.config.html_extra_path:
entry = path.join(self.confdir, extra_path)
if not path.exists(entry):
logger.warning(__('html_extra_path entry %r does not exist'), entry)
continue
copy_asset(entry, self.outdir, excluded)
logger.info(__('done'))
copy_asset(entry, self.outdir, excluded)
logger.info(__('done'))
except EnvironmentError as err:
logger.warning(__('cannot copy extra file %r'), err)
def write_buildinfo(self):
# type: () -> None

View File

@ -160,7 +160,7 @@ class CheckExternalLinksBuilder(Builder):
# the server and the network
response = requests.head(req_url, config=self.app.config, **kwargs)
response.raise_for_status()
except HTTPError as err:
except HTTPError:
# retry with GET request if that fails, some servers
# don't like HEAD requests.
response = requests.get(req_url, stream=True, config=self.app.config,

View File

@ -168,7 +168,15 @@ class PyXrefMixin(object):
class PyField(PyXrefMixin, Field):
pass
def make_xref(self, rolename, domain, target,
innernode=nodes.emphasis, contnode=None, env=None):
# type: (unicode, unicode, unicode, nodes.Node, nodes.Node, BuildEnvironment) -> nodes.Node # NOQA
if rolename == 'class' and target == 'None':
# None is not a type, so use obj role instead.
rolename = 'obj'
return super(PyField, self).make_xref(rolename, domain, target,
innernode, contnode, env)
class PyGroupedField(PyXrefMixin, GroupedField):

View File

@ -221,12 +221,21 @@ def get_object_members(subject, objpath, attrgetter, analyzer=None):
for name, value in subject.__members__.items():
obj_dict[name] = value
members = {}
members = {} # type: Dict[str, Attribute]
# enum members
if isenumclass(subject):
for name, value in subject.__members__.items():
if name not in members:
members[name] = Attribute(name, True, value)
# other members
for name in dir(subject):
try:
value = attrgetter(subject, name)
directly_defined = name in obj_dict
members[name] = Attribute(name, directly_defined, value)
if name not in members:
members[name] = Attribute(name, directly_defined, value)
except AttributeError:
continue

View File

@ -473,21 +473,32 @@ def extract_summary(doc, document):
doc = doc[:i]
break
# Try to find the "first sentence", which may span multiple lines
sentences = periods_re.split(" ".join(doc)) # type: ignore
if len(sentences) == 1:
summary = sentences[0].strip()
if doc == []:
return ''
# parse the docstring
state_machine = RSTStateMachine(state_classes, 'Body')
node = new_document('', document.settings)
node.reporter = NullReporter()
state_machine.run(doc, node)
if not isinstance(node[0], nodes.paragraph):
# document starts with non-paragraph: pick up the first line
summary = doc[0].strip()
else:
summary = ''
state_machine = RSTStateMachine(state_classes, 'Body')
while sentences:
summary += sentences.pop(0) + '.'
node = new_document('', document.settings)
node.reporter = NullReporter()
state_machine.run([summary], node)
if not node.traverse(nodes.system_message):
# considered as that splitting by period does not break inline markups
break
# Try to find the "first sentence", which may span multiple lines
sentences = periods_re.split(" ".join(doc)) # type: ignore
if len(sentences) == 1:
summary = sentences[0].strip()
else:
summary = ''
while sentences:
summary += sentences.pop(0) + '.'
node[:] = []
state_machine.run([summary], node)
if not node.traverse(nodes.system_message):
# considered as that splitting by period does not break inline markups
break
# strip literal notation mark ``::`` from tail of summary
summary = literal_re.sub('.', summary)

View File

@ -269,7 +269,7 @@ def find_autosummary_in_docstring(name, module=None, filename=None):
pass
except ImportError as e:
print("Failed to import '%s': %s" % (name, e))
except SystemExit as e:
except SystemExit:
print("Failed to import '%s'; the module executes module level "
"statement and it might call sys.exit()." % name)
return []

View File

@ -276,10 +276,9 @@ class Locale(SphinxTransform):
continue # skip
# auto-numbered foot note reference should use original 'ids'.
def is_autonumber_footnote_ref(node):
def is_autofootnote_ref(node):
# type: (nodes.Node) -> bool
return isinstance(node, nodes.footnote_reference) and \
node.get('auto') == 1
return isinstance(node, nodes.footnote_reference) and node.get('auto')
def list_replace_or_append(lst, old, new):
# type: (List, Any, Any) -> None
@ -287,8 +286,8 @@ class Locale(SphinxTransform):
lst[lst.index(old)] = new
else:
lst.append(new)
old_foot_refs = node.traverse(is_autonumber_footnote_ref)
new_foot_refs = patch.traverse(is_autonumber_footnote_ref)
old_foot_refs = node.traverse(is_autofootnote_ref)
new_foot_refs = patch.traverse(is_autofootnote_ref)
if len(old_foot_refs) != len(new_foot_refs):
old_foot_ref_rawsources = [ref.rawsource for ref in old_foot_refs]
new_foot_ref_rawsources = [ref.rawsource for ref in new_foot_refs]
@ -309,8 +308,14 @@ class Locale(SphinxTransform):
new['ids'] = old['ids']
for id in new['ids']:
self.document.ids[id] = new
list_replace_or_append(
self.document.autofootnote_refs, old, new)
if new['auto'] == 1:
# autofootnote_refs
list_replace_or_append(self.document.autofootnote_refs, old, new)
else:
# symbol_footnote_refs
list_replace_or_append(self.document.symbol_footnote_refs, old, new)
if refname:
list_replace_or_append(
self.document.footnote_refs.setdefault(refname, []),

View File

@ -532,6 +532,13 @@ class Signature(object):
if annotation.__module__ == 'builtins':
return annotation.__qualname__ # type: ignore
elif (hasattr(typing, 'TupleMeta') and
isinstance(annotation, typing.TupleMeta) and # type: ignore
not hasattr(annotation, '__tuple_params__')):
# This is for Python 3.6+, 3.5 case is handled below
params = annotation.__args__
param_str = ', '.join(self.format_annotation(p) for p in params)
return '%s[%s]' % (qualified_name, param_str)
elif (hasattr(typing, 'GenericMeta') and # for py36 or below
isinstance(annotation, typing.GenericMeta)):
# In Python 3.5.2+, all arguments are stored in __args__,

View File

@ -485,6 +485,14 @@ class LaTeXTranslator(nodes.NodeVisitor):
# sort out some elements
self.elements = DEFAULT_SETTINGS.copy()
self.elements.update(ADDITIONAL_SETTINGS.get(builder.config.latex_engine, {}))
# for xelatex+French, don't use polyglossia
if self.elements['latex_engine'] == 'xelatex':
if builder.config.language:
if builder.config.language[:2] == 'fr':
self.elements.update({
'polyglossia': '',
'babel': '\\usepackage{babel}',
})
# allow the user to override them all
self.check_latex_elements()
self.elements.update(builder.config.latex_elements)

View File

@ -233,3 +233,4 @@ class EnumCls(enum.Enum):
val2 = 23 #: doc for val2
val3 = 34
"""doc for val3"""
val4 = 34

View File

@ -19,8 +19,8 @@ msgstr ""
msgid "i18n with Footnote"
msgstr "I18N WITH FOOTNOTE"
msgid "[100]_ Contents [#]_ for `i18n with Footnote`_ [ref]_ [#named]_."
msgstr "`I18N WITH FOOTNOTE`_ INCLUDE THIS CONTENTS [#named]_ [ref]_ [#]_ [100]_."
msgid "[100]_ Contents [#]_ for `i18n with Footnote`_ [ref]_ [#named]_ [*]_."
msgstr "`I18N WITH FOOTNOTE`_ INCLUDE THIS CONTENTS [#named]_ [ref]_ [#]_ [100]_ [*]_."
msgid "This is a auto numbered footnote."
msgstr "THIS IS A AUTO NUMBERED FOOTNOTE."
@ -34,3 +34,5 @@ msgstr "THIS IS A NUMBERED FOOTNOTE."
msgid "This is a auto numbered named footnote."
msgstr "THIS IS A AUTO NUMBERED NAMED FOOTNOTE."
msgid "This is a auto symbol footnote."
msgstr "THIS IS A AUTO SYMBOL FOOTNOTE."

View File

@ -4,9 +4,10 @@ i18n with Footnote
==================
.. #955 cant-build-html-with-footnotes-when-using
[100]_ Contents [#]_ for `i18n with Footnote`_ [ref]_ [#named]_.
[100]_ Contents [#]_ for `i18n with Footnote`_ [ref]_ [#named]_ [*]_.
.. [#] This is a auto numbered footnote.
.. [ref] This is a named footnote.
.. [100] This is a numbered footnote.
.. [#named] This is a auto numbered named footnote.
.. [*] This is a auto symbol footnote.

View File

@ -873,13 +873,14 @@ def test_generate():
# test members with enum attributes
directive.env.ref_context['py:module'] = 'target'
options.inherited_members = False
options.undoc_members = False
options.undoc_members = True
options.members = ALL
assert_processes([
('class', 'target.EnumCls'),
('attribute', 'target.EnumCls.val1'),
('attribute', 'target.EnumCls.val2'),
('attribute', 'target.EnumCls.val3'),
('attribute', 'target.EnumCls.val4'),
], 'class', 'EnumCls')
assert_result_contains(
' :annotation: = 12', 'attribute', 'EnumCls.val1')

View File

@ -85,6 +85,11 @@ def test_extract_summary(capsys):
doc = ['blah blah::']
assert extract_summary(doc, document) == 'blah blah.'
# heading
doc = ['blah blah',
'=========']
assert extract_summary(doc, document) == 'blah blah'
_, err = capsys.readouterr()
assert err == ''

View File

@ -794,7 +794,7 @@ def test_xml_footnotes(app, warning):
assert_elem(
para0[0],
['I18N WITH FOOTNOTE', 'INCLUDE THIS CONTENTS',
'2', '[ref]', '1', '100', '.'],
'2', '[ref]', '1', '100', '*', '.'],
['i18n-with-footnote', 'ref'])
footnote0 = secs[0].findall('footnote')
@ -813,6 +813,11 @@ def test_xml_footnotes(app, warning):
['2', 'THIS IS A AUTO NUMBERED NAMED FOOTNOTE.'],
None,
['named'])
assert_elem(
footnote0[3],
['*', 'THIS IS A AUTO SYMBOL FOOTNOTE.'],
None,
None)
citation0 = secs[0].findall('citation')
assert_elem(

View File

@ -231,7 +231,7 @@ def test_Signature_partialmethod():
@pytest.mark.skipif(sys.version_info < (3, 5),
reason='type annotation test is available on py35 or above')
def test_Signature_annotations():
from typing_test_data import f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11
from typing_test_data import f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12
# Class annotations
sig = inspect.Signature(f0).format_args()
@ -284,6 +284,10 @@ def test_Signature_annotations():
sig = inspect.Signature(f11, has_retval=False).format_args()
assert sig == '(x: CustomAnnotation, y: 123)'
# tuple with more than two items
sig = inspect.Signature(f12).format_args()
assert sig == '() -> Tuple[int, str, int]'
def test_safe_getattr_with_default():
class Foo(object):

View File

@ -62,3 +62,7 @@ class CustomAnnotation:
def f11(x: CustomAnnotation(), y: 123) -> None:
pass
def f12() -> Tuple[int, str, int]:
pass