mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '3.x'
This commit is contained in:
commit
b1400ac421
@ -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
25
CHANGES
@ -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
|
||||
|
@ -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
|
||||
|
@ -62,6 +62,7 @@ markers =
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
parallel = True
|
||||
source = sphinx
|
||||
|
||||
[coverage:report]
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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]:
|
||||
|
@ -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:
|
||||
|
6
tests/roots/test-ext-autodoc/target/annotated.py
Normal file
6
tests/roots/test-ext-autodoc/target/annotated.py
Normal file
@ -0,0 +1,6 @@
|
||||
from typing import Annotated
|
||||
|
||||
|
||||
def hello(name: Annotated[str, "attribute"]) -> None:
|
||||
"""docstring"""
|
||||
pass
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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$'),
|
||||
|
@ -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$"],
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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
10
tox.ini
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user