Merge pull request #7356 from tk0miya/7301_node_id

Fix #7301: Allow . and _ for node_id
This commit is contained in:
Takeshi KOMIYA 2020-03-22 23:42:56 +09:00 committed by GitHub
commit 8edae5fe28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 137 additions and 63 deletions

View File

@ -425,7 +425,7 @@ class PyObject(ObjectDescription):
signode: desc_signature) -> None:
modname = self.options.get('module', self.env.ref_context.get('py:module'))
fullname = (modname + '.' if modname else '') + name_cls[0]
node_id = make_id(self.env, self.state.document, modname or '', name_cls[0])
node_id = make_id(self.env, self.state.document, '', fullname)
signode['ids'].append(node_id)
# Assign old styled node_id(fullname) not to break old hyperlinks (if possible)

View File

@ -9,6 +9,7 @@
"""
import re
import unicodedata
import warnings
from typing import Any, Callable, Iterable, List, Set, Tuple
from typing import cast
@ -436,6 +437,79 @@ def inline_all_toctrees(builder: "Builder", docnameset: Set[str], docname: str,
return tree
def _make_id(string: str) -> str:
"""Convert `string` into an identifier and return it.
This function is a modified version of ``docutils.nodes.make_id()`` of
docutils-0.16.
Changes:
* Allow to use dots (".") and underscores ("_") for an identifier
without a leading character.
# Author: David Goodger <goodger@python.org>
# Maintainer: docutils-develop@lists.sourceforge.net
# Copyright: This module has been placed in the public domain.
"""
id = string.lower()
id = id.translate(_non_id_translate_digraphs)
id = id.translate(_non_id_translate)
# get rid of non-ascii characters.
# 'ascii' lowercase to prevent problems with turkish locale.
id = unicodedata.normalize('NFKD', id).encode('ascii', 'ignore').decode('ascii')
# shrink runs of whitespace and replace by hyphen
id = _non_id_chars.sub('-', ' '.join(id.split()))
id = _non_id_at_ends.sub('', id)
return str(id)
_non_id_chars = re.compile('[^a-z0-9._]+')
_non_id_at_ends = re.compile('^[-0-9._]+|-+$')
_non_id_translate = {
0x00f8: u'o', # o with stroke
0x0111: u'd', # d with stroke
0x0127: u'h', # h with stroke
0x0131: u'i', # dotless i
0x0142: u'l', # l with stroke
0x0167: u't', # t with stroke
0x0180: u'b', # b with stroke
0x0183: u'b', # b with topbar
0x0188: u'c', # c with hook
0x018c: u'd', # d with topbar
0x0192: u'f', # f with hook
0x0199: u'k', # k with hook
0x019a: u'l', # l with bar
0x019e: u'n', # n with long right leg
0x01a5: u'p', # p with hook
0x01ab: u't', # t with palatal hook
0x01ad: u't', # t with hook
0x01b4: u'y', # y with hook
0x01b6: u'z', # z with stroke
0x01e5: u'g', # g with stroke
0x0225: u'z', # z with hook
0x0234: u'l', # l with curl
0x0235: u'n', # n with curl
0x0236: u't', # t with curl
0x0237: u'j', # dotless j
0x023c: u'c', # c with stroke
0x023f: u's', # s with swash tail
0x0240: u'z', # z with swash tail
0x0247: u'e', # e with stroke
0x0249: u'j', # j with stroke
0x024b: u'q', # q with hook tail
0x024d: u'r', # r with stroke
0x024f: u'y', # y with stroke
}
_non_id_translate_digraphs = {
0x00df: u'sz', # ligature sz
0x00e6: u'ae', # ae
0x0153: u'oe', # ligature oe
0x0238: u'db', # db digraph
0x0239: u'qp', # qp digraph
}
def make_id(env: "BuildEnvironment", document: nodes.document,
prefix: str = '', term: str = None) -> str:
"""Generate an appropriate node_id for given *prefix* and *term*."""
@ -447,12 +521,12 @@ def make_id(env: "BuildEnvironment", document: nodes.document,
# try to generate node_id by *term*
if prefix and term:
node_id = nodes.make_id(idformat % term)
node_id = _make_id(idformat % term)
if node_id == prefix:
# *term* is not good to generate a node_id.
node_id = None
elif term:
node_id = nodes.make_id(term)
node_id = _make_id(term)
if node_id == '':
node_id = None # fallback to None

View File

@ -320,13 +320,13 @@ def test_epub_anchor_id(app):
app.build()
html = (app.outdir / 'index.xhtml').read_text()
assert ('<p id="std-setting-staticfiles-finders">'
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>'
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
assert 'see <a class="reference internal" href="#std-setting-staticfiles_finders">' in html
@pytest.mark.sphinx('epub', testroot='html_assets')

View File

@ -176,9 +176,9 @@ def test_html4_output(app, status, warning):
r'-| |-'),
],
'autodoc.html': [
(".//dl[@class='py class']/dt[@id='autodoc-target-class']", ''),
(".//dl[@class='py function']/dt[@id='autodoc-target-function']/em/span", r'\*\*'),
(".//dl[@class='py function']/dt[@id='autodoc-target-function']/em/span", r'kwds'),
(".//dl[@class='py class']/dt[@id='autodoc_target.class']", ''),
(".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'\*\*'),
(".//dl[@class='py function']/dt[@id='autodoc_target.function']/em/span", r'kwds'),
(".//dd/p", r'Return spam\.'),
],
'extapi.html': [
@ -223,7 +223,7 @@ def test_html4_output(app, status, warning):
"[@class='reference internal']/code/span[@class='pre']", 'HOME'),
(".//a[@href='#with']"
"[@class='reference internal']/code/span[@class='pre']", '^with$'),
(".//a[@href='#grammar-token-try-stmt']"
(".//a[@href='#grammar-token-try_stmt']"
"[@class='reference internal']/code/span", '^statement$'),
(".//a[@href='#some-label'][@class='reference internal']/span", '^here$'),
(".//a[@href='#some-label'][@class='reference internal']/span", '^there$'),
@ -255,7 +255,7 @@ def test_html4_output(app, status, warning):
(".//dl/dt[@id='term-boson']", 'boson'),
# a production list
(".//pre/strong", 'try_stmt'),
(".//pre/a[@href='#grammar-token-try1-stmt']/code/span", 'try1_stmt'),
(".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'),
# tests for ``only`` directive
(".//p", 'A global substitution.'),
(".//p", 'In HTML.'),
@ -263,7 +263,7 @@ def test_html4_output(app, status, warning):
(".//p", 'Always present'),
# tests for ``any`` role
(".//a[@href='#with']/span", 'headings'),
(".//a[@href='objects.html#func-without-body']/code/span", 'objects'),
(".//a[@href='objects.html#func_without_body']/code/span", 'objects'),
# tests for numeric labels
(".//a[@href='#id1'][@class='reference internal']/span", 'Testing various markup'),
# tests for smartypants
@ -275,18 +275,18 @@ def test_html4_output(app, status, warning):
(".//p", 'Il dit : « Cest “super” ! »'),
],
'objects.html': [
(".//dt[@id='mod-cls-meth1']", ''),
(".//dt[@id='errmod-error']", ''),
(".//dt[@id='mod.cls.meth1']", ''),
(".//dt[@id='errmod.error']", ''),
(".//dt/code", r'long\(parameter,\s* list\)'),
(".//dt/code", 'another one'),
(".//a[@href='#mod-cls'][@class='reference internal']", ''),
(".//a[@href='#mod.cls'][@class='reference internal']", ''),
(".//dl[@class='std userdesc']", ''),
(".//dt[@id='userdesc-myobj']", ''),
(".//a[@href='#userdesc-myobj'][@class='reference internal']", ''),
# docfields
(".//a[@class='reference internal'][@href='#timeint']/em", 'TimeInt'),
(".//a[@class='reference internal'][@href='#time']", 'Time'),
(".//a[@class='reference internal'][@href='#errmod-error']/strong", 'Error'),
(".//a[@class='reference internal'][@href='#errmod.error']/strong", 'Error'),
# C references
(".//span[@class='pre']", 'CFunction()'),
(".//a[@href='#c.Sphinx_DoSomething']", ''),
@ -325,7 +325,7 @@ def test_html4_output(app, status, warning):
'\\+p'),
(".//a[@class='reference internal'][@href='#cmdoption-perl-objc']/code/span",
'--ObjC\\+\\+'),
(".//a[@class='reference internal'][@href='#cmdoption-perl-plugin-option']/code/span",
(".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span",
'--plugin.option'),
(".//a[@class='reference internal'][@href='#cmdoption-perl-arg-create-auth-token']"
"/code/span",

View File

@ -123,25 +123,25 @@ def test_domain_js_find_obj(app, status, warning):
('NestedParentA', ('roles', 'nestedparenta', 'class')))
assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
('NestedParentA.NestedChildA',
('roles', 'nestedparenta-nestedchilda', 'class')))
('roles', 'nestedparenta.nestedchilda', 'class')))
assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') ==
('NestedParentA.NestedChildA',
('roles', 'nestedparenta-nestedchilda', 'class')))
('roles', 'nestedparenta.nestedchilda', 'class')))
assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'func') ==
('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'function')))
('roles', 'nestedparenta.nestedchilda.subchild_1', 'function')))
assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'func') ==
('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'function')))
('roles', 'nestedparenta.nestedchilda.subchild_1', 'function')))
assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'func') ==
('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'function')))
('roles', 'nestedparenta.nestedchilda.subchild_1', 'function')))
assert (find_obj('module_a.submodule', 'ModTopLevel', 'mod_child_2', 'meth') ==
('module_a.submodule.ModTopLevel.mod_child_2',
('module', 'module-a-submodule-modtoplevel-mod-child-2', 'method')))
('module', 'module_a.submodule.modtoplevel.mod_child_2', 'method')))
assert (find_obj('module_b.submodule', 'ModTopLevel', 'module_a.submodule', 'mod') ==
('module_a.submodule',
('module', 'module-module-a-submodule', 'module')))
('module', 'module-module_a.submodule', 'module')))
def test_get_full_qualified_name():

View File

@ -171,11 +171,11 @@ def test_resolve_xref_for_properties(app, status, warning):
app.builder.build_all()
content = (app.outdir / 'module.html').read_text()
assert ('Link to <a class="reference internal" href="#module-a-submodule-modtoplevel-prop"'
assert ('Link to <a class="reference internal" href="#module_a.submodule.modtoplevel.prop"'
' title="module_a.submodule.ModTopLevel.prop">'
'<code class="xref py py-attr docutils literal notranslate"><span class="pre">'
'prop</span> <span class="pre">attribute</span></code></a>' in content)
assert ('Link to <a class="reference internal" href="#module-a-submodule-modtoplevel-prop"'
assert ('Link to <a class="reference internal" href="#module_a.submodule.modtoplevel.prop"'
' title="module_a.submodule.ModTopLevel.prop">'
'<code class="xref py py-meth docutils literal notranslate"><span class="pre">'
'prop</span> <span class="pre">method</span></code></a>' in content)
@ -194,18 +194,18 @@ def test_domain_py_find_obj(app, status, warning):
assert (find_obj(None, None, 'NestedParentA', 'class') ==
[('NestedParentA', ('roles', 'nestedparenta', 'class'))])
assert (find_obj(None, None, 'NestedParentA.NestedChildA', 'class') ==
[('NestedParentA.NestedChildA', ('roles', 'nestedparenta-nestedchilda', 'class'))])
[('NestedParentA.NestedChildA', ('roles', 'nestedparenta.nestedchilda', 'class'))])
assert (find_obj(None, 'NestedParentA', 'NestedChildA', 'class') ==
[('NestedParentA.NestedChildA', ('roles', 'nestedparenta-nestedchilda', 'class'))])
[('NestedParentA.NestedChildA', ('roles', 'nestedparenta.nestedchilda', 'class'))])
assert (find_obj(None, None, 'NestedParentA.NestedChildA.subchild_1', 'meth') ==
[('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))])
('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))])
assert (find_obj(None, 'NestedParentA', 'NestedChildA.subchild_1', 'meth') ==
[('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))])
('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))])
assert (find_obj(None, 'NestedParentA.NestedChildA', 'subchild_1', 'meth') ==
[('NestedParentA.NestedChildA.subchild_1',
('roles', 'nestedparenta-nestedchilda-subchild-1', 'method'))])
('roles', 'nestedparenta.nestedchilda.subchild_1', 'method'))])
def test_get_full_qualified_name():
@ -514,61 +514,61 @@ def test_pymethod_options(app):
# method
assert_node(doctree[1][1][0], addnodes.index,
entries=[('single', 'meth1() (Class method)', 'class-meth1', '', None)])
entries=[('single', 'meth1() (Class method)', 'class.meth1', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "meth1"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth1' in domain.objects
assert domain.objects['Class.meth1'] == ('index', 'class-meth1', 'method')
assert domain.objects['Class.meth1'] == ('index', 'class.meth1', 'method')
# :classmethod:
assert_node(doctree[1][1][2], addnodes.index,
entries=[('single', 'meth2() (Class class method)', 'class-meth2', '', None)])
entries=[('single', 'meth2() (Class class method)', 'class.meth2', '', None)])
assert_node(doctree[1][1][3], ([desc_signature, ([desc_annotation, "classmethod "],
[desc_name, "meth2"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth2' in domain.objects
assert domain.objects['Class.meth2'] == ('index', 'class-meth2', 'method')
assert domain.objects['Class.meth2'] == ('index', 'class.meth2', 'method')
# :staticmethod:
assert_node(doctree[1][1][4], addnodes.index,
entries=[('single', 'meth3() (Class static method)', 'class-meth3', '', None)])
entries=[('single', 'meth3() (Class static method)', 'class.meth3', '', None)])
assert_node(doctree[1][1][5], ([desc_signature, ([desc_annotation, "static "],
[desc_name, "meth3"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth3' in domain.objects
assert domain.objects['Class.meth3'] == ('index', 'class-meth3', 'method')
assert domain.objects['Class.meth3'] == ('index', 'class.meth3', 'method')
# :async:
assert_node(doctree[1][1][6], addnodes.index,
entries=[('single', 'meth4() (Class method)', 'class-meth4', '', None)])
entries=[('single', 'meth4() (Class method)', 'class.meth4', '', None)])
assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, "async "],
[desc_name, "meth4"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth4' in domain.objects
assert domain.objects['Class.meth4'] == ('index', 'class-meth4', 'method')
assert domain.objects['Class.meth4'] == ('index', 'class.meth4', 'method')
# :property:
assert_node(doctree[1][1][8], addnodes.index,
entries=[('single', 'meth5() (Class property)', 'class-meth5', '', None)])
entries=[('single', 'meth5() (Class property)', 'class.meth5', '', None)])
assert_node(doctree[1][1][9], ([desc_signature, ([desc_annotation, "property "],
[desc_name, "meth5"])],
[desc_content, ()]))
assert 'Class.meth5' in domain.objects
assert domain.objects['Class.meth5'] == ('index', 'class-meth5', 'method')
assert domain.objects['Class.meth5'] == ('index', 'class.meth5', 'method')
# :abstractmethod:
assert_node(doctree[1][1][10], addnodes.index,
entries=[('single', 'meth6() (Class method)', 'class-meth6', '', None)])
entries=[('single', 'meth6() (Class method)', 'class.meth6', '', None)])
assert_node(doctree[1][1][11], ([desc_signature, ([desc_annotation, "abstract "],
[desc_name, "meth6"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth6' in domain.objects
assert domain.objects['Class.meth6'] == ('index', 'class-meth6', 'method')
assert domain.objects['Class.meth6'] == ('index', 'class.meth6', 'method')
def test_pyclassmethod(app):
@ -583,13 +583,13 @@ def test_pyclassmethod(app):
[desc_content, (addnodes.index,
desc)])]))
assert_node(doctree[1][1][0], addnodes.index,
entries=[('single', 'meth() (Class class method)', 'class-meth', '', None)])
entries=[('single', 'meth() (Class class method)', 'class.meth', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "classmethod "],
[desc_name, "meth"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth' in domain.objects
assert domain.objects['Class.meth'] == ('index', 'class-meth', 'method')
assert domain.objects['Class.meth'] == ('index', 'class.meth', 'method')
def test_pystaticmethod(app):
@ -604,13 +604,13 @@ def test_pystaticmethod(app):
[desc_content, (addnodes.index,
desc)])]))
assert_node(doctree[1][1][0], addnodes.index,
entries=[('single', 'meth() (Class static method)', 'class-meth', '', None)])
entries=[('single', 'meth() (Class static method)', 'class.meth', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_annotation, "static "],
[desc_name, "meth"],
[desc_parameterlist, ()])],
[desc_content, ()]))
assert 'Class.meth' in domain.objects
assert domain.objects['Class.meth'] == ('index', 'class-meth', 'method')
assert domain.objects['Class.meth'] == ('index', 'class.meth', 'method')
def test_pyattribute(app):
@ -627,13 +627,13 @@ def test_pyattribute(app):
[desc_content, (addnodes.index,
desc)])]))
assert_node(doctree[1][1][0], addnodes.index,
entries=[('single', 'attr (Class attribute)', 'class-attr', '', None)])
entries=[('single', 'attr (Class attribute)', 'class.attr', '', None)])
assert_node(doctree[1][1][1], ([desc_signature, ([desc_name, "attr"],
[desc_annotation, ": str"],
[desc_annotation, " = ''"])],
[desc_content, ()]))
assert 'Class.attr' in domain.objects
assert domain.objects['Class.attr'] == ('index', 'class-attr', 'attribute')
assert domain.objects['Class.attr'] == ('index', 'class.attr', 'attribute')
def test_pydecorator_signature(app):
@ -679,10 +679,10 @@ def test_module_index(app):
assert index.generate() == (
[('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
('s', [IndexEntry('sphinx', 1, 'index', 'module-sphinx', '', '', ''),
IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx-builders', '', '', ''), # NOQA
IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx-builders-html', '', '', ''), # NOQA
IndexEntry('sphinx.config', 2, 'index', 'module-sphinx-config', '', '', ''),
IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx-intl', '', '', '')])],
IndexEntry('sphinx.builders', 2, 'index', 'module-sphinx.builders', '', '', ''), # NOQA
IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', ''), # NOQA
IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', ''),
IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])],
False
)
@ -694,7 +694,7 @@ def test_module_index_submodule(app):
index = PythonModuleIndex(app.env.get_domain('py'))
assert index.generate() == (
[('s', [IndexEntry('sphinx', 1, '', '', '', '', ''),
IndexEntry('sphinx.config', 2, 'index', 'module-sphinx-config', '', '', '')])],
IndexEntry('sphinx.config', 2, 'index', 'module-sphinx.config', '', '', '')])],
False
)
@ -723,12 +723,12 @@ def test_modindex_common_prefix(app):
restructuredtext.parse(app, text)
index = PythonModuleIndex(app.env.get_domain('py'))
assert index.generate() == (
[('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx-builders', '', '', ''), # NOQA
IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx-builders-html', '', '', '')]), # NOQA
('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx-config', '', '', '')]),
[('b', [IndexEntry('sphinx.builders', 1, 'index', 'module-sphinx.builders', '', '', ''), # NOQA
IndexEntry('sphinx.builders.html', 2, 'index', 'module-sphinx.builders.html', '', '', '')]), # NOQA
('c', [IndexEntry('sphinx.config', 0, 'index', 'module-sphinx.config', '', '', '')]),
('d', [IndexEntry('docutils', 0, 'index', 'module-docutils', '', '', '')]),
('s', [IndexEntry('sphinx', 0, 'index', 'module-sphinx', '', '', ''),
IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx-intl', '', '', '')])],
IndexEntry('sphinx_intl', 0, 'index', 'module-sphinx_intl', '', '', '')])],
True
)

View File

@ -84,7 +84,7 @@ def test_object_inventory(app):
refs = app.env.domaindata['py']['objects']
assert 'func_without_module' in refs
assert refs['func_without_module'] == ('objects', 'func-without-module', 'function')
assert refs['func_without_module'] == ('objects', 'func_without_module', 'function')
assert 'func_without_module2' in refs
assert 'mod.func_in_module' in refs
assert 'mod.Cls' in refs

View File

@ -870,7 +870,7 @@ def test_xml_refs_in_python_domain(app):
assert_elem(
para0[0],
['SEE THIS DECORATOR:', 'sensitive_variables()', '.'],
['sensitive-sensitive-variables'])
['sensitive.sensitive_variables'])
@sphinx_intl

View File

@ -189,9 +189,9 @@ def test_clean_astext():
('', '', 'id0'),
('term', '', 'term-0'),
('term', 'Sphinx', 'term-sphinx'),
('', 'io.StringIO', 'io-stringio'), # contains a dot
('', 'sphinx.setup_command', 'sphinx-setup-command'), # contains a dot
('', '_io.StringIO', 'io-stringio'), # starts with underscore
('', 'io.StringIO', 'io.stringio'), # contains a dot
('', 'sphinx.setup_command', 'sphinx.setup_command'), # contains a dot & underscore
('', '_io.StringIO', 'io.stringio'), # starts with underscore
('', '', 'sphinx'), # alphabets in unicode fullwidth characters
('', '悠好', 'id0'), # multibytes text (in Chinese)
('', 'Hello=悠好=こんにちは', 'hello'), # alphabets and multibytes text