diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5fdd20320..9a4cf2cbc3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,7 +11,7 @@ jobs: matrix: include: - python: "3.8" - docutils: du17 + docutils: du18 - python: "3.9" docutils: du18 - python: "3.10" diff --git a/setup.py b/setup.py index de9b8875b9..b6430c762c 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ 'sphinxcontrib-qthelp', 'Jinja2>=2.3', 'Pygments>=2.8.0', - 'docutils>=0.17,<0.19', + 'docutils>=0.18,<0.19', 'snowballstemmer>=1.1', 'babel>=1.3', 'alabaster>=0.7,<0.8', diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 455d0c6dfb..85c38e913d 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -2,19 +2,24 @@ from typing import TYPE_CHECKING, Any, Dict, List, Sequence -import docutils from docutils import nodes from docutils.nodes import Element +from sphinx.deprecation import RemovedInSphinx70Warning, deprecated_alias + if TYPE_CHECKING: from sphinx.application import Sphinx -try: - from docutils.nodes import meta as docutils_meta # type: ignore -except ImportError: - # docutils-0.17 - from docutils.parsers.rst.directives.html import MetaBody - docutils_meta = MetaBody.meta +deprecated_alias('sphinx.addnodes', + { + 'meta': nodes.meta, # type: ignore + 'docutils_meta': nodes.meta, # type: ignore + }, + RemovedInSphinx70Warning, + { + 'meta': 'docutils.nodes.meta', + 'docutils_meta': 'docutils.nodes.meta', + }) class document(nodes.document): @@ -424,13 +429,6 @@ class tabular_col_spec(nodes.Element): """Node for specifying tabular columns, used for LaTeX output.""" -class meta(nodes.Special, nodes.PreBibliographic, nodes.Element): - """Node for meta directive -- same as docutils' standard meta node, - but pickleable. - """ - rawcontent = None - - # inline nodes class pending_xref(nodes.Inline, nodes.Element): @@ -558,9 +556,6 @@ def setup(app: "Sphinx") -> Dict[str, Any]: app.add_node(literal_strong) app.add_node(manpage) - if docutils.__version_info__ < (0, 18): - app.add_node(meta) - return { 'version': 'builtin', 'parallel_read_safe': True, diff --git a/sphinx/directives/patches.py b/sphinx/directives/patches.py index a0bf262620..690a715ac1 100644 --- a/sphinx/directives/patches.py +++ b/sphinx/directives/patches.py @@ -1,14 +1,14 @@ import os from os import path -from typing import TYPE_CHECKING, Any, Dict, List, Sequence, cast +from typing import TYPE_CHECKING, Any, Dict, List, cast from docutils import nodes from docutils.nodes import Node, make_id from docutils.parsers.rst import directives from docutils.parsers.rst.directives import images, tables +from docutils.parsers.rst.directives.misc import Meta as Meta # type: ignore from docutils.parsers.rst.roles import set_classes -from sphinx import addnodes from sphinx.directives import optional_int from sphinx.domains.math import MathDomain from sphinx.locale import __ @@ -18,30 +18,6 @@ from sphinx.util.osutil import SEP, os_path, relpath from sphinx.util.typing import OptionSpec -try: - from docutils.parsers.rst.directives.misc import Meta as Meta # type: ignore -except ImportError: - # 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 diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 6bb497e74d..915b4c9ed4 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -9,9 +9,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, Iterator, List, Optional, Set, Tuple, Union) -import docutils from docutils import nodes -from docutils.nodes import Node from sphinx import addnodes from sphinx.config import Config @@ -52,8 +50,6 @@ 'file_insertion_enabled': True, 'smartquotes_locales': [], } -if docutils.__version_info__[:2] <= (0, 17): - default_settings['embed_images'] = False # This is increased every time an environment attribute is added # or changed to properly invalidate pickle files. @@ -537,7 +533,7 @@ def get_and_resolve_doctree(self, docname: str, builder: "Builder", def resolve_toctree(self, docname: str, builder: "Builder", toctree: addnodes.toctree, prune: bool = True, maxdepth: int = 0, titles_only: bool = False, - collapse: bool = False, includehidden: bool = False) -> Node: + collapse: bool = False, includehidden: bool = False) -> nodes.Node: """Resolve a *toctree* node into individual bullet lists with titles as items, returning None (if no containing titles are found) or a new node. diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index e4fdeec910..9705ef03a1 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -187,8 +187,8 @@ def __init__(self, document: nodes.document, lang: SearchLanguage) -> None: self.lang = lang def is_meta_keywords(self, node: Element) -> bool: - if (isinstance(node, (addnodes.meta, addnodes.docutils_meta)) and - node.get('name') == 'keywords'): + if (isinstance(node, nodes.meta) # type: ignore + and node.get('name') == 'keywords'): meta_lang = node.get('lang') if meta_lang is None: # lang not specified return True diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 200501a575..da242c5c56 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -19,7 +19,7 @@ from sphinx.util import get_filetype, logging, split_index_msg from sphinx.util.i18n import docname_to_domain from sphinx.util.nodes import (IMAGE_TYPE_NODES, LITERAL_TYPE_NODES, NodeMatcher, - extract_messages, is_pending_meta, traverse_translatable_index) + extract_messages, traverse_translatable_index) if TYPE_CHECKING: from sphinx.application import Sphinx @@ -253,12 +253,7 @@ def apply(self, **kwargs: Any) -> None: continue # update meta nodes - if isinstance(node, nodes.pending) and is_pending_meta(node): - # docutils-0.17 - node.details['nodes'][0]['content'] = msgstr - continue - elif isinstance(node, addnodes.docutils_meta): - # docutils-0.18+ + if isinstance(node, nodes.meta): # type: ignore node['content'] = msgstr continue diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index fde1c62aa5..b8b4108b54 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -549,16 +549,6 @@ def unknown_visit(self, node: Node) -> None: logger.warning(__('unknown node type: %r'), node, location=node) -# Node.findall() is a new interface to traverse a doctree since docutils-0.18. -# This applies a patch docutils-0.17 to be available Node.findall() -# method to use it from our codebase. -if docutils.__version_info__[:2] <= (0, 17): - def findall(self, *args, **kwargs): - return iter(self.traverse(*args, **kwargs)) - - Node.findall = findall # type: ignore - - # cache a vanilla instance of nodes.document # Used in new_document() function __document_cache__: Optional[nodes.document] = None diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 6a0ecf65d5..b6b115f2a8 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -182,14 +182,6 @@ def apply_source_workaround(node: Element) -> None: ) -def is_pending_meta(node: Node) -> bool: - if (isinstance(node, nodes.pending) and - isinstance(node.details.get('nodes', [None])[0], addnodes.meta)): - return True - else: - return False - - def is_translatable(node: Node) -> bool: if isinstance(node, addnodes.translatable): return True @@ -225,11 +217,7 @@ def is_translatable(node: Node) -> bool: return False return True - if is_pending_meta(node) or isinstance(node, addnodes.meta): - # docutils-0.17 - return True - elif isinstance(node, addnodes.docutils_meta): - # docutils-0.18+ + if isinstance(node, nodes.meta): # type: ignore return True return False @@ -244,9 +232,6 @@ def is_translatable(node: Node) -> bool: IMAGE_TYPE_NODES = ( nodes.image, ) -META_TYPE_NODES = ( - addnodes.meta, -) def extract_messages(doctree: Element) -> Iterable[Tuple[Element, str]]: @@ -267,14 +252,7 @@ def extract_messages(doctree: Element) -> Iterable[Tuple[Element, str]]: msg = '.. image:: %s' % node['uri'] else: msg = '' - elif isinstance(node, META_TYPE_NODES): - # docutils-0.17 - msg = node.rawcontent - elif isinstance(node, nodes.pending) and is_pending_meta(node): - # docutils-0.17 - msg = node.details['nodes'][0].rawcontent - elif isinstance(node, addnodes.docutils_meta): - # docutils-0.18+ + elif isinstance(node, nodes.meta): # type: ignore msg = node["content"] else: msg = node.rawsource.replace('\n', ' ').strip() diff --git a/tests/test_build_html.py b/tests/test_build_html.py index f35e89c94e..972d1566c6 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -5,7 +5,6 @@ from itertools import chain, cycle from unittest.mock import ANY, call, patch -import docutils import pygments import pytest from html5lib import HTMLParser @@ -402,39 +401,6 @@ def test_html5_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.17 or below is required.') -@pytest.mark.parametrize("fname,expect", flat_dict({ - 'index.html': [ - (".//dt[@class='label']/span[@class='brackets']", r'Ref1'), - (".//dt[@class='label']", ''), - ], - 'footnote.html': [ - (".//a[@class='footnote-reference brackets'][@href='#id9'][@id='id1']", r"1"), - (".//a[@class='footnote-reference brackets'][@href='#id10'][@id='id2']", r"2"), - (".//a[@class='footnote-reference brackets'][@href='#foo'][@id='id3']", r"3"), - (".//a[@class='reference internal'][@href='#bar'][@id='id4']/span", r"\[bar\]"), - (".//a[@class='reference internal'][@href='#baz-qux'][@id='id5']/span", r"\[baz_qux\]"), - (".//a[@class='footnote-reference brackets'][@href='#id11'][@id='id6']", r"4"), - (".//a[@class='footnote-reference brackets'][@href='#id12'][@id='id7']", r"5"), - (".//a[@class='fn-backref'][@href='#id1']", r"1"), - (".//a[@class='fn-backref'][@href='#id2']", r"2"), - (".//a[@class='fn-backref'][@href='#id3']", r"3"), - (".//a[@class='fn-backref'][@href='#id4']", r"bar"), - (".//a[@class='fn-backref'][@href='#id5']", r"baz_qux"), - (".//a[@class='fn-backref'][@href='#id6']", r"4"), - (".//a[@class='fn-backref'][@href='#id7']", r"5"), - (".//a[@class='fn-backref'][@href='#id8']", r"6"), - ], -})) -@pytest.mark.sphinx('html') -@pytest.mark.test_params(shared_result='test_build_html_output_docutils17') -def test_docutils17_output(app, cached_etree_parse, fname, expect): - app.build() - print(app.outdir / fname) - check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) - - -@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'), @@ -460,7 +426,7 @@ def test_docutils17_output(app, cached_etree_parse, fname, expect): })) @pytest.mark.sphinx('html') @pytest.mark.test_params(shared_result='test_build_html_output_docutils18') -def test_docutils18_output(app, cached_etree_parse, fname, expect): +def test_docutils_output(app, cached_etree_parse, fname, expect): app.build() print(app.outdir / fname) check_xpath(cached_etree_parse(app.outdir / fname), fname, *expect) diff --git a/tests/test_smartquotes.py b/tests/test_smartquotes.py index 7a18817f2b..1d4e8e1271 100644 --- a/tests/test_smartquotes.py +++ b/tests/test_smartquotes.py @@ -1,6 +1,5 @@ """Test smart quotes.""" -import docutils import pytest from html5lib import HTMLParser @@ -44,10 +43,7 @@ def test_man_builder(app, status, warning): app.build() content = (app.outdir / 'python.1').read_text(encoding='utf8') - if docutils.__version_info__ > (0, 18): - assert r'\-\- \(dqSphinx\(dq is a tool that makes it easy ...' in content - else: - assert r'\-\- "Sphinx" is a tool that makes it easy ...' in content + assert r'\-\- \(dqSphinx\(dq is a tool that makes it easy ...' in content @pytest.mark.sphinx(buildername='latex', testroot='smartquotes', freshenv=True) diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 107e215604..3172b31a14 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -4,17 +4,9 @@ import pytest -from sphinx import addnodes from sphinx.testing.util import SphinxTestApp from sphinx.versioning import add_uids, get_ratio, merge_doctrees -try: - from docutils.nodes import meta -except ImportError: - # docutils-0.18.0 or older - from docutils.parsers.rst.directives.html import MetaBody - meta = MetaBody.meta - app = original = original_uids = None @@ -62,8 +54,6 @@ def test_picklablility(): copy.settings.warning_stream = None copy.settings.env = None copy.settings.record_dependencies = None - for metanode in copy.findall(meta): - metanode.__class__ = addnodes.meta loaded = pickle.loads(pickle.dumps(copy, pickle.HIGHEST_PROTOCOL)) assert all(getattr(n, 'uid', False) for n in loaded.findall(is_paragraph)) diff --git a/tox.ini b/tox.ini index 089b83648c..a68332051a 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ description = py{36,37,38,39,310}: Run unit tests against {envname}. du{17,18,19}: Run unit tests with the given version of docutils. deps = - du17: docutils==0.17.* du18: docutils==0.18.* du19: docutils==0.19.* extras =