From c621b718415fe9b378dd3897d48762c7a30d9e12 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sun, 17 Apr 2022 04:36:20 +0100 Subject: [PATCH] Increase minimum Docutils to 0.17 --- .github/workflows/main.yml | 4 +- pyproject.toml | 2 +- sphinx/addnodes.py | 17 +----- sphinx/directives/patches.py | 41 +++++++------- sphinx/transforms/__init__.py | 13 ++--- sphinx/transforms/i18n.py | 2 +- sphinx/util/docutils.py | 2 +- sphinx/util/nodes.py | 22 +------ tests/test_api_translator.py | 6 +- tests/test_build_html.py | 18 ++---- tests/test_ext_graphviz.py | 82 +++++++++------------------ tests/test_ext_inheritance_diagram.py | 67 +++++++--------------- tests/test_intl.py | 17 ++---- tests/test_markup.py | 3 - tox.ini | 7 +-- 15 files changed, 93 insertions(+), 210 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d88c10ab6dd..08fb55c8568 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,9 +14,9 @@ jobs: matrix: include: - python: "3.8" - docutils: du16 - - python: "3.9" docutils: du17 + - python: "3.9" + docutils: du18 - python: "3.10" docutils: du18 - python: "3.10" diff --git a/pyproject.toml b/pyproject.toml index 1e09d178d5f..5d53f412a63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dependencies = [ "sphinxcontrib-qthelp", "Jinja2>=3.0", "Pygments>=2.12", - "docutils>=0.14,<0.20", + "docutils>=0.17,<0.20", "snowballstemmer>=2.0", "babel>=2.9", "alabaster>=0.7,<0.8", diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index e22ce5fa20e..4abddf2057f 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -12,7 +12,7 @@ try: from docutils.nodes import meta as docutils_meta # type: ignore except ImportError: - # docutils-0.17 or older + # docutils-0.17 from docutils.parsers.rst.directives.html import MetaBody docutils_meta = MetaBody.meta @@ -29,18 +29,7 @@ class document(nodes.document): def set_id(self, node: Element, msgnode: Optional[Element] = None, suggested_prefix: str = '') -> str: - if docutils.__version_info__ >= (0, 16): - ret = super().set_id(node, msgnode, suggested_prefix) # type: ignore - else: - ret = super().set_id(node, msgnode) - - if docutils.__version_info__ < (0, 17): - # register other node IDs forcedly - for node_id in node['ids']: - if node_id not in self.ids: - self.ids[node_id] = node - - return ret + return super().set_id(node, msgnode, suggested_prefix) # type: ignore class translatable(nodes.Node): @@ -198,7 +187,7 @@ class desc_inline(_desc_classes_injector, nodes.Inline, nodes.TextElement): classes = ['sig', 'sig-inline'] def __init__(self, domain: str, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) + super().__init__(*args, **kwargs, domain=domain) self['classes'].append(domain) diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index 876683ad0c6..a0bf2626206 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -19,11 +19,29 @@ from sphinx.util.typing import OptionSpec try: - from docutils.parsers.rst.directives.misc import Meta as MetaBase # type: ignore + from docutils.parsers.rst.directives.misc import Meta as Meta # type: ignore except ImportError: - # docutils-0.17 or older + # docutils-0.17 from docutils.parsers.rst.directives.html import Meta as MetaBase + class Meta(MetaBase, SphinxDirective): # type: ignore + def run(self) -> Sequence[Node]: # type: ignore + result = super().run() + for node in result: + # for docutils-0.17. Since docutils-0.18, patching is no longer needed + # because it uses picklable node; ``docutils.nodes.meta``. + if (isinstance(node, nodes.pending) and + isinstance(node.details['nodes'][0], addnodes.docutils_meta)): + meta = node.details['nodes'][0] + meta.source = self.env.doc2path(self.env.docname) + meta.line = self.lineno + meta.rawcontent = meta['content'] + + # docutils' meta nodes aren't picklable because the class is nested + meta.__class__ = addnodes.meta + + return result + if TYPE_CHECKING: from sphinx.application import Sphinx @@ -57,25 +75,6 @@ def run(self) -> List[Node]: return [figure_node] -class Meta(MetaBase, SphinxDirective): - def run(self) -> Sequence[Node]: - result = super().run() - for node in result: - # for docutils-0.17 or older. Since docutils-0.18, patching is no longer needed - # because it uses picklable node; ``docutils.nodes.meta``. - if (isinstance(node, nodes.pending) and - isinstance(node.details['nodes'][0], addnodes.docutils_meta)): - meta = node.details['nodes'][0] - meta.source = self.env.doc2path(self.env.docname) - meta.line = self.lineno - meta.rawcontent = meta['content'] - - # docutils' meta nodes aren't picklable because the class is nested - meta.__class__ = addnodes.meta - - return result - - class CSVTable(tables.CSVTable): """The csv-table directive which searches a CSV file from Sphinx project's source directory when an absolute path is given via :file: option. diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 2d6a027e319..9ca95a73fca 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -4,7 +4,6 @@ import unicodedata from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Tuple, cast -import docutils from docutils import nodes from docutils.nodes import Element, Node, Text from docutils.transforms import Transform, Transformer @@ -342,16 +341,12 @@ def get_tokens(self, txtnodes: List[Text]) -> Generator[Tuple[str, str], None, N # of "Text" nodes (interface to ``smartquotes.educate_tokens()``). for txtnode in txtnodes: if is_smartquotable(txtnode): - if docutils.__version_info__ >= (0, 16): - # SmartQuotes uses backslash escapes instead of null-escapes - text = re.sub(r'(?<=\x00)([-\\\'".`])', r'\\\1', str(txtnode)) - else: - text = txtnode.astext() - - yield ('plain', text) + # SmartQuotes uses backslash escapes instead of null-escapes + text = re.sub(r'(?<=\x00)([-\\\'".`])', r'\\\1', str(txtnode)) + yield 'plain', text else: # skip smart quotes - yield ('literal', txtnode.astext()) + yield 'literal', txtnode.astext() class DoctreeReadEvent(SphinxTransform): diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 160c7f35d24..fd32f0dc17d 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -255,7 +255,7 @@ def apply(self, **kwargs: Any) -> None: # update meta nodes if isinstance(node, nodes.pending) and is_pending_meta(node): - # docutils-0.17 or older + # docutils-0.17 node.details['nodes'][0]['content'] = msgstr continue elif isinstance(node, addnodes.docutils_meta): diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index cde09058e4d..63c9a06efc9 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -591,7 +591,7 @@ def unknown_visit(self, node: Node) -> None: # Node.findall() is a new interface to traverse a doctree since docutils-0.18. # This applies a patch to docutils up to 0.18 inclusive to provide Node.findall() # method to use it from our codebase. -if docutils.__version_info__ <= (0, 18): +if docutils.__version_info__[:2] <= (0, 17): def findall(self, *args, **kwargs): return iter(self.traverse(*args, **kwargs)) diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 0646b44e642..fdbf94fe659 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -226,7 +226,7 @@ def is_translatable(node: Node) -> bool: return True if is_pending_meta(node) or isinstance(node, addnodes.meta): - # docutils-0.17 or older + # docutils-0.17 return True elif isinstance(node, addnodes.docutils_meta): # docutils-0.18+ @@ -268,10 +268,10 @@ def extract_messages(doctree: Element) -> Iterable[Tuple[Element, str]]: else: msg = '' elif isinstance(node, META_TYPE_NODES): - # docutils-0.17 or older + # docutils-0.17 msg = node.rawcontent elif isinstance(node, nodes.pending) and is_pending_meta(node): - # docutils-0.17 or older + # docutils-0.17 msg = node.details['nodes'][0].rawcontent elif isinstance(node, addnodes.docutils_meta): # docutils-0.18+ @@ -625,19 +625,3 @@ def process_only_nodes(document: Node, tags: "Tags") -> None: # the only node, so we make sure docutils can transfer the id to # something, even if it's just a comment and will lose the id anyway... node.replace_self(nodes.comment()) - - -def _new_copy(self: Element) -> Element: - """monkey-patch Element.copy to copy the rawsource and line - for docutils-0.16 or older versions. - - refs: https://sourceforge.net/p/docutils/patches/165/ - """ - newnode = self.__class__(self.rawsource, **self.attributes) - if isinstance(self, nodes.Element): - newnode.source = self.source - newnode.line = self.line - return newnode - - -nodes.Element.copy = _new_copy # type: ignore diff --git a/tests/test_api_translator.py b/tests/test_api_translator.py index 25aee0c6126..2185fb8db1b 100644 --- a/tests/test_api_translator.py +++ b/tests/test_api_translator.py @@ -2,7 +2,6 @@ import sys -import docutils import pytest @@ -19,10 +18,7 @@ def test_html_translator(app, status, warning): # no set_translator() translator_class = app.builder.get_translator_class() assert translator_class - if docutils.__version_info__ < (0, 13): - assert translator_class.__name__ == 'HTMLTranslator' - else: - assert translator_class.__name__ == 'HTML5Translator' + assert translator_class.__name__ == 'HTML5Translator' @pytest.mark.sphinx('html', testroot='api-set-translator') diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 072f187ba99..10fdbdf79f6 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -15,10 +15,7 @@ from sphinx.util import md5 from sphinx.util.inventory import InventoryFile -if docutils.__version_info__ < (0, 17): - FIGURE_CAPTION = ".//div[@class='figure align-default']/p[@class='caption']" -else: - FIGURE_CAPTION = ".//figure/figcaption/p" +FIGURE_CAPTION = ".//figure/figcaption/p" ENV_WARNINGS = """\ @@ -442,7 +439,7 @@ def test_docutils17_output(app, cached_etree_parse, fname, expect): check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) -@pytest.mark.skipif(docutils.__version_info__ < (0, 18), reason='docutils-0.18+ is required.') +@pytest.mark.skipif(docutils.__version_info__[:2] <= (0, 17), reason='docutils-0.18+ is required.') @pytest.mark.parametrize("fname,expect", flat_dict({ 'index.html': [ (".//div[@class='citation']/span", r'Ref1'), @@ -1324,14 +1321,9 @@ def test_html_inventory(app): def test_html_anchor_for_figure(app): app.builder.build_all() content = (app.outdir / 'index.html').read_text(encoding='utf8') - if docutils.__version_info__ < (0, 17): - assert ('

The caption of pic' - '

' - in content) - else: - assert ('
\n

The caption of pic' - '

\n
' - in content) + assert ('
\n

The caption of pic' + '

\n
' + in content) @pytest.mark.sphinx('html', testroot='directives-raw') diff --git a/tests/test_ext_graphviz.py b/tests/test_ext_graphviz.py index 649cf1861a5..28591674b1e 100644 --- a/tests/test_ext_graphviz.py +++ b/tests/test_ext_graphviz.py @@ -2,7 +2,6 @@ import re -import docutils import pytest from sphinx.ext.graphviz import ClickableMapDefinition @@ -14,15 +13,10 @@ def test_graphviz_png_html(app, status, warning): app.builder.build_all() content = (app.outdir / 'index.html').read_text(encoding='utf8') - if docutils.__version_info__ < (0, 17): - html = (r'
\s*' - r'
\s*

' - r'caption of graph.*

\s*
') - else: - html = (r'
\s*' - r'
\s*
\s*' - r'

caption of graph.*

\s*' - r'
\s*
') + html = (r'
\s*' + r'
\s*
\s*' + r'

caption of graph.*

\s*' + r'
\s*
') assert re.search(html, content, re.S) html = 'Hello
\n graphviz world' @@ -32,15 +26,10 @@ def test_graphviz_png_html(app, status, warning): 'class="graphviz neato-graph" />') assert re.search(html, content, re.S) - if docutils.__version_info__ < (0, 17): - html = (r'
\s*' - r'
\s*

' - r'on right.*

\s*
') - else: - html = (r'
\s*' - r'
\s*
\s*' - r'

on right.*

\s*' - r'
\s*
') + html = (r'
\s*' + r'
\s*
\s*' + r'

on right.*

\s*' + r'
\s*
') assert re.search(html, content, re.S) html = (r'
' @@ -58,24 +47,15 @@ def test_graphviz_svg_html(app, status, warning): content = (app.outdir / 'index.html').read_text(encoding='utf8') - if docutils.__version_info__ < (0, 17): - html = (r'
\n' - r'
\n' - r'\s*

digraph foo {\n' - r'bar -> baz\n' - r'}

\n' - r'

' - r'caption of graph.*

\n
') - else: - html = (r'
\n' - r'
\n' - r'\s*

digraph foo {\n' - r'bar -> baz\n' - r'}

\n' - r'
\n' - r'

caption of graph.*

\n' - r'
\n' - r'
') + html = (r'
\n' + r'
\n' + r'\s*

digraph foo {\n' + r'bar -> baz\n' + r'}

\n' + r'
\n' + r'

caption of graph.*

\n' + r'
\n' + r'
') assert re.search(html, content, re.S) html = (r'Hello
\n' @@ -83,25 +63,15 @@ def test_graphviz_svg_html(app, status, warning): r' graphviz world') assert re.search(html, content, re.S) - if docutils.__version_info__ < (0, 17): - html = (r'
\n' - r'
\n' - r'\s*

digraph bar {\n' - r'foo -> bar\n' - r'}

\n' - r'

' - r'on right.*

\n' - r'
') - else: - html = (r'
\n' - r'
\n' - r'\s*

digraph bar {\n' - r'foo -> bar\n' - r'}

\n' - r'
\n' - r'

on right.*

\n' - r'
\n' - r'
') + html = (r'
\n' + r'
\n' + r'\s*

digraph bar {\n' + r'foo -> bar\n' + r'}

\n' + r'
\n' + r'

on right.*

\n' + r'
\n' + r'
') assert re.search(html, content, re.S) html = (r'
' diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index 4f99ef5da94..7b1ea2b569c 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -4,7 +4,6 @@ import re import sys -import docutils import pytest from sphinx.ext.inheritance_diagram import (InheritanceDiagram, InheritanceException, @@ -140,20 +139,12 @@ def test_inheritance_diagram_png_html(app, status, warning): content = (app.outdir / 'index.html').read_text(encoding='utf8') - if docutils.__version_info__ < (0, 17): - pattern = ('
\n' - '
' - 'Inheritance diagram of test.Foo
\n

' - 'Test Foo!\xb6

\n
\n') - else: - pattern = ('
\n' - '
' - 'Inheritance diagram of test.Foo
\n
\n

' - 'Test Foo!\xb6

\n
\n
\n') + pattern = ('
\n' + '
' + 'Inheritance diagram of test.Foo
\n
\n

' + 'Test Foo!\xb6

\n
\n
\n') assert re.search(pattern, content, re.M) @@ -165,24 +156,14 @@ def test_inheritance_diagram_svg_html(app, status, warning): content = (app.outdir / 'index.html').read_text(encoding='utf8') - if docutils.__version_info__ < (0, 17): - pattern = ('
\n' - '
' - '\n' - '

Inheritance diagram of test.Foo

' - '
\n

' - 'Test Foo!\xb6

\n
\n') - else: - pattern = ('
\n' - '
' - '\n' - '

Inheritance diagram of test.Foo

' - '
\n
\n

' - 'Test Foo!\xb6

\n
\n
\n') + pattern = ('
\n' + '
' + '\n' + '

Inheritance diagram of test.Foo

' + '
\n
\n

' + 'Test Foo!\xb6

\n
\n
\n') assert re.search(pattern, content, re.M) @@ -216,20 +197,12 @@ def test_inheritance_diagram_latex_alias(app, status, warning): content = (app.outdir / 'index.html').read_text(encoding='utf8') - if docutils.__version_info__ < (0, 17): - pattern = ('
\n' - '
' - 'Inheritance diagram of test.Foo
\n

' - 'Test Foo!\xb6

\n
\n') - else: - pattern = ('
\n' - '
' - 'Inheritance diagram of test.Foo
\n
\n

' - 'Test Foo!\xb6

\n
\n
\n') + pattern = ('
\n' + '
' + 'Inheritance diagram of test.Foo
\n
\n

' + 'Test Foo!\xb6

\n
\n
\n') assert re.search(pattern, content, re.M) diff --git a/tests/test_intl.py b/tests/test_intl.py index 796d95bcc1e..9febfe0da26 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -6,7 +6,6 @@ import os import re -import docutils import pytest from babel.messages import mofile, pofile from babel.messages.catalog import Catalog @@ -1121,12 +1120,8 @@ def test_additional_targets_should_not_be_translated(app): result = (app.outdir / 'raw.html').read_text(encoding='utf8') # raw block should not be translated - if docutils.__version_info__ < (0, 17): - expected_expr = """
""" - assert_count(expected_expr, result, 1) - else: - expected_expr = """""" - assert_count(expected_expr, result, 1) + expected_expr = """""" + assert_count(expected_expr, result, 1) # [figure.txt] @@ -1203,12 +1198,8 @@ def test_additional_targets_should_be_translated(app): result = (app.outdir / 'raw.html').read_text(encoding='utf8') # raw block should be translated - if docutils.__version_info__ < (0, 17): - expected_expr = """
""" - assert_count(expected_expr, result, 1) - else: - expected_expr = """""" - assert_count(expected_expr, result, 1) + expected_expr = """""" + assert_count(expected_expr, result, 1) # [figure.txt] diff --git a/tests/test_markup.py b/tests/test_markup.py index f15761c5e5d..4978a501dce 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -3,7 +3,6 @@ import re import warnings -import docutils import pytest from docutils import frontend, nodes, utils from docutils.parsers.rst import Parser as RstParser @@ -395,8 +394,6 @@ def test_inline(get_verifier, type, rst, html_expected, latex_expected): None, ), ]) -@pytest.mark.skipif(docutils.__version_info__ < (0, 16), - reason='docutils-0.16 or above is required') def test_inline_docutils16(get_verifier, type, rst, html_expected, latex_expected): verifier = get_verifier(type) verifier(rst, html_expected, latex_expected) diff --git a/tox.ini b/tox.ini index efbff445bbe..bdd3cef7626 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.4.0 -envlist = docs,flake8,mypy,twine,py{38,39,310},du{14,15,16,17,18,19} +envlist = docs,flake8,mypy,twine,py{38,39,310},du{17,18,19} isolated_build = True [testenv] @@ -17,11 +17,8 @@ passenv = TERM description = py{38,39,310}: Run unit tests against {envname}. - du{14,15,16,17,18,19}: Run unit tests with the given version of docutils. + du{17,18,19}: Run unit tests with the given version of docutils. deps = - du14: docutils==0.14.* - du15: docutils==0.15.* - du16: docutils==0.16.* du17: docutils==0.17.* du18: docutils==0.18.* du19: docutils==0.19.*