Merge branch '3.x'

This commit is contained in:
Takeshi KOMIYA 2020-03-05 23:12:07 +09:00
commit b1400ac421
20 changed files with 319 additions and 104 deletions

View File

@ -7,5 +7,5 @@ jobs:
steps:
- checkout
- run: /python3.6/bin/pip install -U pip setuptools
- run: /python3.6/bin/pip install -U .[test,websupport]
- run: /python3.6/bin/pip install -U .[test]
- run: make test PYTHON=/python3.6/bin/python

25
CHANGES
View File

@ -48,6 +48,10 @@ Incompatible changes
modules have node_id for cross reference
* #7210: js domain: Non intended behavior is removed such as ``parseInt_`` links
to ``.. js:function:: parseInt``
* #6903: rst domain: Internal data structure has changed. Now objects have
node_id for cross reference
* #7229: rst domain: Non intended behavior is removed such as ``numref_`` links
to ``.. rst:role:: numref``
Deprecated
----------
@ -69,6 +73,7 @@ Features added
not to document inherited members of the class and uppers
* #6830: autodoc: consider a member private if docstring contains
``:meta private:`` in info-field-list
* #7165: autodoc: Support Annotated type (PEP-593)
* #6558: glossary: emit a warning for duplicated glossary entry
* #3106: domain: Register hyperlink target for index page automatically
* #6558: std domain: emit a warning for duplicated generic objects
@ -84,6 +89,10 @@ Features added
``no-scaled-link`` class
* #7144: Add CSS class indicating its domain for each desc node
* #7211: latex: Use babel for Chinese document when using XeLaTeX
* #7220: genindex: Show "main" index entries at first
* #7103: linkcheck: writes all links to ``output.json``
* #7025: html search: full text search can be disabled for individual document
using ``:nosearch:`` file-wide metadata
Bugs fixed
----------
@ -101,7 +110,7 @@ Bugs fixed
Testing
--------
Release 2.4.4 (in development)
Release 2.4.5 (in development)
==============================
Dependencies
@ -119,12 +128,18 @@ Features added
Bugs fixed
----------
* #7197: LaTeX: platex cause error to build image directive with target url
* #7223: Sphinx builds has been slower since 2.4.0
Testing
--------
Release 2.4.4 (released Mar 05, 2020)
=====================================
Bugs fixed
----------
* #7197: LaTeX: platex cause error to build image directive with target url
* #7223: Sphinx builds has been slower since 2.4.0
Release 2.4.3 (released Feb 22, 2020)
=====================================
@ -208,8 +223,6 @@ Features added
* #6966: graphviz: Support ``:class:`` option
* #6696: html: ``:scale:`` option of image/figure directive not working for SVG
images (imagesize-1.2.0 or above is required)
* #7025: html search: full text search can be disabled for individual document
using ``:nosearch:`` file-wide metadata
* #6994: imgconverter: Support illustrator file (.ai) to .png conversion
* autodoc: Support Positional-Only Argument separator (PEP-570 compliant)
* autodoc: Support type annotations for variables

View File

@ -59,4 +59,4 @@ At the moment, these metadata fields are recognized:
.. note:: object search is still available even if `nosearch` option is set.
.. versionadded:: 2.4
.. versionadded:: 3.0

View File

@ -62,6 +62,7 @@ markers =
[coverage:run]
branch = True
parallel = True
source = sphinx
[coverage:report]

View File

@ -8,6 +8,7 @@
:license: BSD, see LICENSE for details.
"""
import json
import queue
import re
import socket
@ -90,6 +91,8 @@ class CheckExternalLinksBuilder(Builder):
socket.setdefaulttimeout(5.0)
# create output file
open(path.join(self.outdir, 'output.txt'), 'w').close()
# create JSON output file
open(path.join(self.outdir, 'output.json'), 'w').close()
# create queues and worker threads
self.wqueue = queue.Queue() # type: queue.Queue
@ -225,9 +228,16 @@ class CheckExternalLinksBuilder(Builder):
def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None:
uri, docname, lineno, status, info, code = result
filename = self.env.doc2path(docname, None)
linkstat = dict(filename=filename, lineno=lineno,
status=status, code=code, uri=uri,
info=info)
if status == 'unchecked':
self.write_linkstat(linkstat)
return
if status == 'working' and info == 'old':
self.write_linkstat(linkstat)
return
if lineno:
logger.info('(line %4d) ', lineno, nonl=True)
@ -236,18 +246,22 @@ class CheckExternalLinksBuilder(Builder):
logger.info(darkgray('-ignored- ') + uri + ': ' + info)
else:
logger.info(darkgray('-ignored- ') + uri)
self.write_linkstat(linkstat)
elif status == 'local':
logger.info(darkgray('-local- ') + uri)
self.write_entry('local', docname, lineno, uri)
self.write_entry('local', docname, filename, lineno, uri)
self.write_linkstat(linkstat)
elif status == 'working':
logger.info(darkgreen('ok ') + uri + info)
self.write_linkstat(linkstat)
elif status == 'broken':
self.write_entry('broken', docname, lineno, uri + ': ' + info)
if self.app.quiet or self.app.warningiserror:
logger.warning(__('broken link: %s (%s)'), uri, info,
location=(self.env.doc2path(docname), lineno))
location=(filename, lineno))
else:
logger.info(red('broken ') + uri + red(' - ' + info))
self.write_entry('broken', docname, filename, lineno, uri + ': ' + info)
self.write_linkstat(linkstat)
elif status == 'redirected':
try:
text, color = {
@ -259,9 +273,11 @@ class CheckExternalLinksBuilder(Builder):
}[code]
except KeyError:
text, color = ('with unknown code', purple)
self.write_entry('redirected ' + text, docname, lineno,
uri + ' to ' + info)
linkstat['text'] = text
logger.info(color('redirect ') + uri + color(' - ' + text + ' to ' + info))
self.write_entry('redirected ' + text, docname, filename,
lineno, uri + ' to ' + info)
self.write_linkstat(linkstat)
def get_target_uri(self, docname: str, typ: str = None) -> str:
return ''
@ -301,10 +317,15 @@ class CheckExternalLinksBuilder(Builder):
if self.broken:
self.app.statuscode = 1
def write_entry(self, what: str, docname: str, line: int, uri: str) -> None:
with open(path.join(self.outdir, 'output.txt'), 'a', encoding='utf-8') as output:
output.write("%s:%s: [%s] %s\n" % (self.env.doc2path(docname, None),
line, what, uri))
def write_entry(self, what: str, docname: str, filename: str, line: int,
uri: str) -> None:
with open(path.join(self.outdir, 'output.txt'), 'a') as output:
output.write("%s:%s: [%s] %s\n" % (filename, line, what, uri))
def write_linkstat(self, data: dict) -> None:
with open(path.join(self.outdir, 'output.json'), 'a') as output:
output.write(json.dumps(data))
output.write('\n')
def finish(self) -> None:
for worker in self.workers:

View File

@ -34,24 +34,6 @@ from sphinx.util.nodes import make_id, make_refnode
logger = logging.getLogger(__name__)
def make_old_jsmod_id(modname: str) -> str:
"""Generate old styled node_id for JS modules.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return 'module-' + modname
def make_old_jsobj_id(fullname: str) -> str:
"""Generate old styled node_id for JS objects.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return fullname.replace('$', '_S_')
class JSObject(ObjectDescription):
"""
Description of a JavaScript object.
@ -129,7 +111,7 @@ class JSObject(ObjectDescription):
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = make_old_jsobj_id(fullname)
old_node_id = self.make_old_id(fullname)
if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']:
signode['ids'].append(old_node_id)
@ -211,6 +193,14 @@ class JSObject(ObjectDescription):
self.env.ref_context['js:object'] = (objects[-1] if len(objects) > 0
else None)
def make_old_id(self, fullname: str) -> str:
"""Generate old styled node_id for JS objects.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return fullname.replace('$', '_S_')
class JSCallable(JSObject):
"""Description of a JavaScript function, method or constructor."""
@ -282,7 +272,7 @@ class JSModule(SphinxDirective):
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = make_old_jsmod_id(mod_name)
old_node_id = self.make_old_id(mod_name)
if old_node_id not in self.state.document.ids and old_node_id not in target['ids']:
target['ids'].append(old_node_id)
@ -293,6 +283,14 @@ class JSModule(SphinxDirective):
ret.append(inode)
return ret
def make_old_id(self, modname: str) -> str:
"""Generate old styled node_id for JS modules.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return 'module-' + modname
class JSXRefRole(XRefRole):
def process_link(self, env: BuildEnvironment, refnode: Element,

View File

@ -334,7 +334,7 @@ class PyObject(ObjectDescription):
# it supports to represent optional arguments (ex. "func(foo [, bar])")
_pseudo_parse_arglist(signode, arglist)
except NotImplementedError as exc:
logger.warning(exc)
logger.warning("could not parse arglist (%r): %s", arglist, exc)
_pseudo_parse_arglist(signode, arglist)
else:
if self.needs_arglist():
@ -437,7 +437,7 @@ class PyModulelevel(PyObject):
"""
def run(self) -> List[Node]:
warnings.warn('PyClassmember is deprecated.',
warnings.warn('PyModulelevel is deprecated.',
RemovedInSphinx40Warning)
return super().run()
@ -1034,7 +1034,8 @@ class PythonDomain(Domain):
self.modules[modname] = data
def find_obj(self, env: BuildEnvironment, modname: str, classname: str,
name: str, type: str, searchmode: int = 0) -> List[Tuple[str, Any]]:
name: str, type: str, searchmode: int = 0
) -> List[Tuple[str, Tuple[str, str]]]:
"""Find a Python object for "name", perhaps using the given module
and/or classname. Returns a list of (name, object entry) tuples.
"""
@ -1045,7 +1046,7 @@ class PythonDomain(Domain):
if not name:
return []
matches = [] # type: List[Tuple[str, Any]]
matches = [] # type: List[Tuple[str, Tuple[str, str]]]
newname = None
if searchmode == 1:

View File

@ -25,7 +25,7 @@ from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __
from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode
from sphinx.util.nodes import make_id, make_refnode
logger = logging.getLogger(__name__)
@ -39,23 +39,35 @@ class ReSTMarkup(ObjectDescription):
"""
def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
targetname = self.objtype + '-' + name
if targetname not in self.state.document.ids:
signode['names'].append(targetname)
signode['ids'].append(targetname)
self.state.document.note_explicit_target(signode)
node_id = make_id(self.env, self.state.document, self.objtype, name)
signode['ids'].append(node_id)
domain = cast(ReSTDomain, self.env.get_domain('rst'))
domain.note_object(self.objtype, name, location=signode)
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = self.make_old_id(name)
if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']:
signode['ids'].append(old_node_id)
self.state.document.note_explicit_target(signode)
domain = cast(ReSTDomain, self.env.get_domain('rst'))
domain.note_object(self.objtype, name, node_id, location=signode)
indextext = self.get_index_text(self.objtype, name)
if indextext:
self.indexnode['entries'].append(('single', indextext,
targetname, '', None))
self.indexnode['entries'].append(('single', indextext, node_id, '', None))
def get_index_text(self, objectname: str, name: str) -> str:
return ''
def make_old_id(self, name: str) -> str:
"""Generate old styled node_id for reST markups.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return self.objtype + '-' + name
def parse_directive(d: str) -> Tuple[str, str]:
"""Parse a directive signature.
@ -127,26 +139,37 @@ class ReSTDirectiveOption(ReSTMarkup):
return name
def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
directive_name = self.current_directive
targetname = '-'.join([self.objtype, self.current_directive, name])
if targetname not in self.state.document.ids:
signode['names'].append(targetname)
signode['ids'].append(targetname)
self.state.document.note_explicit_target(signode)
domain = cast(ReSTDomain, self.env.get_domain('rst'))
objname = ':'.join(filter(None, [directive_name, name]))
domain = cast(ReSTDomain, self.env.get_domain('rst'))
domain.note_object(self.objtype, objname, location=signode)
directive_name = self.current_directive
if directive_name:
prefix = '-'.join([self.objtype, directive_name])
objname = ':'.join([directive_name, name])
else:
prefix = self.objtype
objname = name
node_id = make_id(self.env, self.state.document, prefix, name)
signode['ids'].append(node_id)
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = self.make_old_id(name)
if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']:
signode['ids'].append(old_node_id)
self.state.document.note_explicit_target(signode)
domain.note_object(self.objtype, objname, node_id, location=signode)
if directive_name:
key = name[0].upper()
pair = [_('%s (directive)') % directive_name,
_(':%s: (directive option)') % name]
self.indexnode['entries'].append(('pair', '; '.join(pair), targetname, '', key))
self.indexnode['entries'].append(('pair', '; '.join(pair), node_id, '', key))
else:
key = name[0].upper()
text = _(':%s: (directive option)') % name
self.indexnode['entries'].append(('single', text, targetname, '', key))
self.indexnode['entries'].append(('single', text, node_id, '', key))
@property
def current_directive(self) -> str:
@ -156,6 +179,14 @@ class ReSTDirectiveOption(ReSTMarkup):
else:
return ''
def make_old_id(self, name: str) -> str:
"""Generate old styled node_id for directive options.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return '-'.join([self.objtype, self.current_directive, name])
class ReSTRole(ReSTMarkup):
"""
@ -193,37 +224,36 @@ class ReSTDomain(Domain):
} # type: Dict[str, Dict[Tuple[str, str], str]]
@property
def objects(self) -> Dict[Tuple[str, str], str]:
return self.data.setdefault('objects', {}) # (objtype, fullname) -> docname
def objects(self) -> Dict[Tuple[str, str], Tuple[str, str]]:
return self.data.setdefault('objects', {}) # (objtype, fullname) -> (docname, node_id)
def note_object(self, objtype: str, name: str, location: Any = None) -> None:
def note_object(self, objtype: str, name: str, node_id: str, location: Any = None) -> None:
if (objtype, name) in self.objects:
docname = self.objects[objtype, name]
docname, node_id = self.objects[objtype, name]
logger.warning(__('duplicate description of %s %s, other instance in %s') %
(objtype, name, docname), location=location)
self.objects[objtype, name] = self.env.docname
self.objects[objtype, name] = (self.env.docname, node_id)
def clear_doc(self, docname: str) -> None:
for (typ, name), doc in list(self.objects.items()):
for (typ, name), (doc, node_id) in list(self.objects.items()):
if doc == docname:
del self.objects[typ, name]
def merge_domaindata(self, docnames: List[str], otherdata: Dict) -> None:
# XXX check duplicates
for (typ, name), doc in otherdata['objects'].items():
for (typ, name), (doc, node_id) in otherdata['objects'].items():
if doc in docnames:
self.objects[typ, name] = doc
self.objects[typ, name] = (doc, node_id)
def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
typ: str, target: str, node: pending_xref, contnode: Element
) -> Element:
objtypes = self.objtypes_for_role(typ)
for objtype in objtypes:
todocname = self.objects.get((objtype, target))
todocname, node_id = self.objects.get((objtype, target), (None, None))
if todocname:
return make_refnode(builder, fromdocname, todocname,
objtype + '-' + target,
return make_refnode(builder, fromdocname, todocname, node_id,
contnode, target + ' ' + objtype)
return None
@ -232,17 +262,16 @@ class ReSTDomain(Domain):
) -> List[Tuple[str, Element]]:
results = [] # type: List[Tuple[str, Element]]
for objtype in self.object_types:
todocname = self.objects.get((objtype, target))
todocname, node_id = self.objects.get((objtype, target), (None, None))
if todocname:
results.append(('rst:' + self.role_for_objtype(objtype),
make_refnode(builder, fromdocname, todocname,
objtype + '-' + target,
make_refnode(builder, fromdocname, todocname, node_id,
contnode, target + ' ' + objtype)))
return results
def get_objects(self) -> Iterator[Tuple[str, str, str, str, str, int]]:
for (typ, name), docname in self.data['objects'].items():
yield name, name, typ, docname, typ + '-' + name, 1
for (typ, name), (docname, node_id) in self.data['objects'].items():
yield name, name, typ, docname, node_id, 1
def setup(app: Sphinx) -> Dict[str, Any]:
@ -250,7 +279,7 @@ def setup(app: Sphinx) -> Dict[str, Any]:
return {
'version': 'builtin',
'env_version': 1,
'env_version': 2,
'parallel_read_safe': True,
'parallel_write_safe': True,
}

View File

@ -66,9 +66,17 @@ class GenericObject(ObjectDescription):
return name
def add_target_and_index(self, name: str, sig: str, signode: desc_signature) -> None:
targetname = '%s-%s' % (self.objtype, name)
signode['ids'].append(targetname)
node_id = make_id(self.env, self.state.document, self.objtype, name)
signode['ids'].append(node_id)
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = self.make_old_id(name)
if old_node_id not in self.state.document.ids and old_node_id not in signode['ids']:
signode['ids'].append(old_node_id)
self.state.document.note_explicit_target(signode)
if self.indextemplate:
colon = self.indextemplate.find(':')
if colon != -1:
@ -77,11 +85,18 @@ class GenericObject(ObjectDescription):
else:
indextype = 'single'
indexentry = self.indextemplate % (name,)
self.indexnode['entries'].append((indextype, indexentry,
targetname, '', None))
self.indexnode['entries'].append((indextype, indexentry, node_id, '', None))
std = cast(StandardDomain, self.env.get_domain('std'))
std.note_object(self.objtype, name, targetname, location=signode)
std.note_object(self.objtype, name, node_id, location=signode)
def make_old_id(self, name: str) -> str:
"""Generate old styled node_id for generic objects.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return self.objtype + '-' + name
class EnvVar(GenericObject):
@ -124,9 +139,16 @@ class Target(SphinxDirective):
def run(self) -> List[Node]:
# normalize whitespace in fullname like XRefRole does
fullname = ws_re.sub(' ', self.arguments[0].strip())
targetname = '%s-%s' % (self.name, fullname)
node = nodes.target('', '', ids=[targetname])
node_id = make_id(self.env, self.state.document, self.name, fullname)
node = nodes.target('', '', ids=[node_id])
self.set_source_info(node)
# Assign old styled node_id not to break old hyperlinks (if possible)
# Note: Will be removed in Sphinx-5.0 (RemovedInSphinx50Warning)
old_node_id = self.make_old_id(fullname)
if old_node_id not in self.state.document.ids and old_node_id not in node['ids']:
node['ids'].append(old_node_id)
self.state.document.note_explicit_target(node)
ret = [node] # type: List[Node]
if self.indextemplate:
@ -136,18 +158,25 @@ class Target(SphinxDirective):
if colon != -1:
indextype = indexentry[:colon].strip()
indexentry = indexentry[colon + 1:].strip()
inode = addnodes.index(entries=[(indextype, indexentry,
targetname, '', None)])
inode = addnodes.index(entries=[(indextype, indexentry, node_id, '', None)])
ret.insert(0, inode)
name = self.name
if ':' in self.name:
_, name = self.name.split(':', 1)
std = cast(StandardDomain, self.env.get_domain('std'))
std.note_object(name, fullname, targetname, location=node)
std.note_object(name, fullname, node_id, location=node)
return ret
def make_old_id(self, name: str) -> str:
"""Generate old styled node_id for targets.
.. note:: Old Styled node_id was used until Sphinx-3.0.
This will be removed in Sphinx-5.0.
"""
return self.name + '-' + name
class Cmdoption(ObjectDescription):
"""

View File

@ -7,7 +7,7 @@
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import bisect
import re
import unicodedata
from itertools import groupby
@ -52,8 +52,7 @@ class IndexEntries:
except NoUri:
pass
else:
# maintain links in sorted/deterministic order
bisect.insort(entry[0], (main, uri))
entry[0].append((main, uri))
domain = cast(IndexDomain, self.env.get_domain('index'))
for fn, entries in domain.entries.items():
@ -89,6 +88,16 @@ class IndexEntries:
except ValueError as err:
logger.warning(str(err), location=fn)
# sort the index entries for same keyword.
def keyfunc0(entry: Tuple[str, str]) -> Tuple[bool, str]:
main, uri = entry
return (not main, uri) # show main entries at first
for indexentry in new.values():
indexentry[0].sort(key=keyfunc0)
for subentry in indexentry[1].values():
subentry[0].sort(key=keyfunc0) # type: ignore
# sort the index entries; put all symbols at the front, even those
# following the letters in ASCII, this is where the chr(127) comes from
def keyfunc(entry: Tuple[str, List]) -> Tuple[str, str]:

View File

@ -49,7 +49,8 @@ def stringify(annotation: Any) -> str:
return repr(annotation)
elif annotation is NoneType: # type: ignore
return 'None'
elif getattr(annotation, '__module__', None) == 'builtins':
elif (getattr(annotation, '__module__', None) == 'builtins' and
hasattr(annotation, '__qualname__')):
return annotation.__qualname__
elif annotation is Ellipsis:
return '...'
@ -88,6 +89,8 @@ def _stringify_py37(annotation: Any) -> str:
args = ', '.join(stringify(a) for a in annotation.__args__[:-1])
returns = stringify(annotation.__args__[-1])
return '%s[[%s], %s]' % (qualname, args, returns)
elif str(annotation).startswith('typing.Annotated'): # for py39+
return stringify(annotation.__args__[0])
elif annotation._special:
return qualname
else:

View File

@ -0,0 +1,6 @@
from typing import Annotated
def hello(name: Annotated[str, "attribute"]) -> None:
"""docstring"""
pass

View File

@ -1524,6 +1524,24 @@ def test_autodoc_typed_instance_variables(app):
]
@pytest.mark.skipif(sys.version_info < (3, 9), reason='py39+ is required.')
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_Annotated(app):
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.annotated', options)
assert list(actual) == [
'',
'.. py:module:: target.annotated',
'',
'',
'.. py:function:: hello(name: str) -> None',
' :module: target.annotated',
'',
' docstring',
' '
]
@pytest.mark.sphinx('html', testroot='pycode-egg')
def test_autodoc_for_egged_code(app):
options = {"members": None,

View File

@ -320,9 +320,13 @@ def test_epub_anchor_id(app):
app.build()
html = (app.outdir / 'index.xhtml').read_text()
assert '<p id="std-setting-STATICFILES_FINDERS">blah blah blah</p>' in html
assert '<span id="std-setting-STATICFILES_SECTION"></span><h1>blah blah blah</h1>' in html
assert 'see <a class="reference internal" href="#std-setting-STATICFILES_FINDERS">' in html
assert ('<p id="std-setting-staticfiles-finders">'
'<span id="std-setting-STATICFILES_FINDERS"></span>'
'blah blah blah</p>' in html)
assert ('<span id="std-setting-staticfiles-section"></span>'
'<span id="std-setting-STATICFILES_SECTION"></span>'
'<h1>blah blah blah</h1>' in html)
assert 'see <a class="reference internal" href="#std-setting-staticfiles-finders">' in html
@pytest.mark.sphinx('epub', testroot='html_assets')

View File

@ -218,7 +218,7 @@ def test_html4_output(app, status, warning):
"[@class='rfc reference external']/strong", 'RFC 1'),
(".//a[@href='https://tools.ietf.org/html/rfc1.html']"
"[@class='rfc reference external']/strong", 'Request for Comments #1'),
(".//a[@href='objects.html#envvar-HOME']"
(".//a[@href='objects.html#envvar-home']"
"[@class='reference internal']/code/span[@class='pre']", 'HOME'),
(".//a[@href='#with']"
"[@class='reference internal']/code/span[@class='pre']", '^with$'),

View File

@ -8,6 +8,8 @@
:license: BSD, see LICENSE for details.
"""
import json
import re
from unittest import mock
import pytest
@ -20,7 +22,7 @@ def test_defaults(app, status, warning):
content = (app.outdir / 'output.txt').read_text()
print(content)
# looking for '#top' and 'does-not-exist' not found should fail
# looking for '#top' and '#does-not-exist' not found should fail
assert "Anchor 'top' not found" in content
assert "Anchor 'does-not-exist' not found" in content
# looking for non-existent URL should fail
@ -31,6 +33,58 @@ def test_defaults(app, status, warning):
assert len(content.splitlines()) == 5
@pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True)
def test_defaults_json(app, status, warning):
app.builder.build_all()
assert (app.outdir / 'output.json').exists()
content = (app.outdir / 'output.json').read_text()
print(content)
rows = [json.loads(x) for x in content.splitlines()]
row = rows[0]
for attr in ["filename", "lineno", "status", "code", "uri",
"info"]:
assert attr in row
assert len(content.splitlines()) == 8
assert len(rows) == 8
# the output order of the rows is not stable
# due to possible variance in network latency
rowsby = {row["uri"]:row for row in rows}
assert rowsby["https://www.google.com#!bar"] == {
'filename': 'links.txt',
'lineno': 10,
'status': 'working',
'code': 0,
'uri': 'https://www.google.com#!bar',
'info': ''
}
# looking for non-existent URL should fail
dnerow = rowsby['https://localhost:7777/doesnotexist']
assert dnerow['filename'] == 'links.txt'
assert dnerow['lineno'] == 13
assert dnerow['status'] == 'broken'
assert dnerow['code'] == 0
assert dnerow['uri'] == 'https://localhost:7777/doesnotexist'
assert rowsby['https://www.google.com/image2.png'] == {
'filename': 'links.txt',
'lineno': 16,
'status': 'broken',
'code': 0,
'uri': 'https://www.google.com/image2.png',
'info': '404 Client Error: Not Found for url: https://www.google.com/image2.png'
}
# looking for '#top' and '#does-not-exist' not found should fail
assert "Anchor 'top' not found" == \
rowsby["https://www.google.com/#top"]["info"]
assert "Anchor 'does-not-exist' not found" == \
rowsby["http://www.sphinx-doc.org/en/1.7/intro.html#does-not-exist"]["info"]
# images should fail
assert "Not Found for url: https://www.google.com/image.png" in \
rowsby["https://www.google.com/image.png"]["info"]
@pytest.mark.sphinx(
'linkcheck', testroot='linkcheck', freshenv=True,
confoverrides={'linkcheck_anchors_ignore': ["^!", "^top$"],

View File

@ -76,7 +76,7 @@ def test_rst_directive_option(app):
[desc_content, ()])]))
assert_node(doctree[0],
entries=[("single", ":foo: (directive option)",
"directive:option--foo", "", "F")])
"directive-option-foo", "", "F")])
assert_node(doctree[1], addnodes.desc, desctype="directive:option",
domain="rst", objtype="directive:option", noindex=False)
@ -90,7 +90,7 @@ def test_rst_directive_option_with_argument(app):
[desc_content, ()])]))
assert_node(doctree[0],
entries=[("single", ":foo: (directive option)",
"directive:option--foo", "", "F")])
"directive-option-foo", "", "F")])
assert_node(doctree[1], addnodes.desc, desctype="directive:option",
domain="rst", objtype="directive:option", noindex=False)
@ -105,7 +105,7 @@ def test_rst_directive_option_type(app):
[desc_content, ()])]))
assert_node(doctree[0],
entries=[("single", ":foo: (directive option)",
"directive:option--foo", "", "F")])
"directive-option-foo", "", "F")])
assert_node(doctree[1], addnodes.desc, desctype="directive:option",
domain="rst", objtype="directive:option", noindex=False)
@ -121,7 +121,7 @@ def test_rst_directive_and_directive_option(app):
desc)])]))
assert_node(doctree[1][1][0],
entries=[("pair", "foo (directive); :bar: (directive option)",
"directive:option-foo-bar", "", "B")])
"directive-option-foo-bar", "", "B")])
assert_node(doctree[1][1][1], ([desc_signature, desc_name, ":bar:"],
[desc_content, ()]))
assert_node(doctree[1][1][1], addnodes.desc, desctype="directive:option",

View File

@ -112,6 +112,21 @@ def test_create_seealso_index(app):
assert index[2] == ('S', [('Sphinx', [[], [('see also documentation tool', [])], None])])
@pytest.mark.sphinx('dummy', freshenv=True)
def test_create_main_index(app):
text = (".. index:: !docutils\n"
".. index:: docutils\n"
".. index:: pip; install\n"
".. index:: !pip; install\n")
restructuredtext.parse(app, text)
index = IndexEntries(app.env).create_index(app.builder)
assert len(index) == 2
assert index[0] == ('D', [('docutils', [[('main', '#index-0'),
('', '#index-1')], [], None])])
assert index[1] == ('P', [('pip', [[], [('install', [('main', '#index-3'),
('', '#index-2')])], None])])
@pytest.mark.sphinx('dummy', freshenv=True)
def test_create_index_with_name(app):
text = (".. index:: single: docutils\n"

View File

@ -12,6 +12,8 @@ import sys
from numbers import Integral
from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional
import pytest
from sphinx.util.typing import stringify
@ -42,6 +44,12 @@ def test_stringify_type_hints_containers():
assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]"
@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.')
def test_stringify_Annotated():
from typing import Annotated
assert stringify(Annotated[str, "foo", "bar"]) == "str"
def test_stringify_type_hints_string():
assert stringify("int") == "int"
assert stringify("str") == "str"

10
tox.ini
View File

@ -5,12 +5,18 @@ envlist = docs,flake8,mypy,coverage,py{35,36,37,38,39},du{12,13,14,15}
[testenv]
usedevelop = True
passenv =
https_proxy http_proxy no_proxy PERL PERL5LIB PYTEST_ADDOPTS EPUBCHECK_PATH
https_proxy
http_proxy
no_proxy
PERL
PERL5LIB
PYTEST_ADDOPTS
EPUBCHECK_PATH
TERM
description =
py{35,36,37,38,39}: Run unit tests against {envname}.
du{12,13,14}: Run unit tests with the given version of docutils.
deps =
coverage < 5.0 # refs: https://github.com/sphinx-doc/sphinx/pull/6924
git+https://github.com/html5lib/html5lib-python # refs: https://github.com/html5lib/html5lib-python/issues/419
du12: docutils==0.12
du13: docutils==0.13.1