From a9ab8c0f56478036f99deea7ae4a631692af803f Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Wed, 25 Mar 2020 01:43:23 +0900 Subject: [PATCH] Fix #7301: capital characters are not allowed for node_id --- CHANGES | 1 + sphinx/util/nodes.py | 6 +-- tests/test_build_epub.py | 6 +-- tests/test_build_html.py | 18 ++++----- tests/test_domain_js.py | 16 ++++---- tests/test_domain_py.py | 52 +++++++++++++------------- tests/test_domain_std.py | 34 ++++++++--------- tests/test_environment_indexentries.py | 2 +- tests/test_intl.py | 6 +-- tests/test_util_nodes.py | 10 ++--- 10 files changed, 76 insertions(+), 75 deletions(-) diff --git a/CHANGES b/CHANGES index 1110b819aeb..0cae05b6a43 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ Bugs fixed * #7364: autosummary: crashed when :confval:`autosummary_generate` is False * #7370: autosummary: raises UnboundLocalError when unknown module given +* #7301: capital characters are not allowed for node_id Testing -------- diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index b39a7ca0170..bca6e0bfc53 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -445,6 +445,7 @@ def _make_id(string: str) -> str: Changes: + * Allow to use capital alphabet characters * Allow to use dots (".") and underscores ("_") for an identifier without a leading character. @@ -452,8 +453,7 @@ def _make_id(string: str) -> str: # 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 = string.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. @@ -464,7 +464,7 @@ def _make_id(string: str) -> str: return str(id) -_non_id_chars = re.compile('[^a-z0-9._]+') +_non_id_chars = re.compile('[^a-zA-Z0-9._]+') _non_id_at_ends = re.compile('^[-0-9._]+|-+$') _non_id_translate = { 0x00f8: u'o', # o with stroke diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index ca6f09cdd7a..f893e1351e4 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -320,13 +320,13 @@ def test_epub_anchor_id(app): app.build() html = (app.outdir / 'index.xhtml').read_text() - assert ('

' + assert ('

' '' 'blah blah blah

' in html) - assert ('' + assert ('' '' '

blah blah blah

' in html) - assert 'see ' in html + assert 'see ' in html @pytest.mark.sphinx('epub', testroot='html_assets') diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 2cf8dde20cb..ede3683155b 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -176,7 +176,7 @@ def test_html4_output(app, status, warning): r'-| |-'), ], 'autodoc.html': [ - (".//dl[@class='py class']/dt[@id='autodoc_target.class']", ''), + (".//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\.'), @@ -219,7 +219,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$'), @@ -275,18 +275,18 @@ def test_html4_output(app, status, warning): (".//p", 'Il dit : « C’est “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='#TimeInt']/em", 'TimeInt'), + (".//a[@class='reference internal'][@href='#Time']", 'Time'), + (".//a[@class='reference internal'][@href='#errmod.Error']/strong", 'Error'), # C references (".//span[@class='pre']", 'CFunction()'), (".//a[@href='#c.Sphinx_DoSomething']", ''), @@ -323,7 +323,7 @@ def test_html4_output(app, status, warning): 'perl'), (".//a[@class='reference internal'][@href='#cmdoption-perl-arg-p']/code/span", '\\+p'), - (".//a[@class='reference internal'][@href='#cmdoption-perl-objc']/code/span", + (".//a[@class='reference internal'][@href='#cmdoption-perl-ObjC']/code/span", '--ObjC\\+\\+'), (".//a[@class='reference internal'][@href='#cmdoption-perl-plugin.option']/code/span", '--plugin.option'), diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index f7bacb90e26..189f22e5b4e 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -120,25 +120,25 @@ def find_obj(mod_name, prefix, obj_name, obj_type, searchmode=0): assert (find_obj(None, None, 'NONEXISTANT', 'class') == (None, None)) assert (find_obj(None, None, 'NestedParentA', 'class') == - ('NestedParentA', ('roles', 'nestedparenta', 'class'))) + ('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'))) @@ -205,7 +205,7 @@ def test_js_class(app): [desc_parameterlist, ()])], [desc_content, ()])])) assert_node(doctree[0], addnodes.index, - entries=[("single", "Application() (class)", "application", "", None)]) + entries=[("single", "Application() (class)", "Application", "", None)]) assert_node(doctree[1], addnodes.desc, domain="js", objtype="class", noindex=False) diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index e4bc1700462..dd7476024af 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -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 ' '' 'prop attribute' in content) - assert ('Link to ' '' 'prop method' in content) @@ -192,20 +192,20 @@ def find_obj(modname, prefix, obj_name, obj_type, searchmode=0): assert (find_obj(None, None, 'NONEXISTANT', 'class') == []) assert (find_obj(None, None, 'NestedParentA', 'class') == - [('NestedParentA', ('roles', '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): diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 1f0024efc14..32b5c6970a2 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -352,23 +352,23 @@ def test_productionlist(app, status, warning): linkText = span.text.strip() cases.append((text, link, linkText)) assert cases == [ - ('A', 'Bare.html#grammar-token-a', 'A'), - ('B', 'Bare.html#grammar-token-b', 'B'), - ('P1:A', 'P1.html#grammar-token-p1-a', 'P1:A'), - ('P1:B', 'P1.html#grammar-token-p1-b', 'P1:B'), - ('P2:A', 'P1.html#grammar-token-p1-a', 'P1:A'), - ('P2:B', 'P2.html#grammar-token-p2-b', 'P2:B'), - ('Explicit title A, plain', 'Bare.html#grammar-token-a', 'MyTitle'), - ('Explicit title A, colon', 'Bare.html#grammar-token-a', 'My:Title'), - ('Explicit title P1:A, plain', 'P1.html#grammar-token-p1-a', 'MyTitle'), - ('Explicit title P1:A, colon', 'P1.html#grammar-token-p1-a', 'My:Title'), - ('Tilde A', 'Bare.html#grammar-token-a', 'A'), - ('Tilde P1:A', 'P1.html#grammar-token-p1-a', 'A'), - ('Tilde explicit title P1:A', 'P1.html#grammar-token-p1-a', '~MyTitle'), - ('Tilde, explicit title P1:A', 'P1.html#grammar-token-p1-a', 'MyTitle'), - ('Dup', 'Dup2.html#grammar-token-dup', 'Dup'), - ('FirstLine', 'firstLineRule.html#grammar-token-firstline', 'FirstLine'), - ('SecondLine', 'firstLineRule.html#grammar-token-secondline', 'SecondLine'), + ('A', 'Bare.html#grammar-token-A', 'A'), + ('B', 'Bare.html#grammar-token-B', 'B'), + ('P1:A', 'P1.html#grammar-token-P1-A', 'P1:A'), + ('P1:B', 'P1.html#grammar-token-P1-B', 'P1:B'), + ('P2:A', 'P1.html#grammar-token-P1-A', 'P1:A'), + ('P2:B', 'P2.html#grammar-token-P2-B', 'P2:B'), + ('Explicit title A, plain', 'Bare.html#grammar-token-A', 'MyTitle'), + ('Explicit title A, colon', 'Bare.html#grammar-token-A', 'My:Title'), + ('Explicit title P1:A, plain', 'P1.html#grammar-token-P1-A', 'MyTitle'), + ('Explicit title P1:A, colon', 'P1.html#grammar-token-P1-A', 'My:Title'), + ('Tilde A', 'Bare.html#grammar-token-A', 'A'), + ('Tilde P1:A', 'P1.html#grammar-token-P1-A', 'A'), + ('Tilde explicit title P1:A', 'P1.html#grammar-token-P1-A', '~MyTitle'), + ('Tilde, explicit title P1:A', 'P1.html#grammar-token-P1-A', 'MyTitle'), + ('Dup', 'Dup2.html#grammar-token-Dup', 'Dup'), + ('FirstLine', 'firstLineRule.html#grammar-token-FirstLine', 'FirstLine'), + ('SecondLine', 'firstLineRule.html#grammar-token-SecondLine', 'SecondLine'), ] text = (app.outdir / 'LineContinuation.html').read_text() diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index 97882d44b33..7f6003d54b4 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -161,5 +161,5 @@ def test_create_index_by_key(app): index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 3 assert index[0] == ('D', [('docutils', [[('main', '#term-docutils')], [], None])]) - assert index[1] == ('P', [('Python', [[('main', '#term-python')], [], None])]) + assert index[1] == ('P', [('Python', [[('main', '#term-Python')], [], None])]) assert index[2] == ('ス', [('スフィンクス', [[('main', '#term-0')], [], 'ス'])]) diff --git a/tests/test_intl.py b/tests/test_intl.py index 0e7dd4f62c9..d0c64b589df 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -946,14 +946,14 @@ def test_xml_role_xref(app): ['LINK TO', "I18N ROCK'N ROLE XREF", ',', 'CONTENTS', ',', 'SOME NEW TERM', '.'], ['i18n-role-xref', 'index', - 'glossary_terms#term-some-term']) + 'glossary_terms#term-Some-term']) para2 = sec2.findall('paragraph') assert_elem( para2[0], ['LINK TO', 'SOME OTHER NEW TERM', 'AND', 'SOME NEW TERM', '.'], - ['glossary_terms#term-some-other-term', - 'glossary_terms#term-some-term']) + ['glossary_terms#term-Some-other-term', + 'glossary_terms#term-Some-term']) assert_elem( para2[1], ['LINK TO', 'SAME TYPE LINKS', 'AND', diff --git a/tests/test_util_nodes.py b/tests/test_util_nodes.py index 01c5d2e3f9e..8fe9ee773c3 100644 --- a/tests/test_util_nodes.py +++ b/tests/test_util_nodes.py @@ -188,13 +188,13 @@ def test_clean_astext(): [ ('', '', 'id0'), ('term', '', 'term-0'), - ('term', 'Sphinx', 'term-sphinx'), - ('', 'io.StringIO', 'io.stringio'), # contains a dot + ('term', 'Sphinx', 'term-Sphinx'), + ('', 'io.StringIO', 'io.StringIO'), # contains a dot ('', 'sphinx.setup_command', 'sphinx.setup_command'), # contains a dot & underscore - ('', '_io.StringIO', 'io.stringio'), # starts with underscore + ('', '_io.StringIO', 'io.StringIO'), # starts with underscore ('', 'sphinx', 'sphinx'), # alphabets in unicode fullwidth characters ('', '悠好', 'id0'), # multibytes text (in Chinese) - ('', 'Hello=悠好=こんにちは', 'hello'), # alphabets and multibytes text + ('', 'Hello=悠好=こんにちは', 'Hello'), # alphabets and multibytes text ('', 'fünf', 'funf'), # latin1 (umlaut) ('', '0sphinx', 'sphinx'), # starts with number ('', 'sphinx-', 'sphinx'), # ends with hyphen @@ -206,7 +206,7 @@ def test_make_id(app, prefix, term, expected): def test_make_id_already_registered(app): document = create_new_document() - document.ids['term-sphinx'] = True # register "term-sphinx" manually + document.ids['term-Sphinx'] = True # register "term-Sphinx" manually assert make_id(app.env, document, 'term', 'Sphinx') == 'term-0'