From 738691a1effb5ec176eb84d6b1d3893e0b640068 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sun, 17 Apr 2022 04:01:17 +0100 Subject: [PATCH 1/4] Clean up after dropping Python 3.6 --- .github/workflows/main.yml | 2 +- CHANGES | 2 +- doc/internals/release-process.rst | 16 +++++---- sphinx/domains/python.py | 2 +- sphinx/ext/autodoc/__init__.py | 7 ++-- sphinx/ext/autodoc/preserve_defaults.py | 4 +-- sphinx/ext/napoleon/docstring.py | 2 -- sphinx/pycode/ast.py | 6 ++-- sphinx/util/images.py | 2 +- sphinx/util/inspect.py | 35 +++++++++---------- sphinx/util/parallel.py | 12 ++----- sphinx/util/typing.py | 23 +++---------- tests/test_build.py | 20 +++++------ tests/test_domain_py.py | 6 ++-- tests/test_ext_autodoc.py | 14 ++++---- tests/test_ext_autodoc_autoproperty.py | 2 +- tests/test_ext_autodoc_configs.py | 4 +-- tests/test_ext_autodoc_preserve_defaults.py | 2 +- tests/test_pycode.py | 2 +- tests/test_pycode_ast.py | 2 +- tests/test_util_inspect.py | 10 +++--- tests/test_util_typing.py | 38 +++++++++++---------- tox.ini | 4 +-- 23 files changed, 95 insertions(+), 122 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a350bde734..4889bb9a81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ permissions: jobs: ubuntu: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 name: Python ${{ matrix.python }} (${{ matrix.docutils }}) strategy: fail-fast: false diff --git a/CHANGES b/CHANGES index 069503d7e3..ae031c69a4 100644 --- a/CHANGES +++ b/CHANGES @@ -4,7 +4,7 @@ Release 6.0.0 (in development) Dependencies ------------ -* Drop python 3.6 support +* #10468: Drop Python 3.6 support Incompatible changes -------------------- diff --git a/doc/internals/release-process.rst b/doc/internals/release-process.rst index 0d89831bc7..3463fc1eb7 100644 --- a/doc/internals/release-process.rst +++ b/doc/internals/release-process.rst @@ -113,13 +113,15 @@ April 2023 and shipping Python 3.8. This is a summary table with the current policy: -========== ========= ====== -Date Ubuntu Python -========== ========= ====== -April 2023 20.04 LTS 3.8+ ----------- --------- ------ -April 2025 22.04 LTS 3.10+ -========== ========= ====== +========== ========= ====== ====== +Date Ubuntu Python Sphinx +========== ========= ====== ====== +April 2021 18.04 LTS 3.6+ 4, 5 +---------- --------- ------ ------ +April 2023 20.04 LTS 3.8+ 6, 7 +---------- --------- ------ ------ +April 2025 22.04 LTS 3.10+ 8 +========== ========= ====== ====== Release procedures ------------------ diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 8bf3ceff63..7ce7fb8b3d 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -204,7 +204,7 @@ def unparse(node: ast.AST) -> List[Node]: return result else: - if sys.version_info < (3, 8): + if sys.version_info[:2] <= (3, 7): if isinstance(node, ast.Bytes): return [addnodes.desc_sig_literal_string('', repr(node.s))] elif isinstance(node, ast.Ellipsis): diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index cb230f3e23..546aa3fc5d 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -820,6 +820,8 @@ def sort_members(self, documenters: List[Tuple["Documenter", bool]], # sort by group; alphabetically within groups documenters.sort(key=lambda e: (e[0].member_order, e[0].name)) elif order == 'bysource': + # By default, member discovery order matches source order, + # as dicts are insertion-ordered from Python 3.7. if self.analyzer: # sort by source order, by virtue of the module analyzer tagorder = self.analyzer.tagorder @@ -828,11 +830,6 @@ def keyfunc(entry: Tuple[Documenter, bool]) -> int: fullname = entry[0].name.split('::')[1] return tagorder.get(fullname, len(tagorder)) documenters.sort(key=keyfunc) - else: - # Assume that member discovery order matches source order. - # This is a reasonable assumption in Python 3.6 and up, where - # module.__dict__ is insertion-ordered. - pass else: # alphabetical documenters.sort(key=lambda e: e[0].name) diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py index a12f42fcf1..4230335d73 100644 --- a/sphinx/ext/autodoc/preserve_defaults.py +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -48,9 +48,9 @@ def get_function_def(obj: Any) -> Optional[ast.FunctionDef]: def get_default_value(lines: List[str], position: ast.AST) -> Optional[str]: try: - if sys.version_info < (3, 8): # only for py38+ + if sys.version_info[:2] <= (3, 7): # only for py38+ return None - elif position.lineno == position.end_lineno: + if position.lineno == position.end_lineno: line = lines[position.lineno - 1] return line[position.col_offset:position.end_col_offset] else: diff --git a/sphinx/ext/napoleon/docstring.py b/sphinx/ext/napoleon/docstring.py index 6989c9d661..8472a528aa 100644 --- a/sphinx/ext/napoleon/docstring.py +++ b/sphinx/ext/napoleon/docstring.py @@ -597,8 +597,6 @@ def _parse(self) -> None: self._parsed_lines = self._consume_empty() if self._name and self._what in ('attribute', 'data', 'property'): - # Implicit stop using StopIteration no longer allowed in - # Python 3.7; see PEP 479 res: List[str] = [] try: res = self._parse_attribute_docstring() diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index d4646f0b72..c0d6fc463e 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -3,7 +3,7 @@ import sys from typing import Dict, List, Optional, Type, overload -if sys.version_info > (3, 8): +if sys.version_info[:2] >= (3, 8): import ast else: try: @@ -159,7 +159,7 @@ def visit_Constant(self, node: ast.Constant) -> str: # type: ignore if node.value is Ellipsis: return "..." elif isinstance(node.value, (int, float, complex)): - if self.code and sys.version_info > (3, 8): + if self.code and sys.version_info[:2] >= (3, 8): return ast.get_source_segment(self.code, node) # type: ignore else: return repr(node.value) @@ -219,7 +219,7 @@ def visit_Tuple(self, node: ast.Tuple) -> str: else: return "(" + ", ".join(self.visit(e) for e in node.elts) + ")" - if sys.version_info < (3, 8): + if sys.version_info[:2] <= (3, 7): # these ast nodes were deprecated in python 3.8 def visit_Bytes(self, node: ast.Bytes) -> str: return repr(node.s) diff --git a/sphinx/util/images.py b/sphinx/util/images.py index 2def252f49..2e13957f6a 100644 --- a/sphinx/util/images.py +++ b/sphinx/util/images.py @@ -107,5 +107,5 @@ def test_svg(h: bytes, f: Optional[BinaryIO]) -> Optional[str]: # install test_svg() to imghdr -# refs: https://docs.python.org/3.6/library/imghdr.html#imghdr.tests +# refs: https://docs.python.org/3.9/library/imghdr.html#imghdr.tests imghdr.tests.append(test_svg) diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 76e3ca3ac4..c3f4d1af3c 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -12,7 +12,8 @@ from importlib import import_module from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA from io import StringIO -from types import MethodType, ModuleType +from types import (ClassMethodDescriptorType, MethodDescriptorType, MethodType, ModuleType, + WrapperDescriptorType) from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast from sphinx.pycode.ast import ast # for py37 @@ -21,13 +22,6 @@ from sphinx.util.typing import ForwardRef from sphinx.util.typing import stringify as stringify_annotation -if sys.version_info > (3, 7): - from types import ClassMethodDescriptorType, MethodDescriptorType, WrapperDescriptorType -else: - ClassMethodDescriptorType = type(object.__init__) - MethodDescriptorType = type(str.join) - WrapperDescriptorType = type(dict.__dict__['fromkeys']) - logger = logging.getLogger(__name__) memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) @@ -149,7 +143,7 @@ def getslots(obj: Any) -> Optional[Dict]: def isNewType(obj: Any) -> bool: """Check the if object is a kind of NewType.""" - if sys.version_info >= (3, 10): + if sys.version_info[:2] >= (3, 10): return isinstance(obj, typing.NewType) else: __module__ = safe_getattr(obj, '__module__', None) @@ -335,19 +329,22 @@ def iswrappedcoroutine(obj: Any) -> bool: return False -def isasyncgenfunction(obj: Any) -> bool: - """Check if the object is async-gen function.""" - if hasattr(obj, '__code__') and inspect.isasyncgenfunction(obj): - # check obj.__code__ because isasyncgenfunction() crashes for custom method-like - # objects on python3.7 (see https://github.com/sphinx-doc/sphinx/issues/9838) - return True - else: - return False +if sys.version_info[:2] <= (3, 7): + def isasyncgenfunction(obj: Any) -> bool: + """Check if the object is async-gen function.""" + if hasattr(obj, '__code__') and inspect.isasyncgenfunction(obj): + # check obj.__code__ because isasyncgenfunction() crashes for custom method-like + # objects on python3.7 (see https://github.com/sphinx-doc/sphinx/issues/9838) + return True + else: + return False +else: + isasyncgenfunction = inspect.isasyncgenfunction def isproperty(obj: Any) -> bool: """Check if the object is property.""" - if sys.version_info >= (3, 8): + if sys.version_info[:2] >= (3, 8): from functools import cached_property # cached_property is available since py3.8 if isinstance(obj, cached_property): return True @@ -619,7 +616,7 @@ def evaluate_signature(sig: inspect.Signature, globalns: Optional[Dict] = None, """Evaluate unresolved type annotations in a signature object.""" def evaluate_forwardref(ref: ForwardRef, globalns: Dict, localns: Dict) -> Any: """Evaluate a forward reference.""" - if sys.version_info > (3, 9): + if sys.version_info[:2] >= (3, 9): return ref._evaluate(globalns, localns, frozenset()) else: return ref._evaluate(globalns, localns) diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index 16a95e0397..201e5b5e25 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -1,7 +1,6 @@ """Parallel building utilities.""" import os -import sys import time import traceback from math import sqrt @@ -18,13 +17,6 @@ logger = logging.getLogger(__name__) -if sys.platform != "win32": - ForkContext = multiprocessing.context.ForkContext - ForkProcess = multiprocessing.context.ForkProcess -else: - # For static typing, as ForkProcess doesn't exist on Windows - ForkContext = ForkProcess = Any - # our parallel functionality only works for the forking Process parallel_available = multiprocessing and os.name == 'posix' @@ -59,7 +51,7 @@ def __init__(self, nproc: int) -> None: # task arguments self._args: Dict[int, Optional[List[Any]]] = {} # list of subprocesses (both started and waiting) - self._procs: Dict[int, ForkProcess] = {} + self._procs: Dict[int, Any] = {} # list of receiving pipe connections of running subprocesses self._precvs: Dict[int, Any] = {} # list of receiving pipe connections of waiting subprocesses @@ -93,7 +85,7 @@ def add_task( self._result_funcs[tid] = result_func or (lambda arg, result: None) self._args[tid] = arg precv, psend = multiprocessing.Pipe(False) - context: ForkContext = multiprocessing.get_context('fork') + context: Any = multiprocessing.get_context('fork') proc = context.Process(target=self._process, args=(psend, task_func, arg)) self._procs[tid] = proc self._precvsWaiting[tid] = precv diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index a74ed30cad..b67ec3cd02 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -5,27 +5,14 @@ import warnings from struct import Struct from types import TracebackType -from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, Type, TypeVar, Union +from typing import (Any, Callable, Dict, ForwardRef, Generator, List, Optional, Tuple, Type, + TypeVar, Union) from docutils import nodes from docutils.parsers.rst.states import Inliner from sphinx.deprecation import RemovedInSphinx70Warning -if sys.version_info > (3, 7): - from typing import ForwardRef -else: - from typing import _ForwardRef # type: ignore - - class ForwardRef: - """A pseudo ForwardRef class for py36.""" - def __init__(self, arg: Any, is_argument: bool = True) -> None: - self.arg = arg - - def _evaluate(self, globalns: Dict, localns: Dict) -> Any: - ref = _ForwardRef(self.arg) - return ref._eval_type(globalns, localns) - try: from types import UnionType # type: ignore # python 3.10 or above except ImportError: @@ -137,7 +124,7 @@ def restify(cls: Optional[Type], mode: str = 'fully-qualified-except-typing') -> elif is_invalid_builtin_class(cls): return ':py:class:`%s%s`' % (modprefix, INVALID_BUILTIN_CLASSES[cls]) elif inspect.isNewType(cls): - if sys.version_info > (3, 10): + if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ return ':py:class:`%s%s.%s`' % (modprefix, cls.__module__, cls.__name__) else: @@ -211,7 +198,7 @@ def _restify_py37(cls: Optional[Type], mode: str = 'fully-qualified-except-typin return text elif isinstance(cls, typing._SpecialForm): return ':py:obj:`~%s.%s`' % (cls.__module__, cls._name) - elif sys.version_info >= (3, 11) and cls is typing.Any: + elif sys.version_info[:2] >= (3, 11) and cls is typing.Any: # handle bpo-46998 return f':py:obj:`~{cls.__module__}.{cls.__name__}`' elif hasattr(cls, '__qualname__'): @@ -362,7 +349,7 @@ def stringify(annotation: Any, mode: str = 'fully-qualified-except-typing') -> s else: return modprefix + '.'.join([annotation.__module__, annotation.__name__]) elif inspect.isNewType(annotation): - if sys.version_info > (3, 10): + if sys.version_info[:2] >= (3, 10): # newtypes have correct module info since Python 3.10+ return modprefix + '%s.%s' % (annotation.__module__, annotation.__name__) else: diff --git a/tests/test_build.py b/tests/test_build.py index c5ce79da37..77211bb4dd 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -1,7 +1,6 @@ """Test all builders.""" import sys -from textwrap import dedent from unittest import mock import pytest @@ -19,7 +18,7 @@ def request_session_head(url, **kwargs): @pytest.fixture def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): - # If supported, build in a non-ASCII source dir + # Build in a non-ASCII source dir test_name = '\u65e5\u672c\u8a9e' basedir = sphinx_test_tempdir / request.node.originalname srcdir = basedir / test_name @@ -27,18 +26,17 @@ def nonascii_srcdir(request, rootdir, sphinx_test_tempdir): (rootdir / 'test-root').copytree(srcdir) # add a doc with a non-ASCII file name to the source dir - (srcdir / (test_name + '.txt')).write_text(dedent(""" - nonascii file name page - ======================= - """), encoding='utf8') + (srcdir / (test_name + '.txt')).write_text(""" +nonascii file name page +======================= +""", encoding='utf8') root_doc = srcdir / 'index.txt' - root_doc.write_text(root_doc.read_text(encoding='utf8') + dedent(""" - .. toctree:: - - %(test_name)s/%(test_name)s - """ % {'test_name': test_name}), encoding='utf8') + root_doc.write_text(root_doc.read_text(encoding='utf8') + f""" +.. toctree:: +{test_name}/{test_name} +""", encoding='utf8') return srcdir diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 11e66fd279..a4e276226f 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -365,7 +365,7 @@ def test_parse_annotation_suppress(app): assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Dict") -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_parse_annotation_Literal(app): doctree = _parse_annotation("Literal[True, False]", app.env) assert_node(doctree, ([pending_xref, "Literal"], @@ -480,7 +480,7 @@ def test_pyfunction_with_binary_operators(app): [nodes.inline, "2**64"])])]) -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_pyfunction_signature_full_py38(app): # case: separator at head text = ".. py:function:: hello(*, a)" @@ -516,7 +516,7 @@ def test_pyfunction_signature_full_py38(app): [desc_parameter, desc_sig_operator, "/"])]) -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_pyfunction_with_number_literals(app): text = ".. py:function:: hello(age=0x10, height=1_6_0)" doctree = restructuredtext.parse(app, text) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index c994282f6e..c980b5b267 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1072,7 +1072,7 @@ def test_autodoc_descriptor(app): ] -@pytest.mark.skipif(sys.version_info < (3, 8), +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='cached_property is available since python3.8.') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_cached_property(app): @@ -1404,7 +1404,7 @@ def test_enum_class(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.enums.EnumCls', options) - if sys.version_info > (3, 11): + if sys.version_info[:2] >= (3, 11): args = ('(value, names=None, *, module=None, qualname=None, ' 'type=None, start=1, boundary=None)') else: @@ -1976,7 +1976,7 @@ def test_autodoc_TypeVar(app): ] -@pytest.mark.skipif(sys.version_info < (3, 9), reason='py39+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason='py39+ is required.') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_Annotated(app): options = {"members": None} @@ -2059,7 +2059,7 @@ def test_singledispatch(app): ] -@pytest.mark.skipif(sys.version_info < (3, 8), +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='singledispatchmethod is available since python3.8') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_singledispatchmethod(app): @@ -2088,7 +2088,7 @@ def test_singledispatchmethod(app): ] -@pytest.mark.skipif(sys.version_info < (3, 8), +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='singledispatchmethod is available since python3.8') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_singledispatchmethod_automethod(app): @@ -2108,7 +2108,7 @@ def test_singledispatchmethod_automethod(app): ] -@pytest.mark.skipif(sys.version_info > (3, 11), +@pytest.mark.skipif(sys.version_info[:2] >= (3, 11), reason=('cython does not support python-3.11 yet. ' 'see https://github.com/cython/cython/issues/4365')) @pytest.mark.skipif(pyximport is None, reason='cython is not installed') @@ -2142,7 +2142,7 @@ def test_cython(app): ] -@pytest.mark.skipif(sys.version_info < (3, 8), +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='typing.final is available since python3.8') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_final(app): diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_ext_autodoc_autoproperty.py index 3d50020b37..bf54be36c0 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_ext_autodoc_autoproperty.py @@ -40,7 +40,7 @@ def test_class_properties(app): ] -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_cached_properties(app): actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop') diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index dbd432c800..d97f3e6f94 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -661,7 +661,7 @@ def test_mocked_module_imports(app, warning): @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_typehints': "signature"}) def test_autodoc_typehints_signature(app): - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): type_o = "~typing.Optional[~typing.Any]" else: type_o = "~typing.Any" @@ -1399,7 +1399,7 @@ def test_autodoc_typehints_description_and_type_aliases(app): @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_typehints_format': "fully-qualified"}) def test_autodoc_typehints_format_fully_qualified(app): - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): type_o = "typing.Optional[typing.Any]" else: type_o = "typing.Any" diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_ext_autodoc_preserve_defaults.py index ba5ff6e62d..b5548ad25a 100644 --- a/tests/test_ext_autodoc_preserve_defaults.py +++ b/tests/test_ext_autodoc_preserve_defaults.py @@ -10,7 +10,7 @@ @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_preserve_defaults': True}) def test_preserve_defaults(app): - if sys.version_info < (3, 8): + if sys.version_info[:2] <= (3, 7): color = "16777215" else: color = "0xFFFFFF" diff --git a/tests/test_pycode.py b/tests/test_pycode.py index 1f9882eb98..cc3966b8df 100644 --- a/tests/test_pycode.py +++ b/tests/test_pycode.py @@ -185,7 +185,7 @@ def test_ModuleAnalyzer_find_attr_docs(): 'Qux.attr2': 17} -@pytest.mark.skipif(sys.version_info < (3, 8), +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='posonlyargs are available since python3.8.') def test_ModuleAnalyzer_find_attr_docs_for_posonlyargs_method(): code = ('class Foo(object):\n' diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index 31018bacaa..e79e9d99b2 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -58,7 +58,7 @@ def test_unparse_None(): assert ast.unparse(None) is None -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') @pytest.mark.parametrize('source,expected', [ ("lambda x=0, /, y=1, *args, z, **kwargs: x + y + z", "lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 3581b3946c..3c20908387 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -186,7 +186,7 @@ def test_signature_annotations(): # Space around '=' for defaults sig = inspect.signature(f7) - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig) == '(x: typing.Optional[int] = None, y: dict = {}) -> None' else: assert stringify_signature(sig) == '(x: int = None, y: dict = {}) -> None' @@ -260,20 +260,20 @@ def test_signature_annotations(): # show_return_annotation is False sig = inspect.signature(f7) - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig, show_return_annotation=False) == '(x: typing.Optional[int] = None, y: dict = {})' else: assert stringify_signature(sig, show_return_annotation=False) == '(x: int = None, y: dict = {})' # unqualified_typehints is True sig = inspect.signature(f7) - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): assert stringify_signature(sig, unqualified_typehints=True) == '(x: ~typing.Optional[int] = None, y: dict = {}) -> None' else: assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') @pytest.mark.sphinx(testroot='ext-autodoc') def test_signature_annotations_py38(app): from target.pep570 import bar, baz, foo, qux @@ -373,7 +373,7 @@ def test_signature_from_str_kwonly_args(): assert sig.parameters['b'].default == Parameter.empty -@pytest.mark.skipif(sys.version_info < (3, 8), +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python-3.8 or above is required') def test_signature_from_str_positionaly_only_args(): sig = inspect.signature_from_str('(a, b=0, /, c=1)') diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index a70f01431e..658aa71680 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -71,7 +71,7 @@ def test_restify_type_hints_containers(): ":py:class:`str`]") assert restify(Tuple[str, ...]) == ":py:class:`~typing.Tuple`\\ [:py:class:`str`, ...]" - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): assert restify(Tuple[()]) == ":py:class:`~typing.Tuple`\\ [()]" else: assert restify(Tuple[()]) == ":py:class:`~typing.Tuple`" @@ -89,6 +89,7 @@ def test_restify_type_hints_containers(): def test_restify_type_hints_Callable(): assert restify(Callable) == ":py:class:`~typing.Callable`" + assert restify(Callable[[str], int]) == (":py:class:`~typing.Callable`\\ " "[[:py:class:`str`], :py:class:`int`]") assert restify(Callable[..., int]) == (":py:class:`~typing.Callable`\\ " @@ -100,19 +101,20 @@ def test_restify_type_hints_Union(): assert restify(Union[str, None]) == ":py:obj:`~typing.Optional`\\ [:py:class:`str`]" assert restify(Union[int, str]) == (":py:obj:`~typing.Union`\\ " "[:py:class:`int`, :py:class:`str`]") - assert restify(Union[int, Integral]) == (":py:obj:`~typing.Union`\\ " "[:py:class:`int`, :py:class:`numbers.Integral`]") assert restify(Union[int, Integral], "smart") == (":py:obj:`~typing.Union`\\ " "[:py:class:`int`," " :py:class:`~numbers.Integral`]") - assert (restify(Union[MyClass1, MyClass2]) == (":py:obj:`~typing.Union`\\ " - "[:py:class:`tests.test_util_typing.MyClass1`, " - ":py:class:`tests.test_util_typing.`]")) - assert (restify(Union[MyClass1, MyClass2], "smart") == (":py:obj:`~typing.Union`\\ " - "[:py:class:`~tests.test_util_typing.MyClass1`," - " :py:class:`~tests.test_util_typing.`]")) + assert (restify(Union[MyClass1, MyClass2]) == + (":py:obj:`~typing.Union`\\ " + "[:py:class:`tests.test_util_typing.MyClass1`, " + ":py:class:`tests.test_util_typing.`]")) + assert (restify(Union[MyClass1, MyClass2], "smart") == + (":py:obj:`~typing.Union`\\ " + "[:py:class:`~tests.test_util_typing.MyClass1`," + " :py:class:`~tests.test_util_typing.`]")) def test_restify_type_hints_typevars(): @@ -132,7 +134,7 @@ def test_restify_type_hints_typevars(): assert restify(List[T]) == ":py:class:`~typing.List`\\ [:py:obj:`tests.test_util_typing.T`]" assert restify(List[T], "smart") == ":py:class:`~typing.List`\\ [:py:obj:`~tests.test_util_typing.T`]" - if sys.version_info >= (3, 10): + if sys.version_info[:2] >= (3, 10): assert restify(MyInt) == ":py:class:`tests.test_util_typing.MyInt`" assert restify(MyInt, "smart") == ":py:class:`~tests.test_util_typing.MyInt`" else: @@ -160,13 +162,13 @@ def test_restify_type_ForwardRef(): assert restify(ForwardRef("myint")) == ":py:class:`myint`" -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_restify_type_Literal(): from typing import Literal # type: ignore assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" -@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason='python 3.9+ is required.') def test_restify_pep_585(): assert restify(list[str]) == ":py:class:`list`\\ [:py:class:`str`]" # type: ignore assert restify(dict[str, str]) == (":py:class:`dict`\\ " # type: ignore @@ -176,7 +178,7 @@ def test_restify_pep_585(): "[:py:class:`int`, ...]]") -@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_restify_type_union_operator(): assert restify(int | None) == ":py:class:`int` | :py:obj:`None`" # type: ignore assert restify(int | str) == ":py:class:`int` | :py:class:`str`" # type: ignore @@ -250,7 +252,7 @@ def test_stringify_type_hints_containers(): assert stringify(Tuple[str, ...], "fully-qualified") == "typing.Tuple[str, ...]" assert stringify(Tuple[str, ...], "smart") == "~typing.Tuple[str, ...]" - if sys.version_info < (3, 11): + if sys.version_info[:2] <= (3, 10): assert stringify(Tuple[()]) == "Tuple[()]" assert stringify(Tuple[()], "fully-qualified") == "typing.Tuple[()]" assert stringify(Tuple[()], "smart") == "~typing.Tuple[()]" @@ -272,7 +274,7 @@ def test_stringify_type_hints_containers(): assert stringify(Generator[None, None, None], "smart") == "~typing.Generator[None, None, None]" -@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason='python 3.9+ is required.') def test_stringify_type_hints_pep_585(): assert stringify(list[int]) == "list[int]" assert stringify(list[int], "smart") == "list[int]" @@ -299,7 +301,7 @@ def test_stringify_type_hints_pep_585(): assert stringify(type[int], "smart") == "type[int]" -@pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 8), reason='python 3.9+ is required.') def test_stringify_Annotated(): from typing import Annotated # type: ignore assert stringify(Annotated[str, "foo", "bar"]) == "str" @@ -379,7 +381,7 @@ def test_stringify_type_hints_typevars(): assert stringify(List[T]) == "List[tests.test_util_typing.T]" assert stringify(List[T], "smart") == "~typing.List[~tests.test_util_typing.T]" - if sys.version_info >= (3, 10): + if sys.version_info[:2] >= (3, 10): assert stringify(MyInt) == "tests.test_util_typing.MyInt" assert stringify(MyInt, "smart") == "~tests.test_util_typing.MyInt" else: @@ -406,7 +408,7 @@ def test_stringify_type_hints_alias(): assert stringify(MyTuple, "smart") == "~typing.Tuple[str, str]" # type: ignore -@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_stringify_type_Literal(): from typing import Literal # type: ignore assert stringify(Literal[1, "2", "\r"]) == "Literal[1, '2', '\\r']" @@ -414,7 +416,7 @@ def test_stringify_type_Literal(): assert stringify(Literal[1, "2", "\r"], "smart") == "~typing.Literal[1, '2', '\\r']" -@pytest.mark.skipif(sys.version_info < (3, 10), reason='python 3.10+ is required.') +@pytest.mark.skipif(sys.version_info[:2] <= (3, 9), reason='python 3.10+ is required.') def test_stringify_type_union_operator(): assert stringify(int | None) == "int | None" # type: ignore assert stringify(int | None, "smart") == "int | None" # type: ignore diff --git a/tox.ini b/tox.ini index dfae569cfa..31b7c5b31c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.4.0 -envlist = docs,flake8,mypy,twine,py{36,37,38,39,310,311},du{14,15,16,17,18,19} +envlist = docs,flake8,mypy,twine,py{37,38,39,310,311},du{14,15,16,17,18,19} isolated_build = True [testenv] @@ -16,7 +16,7 @@ passenv = EPUBCHECK_PATH TERM description = - py{36,37,38,39,310,311}: Run unit tests against {envname}. + py{37,38,39,310,311}: Run unit tests against {envname}. du{14,15,16,17,18,19}: Run unit tests with the given version of docutils. deps = du14: docutils==0.14.* From 0bfcb6bcd226357d089cd78c8c785db44593ff30 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Mon, 18 Apr 2022 17:33:56 +0100 Subject: [PATCH 2/4] Drop Python 3.7 --- .github/workflows/main.yml | 2 - CHANGES | 1 + doc/usage/installation.rst | 2 +- pyproject.toml | 7 +- sphinx/directives/other.py | 2 +- sphinx/domains/python.py | 17 +--- sphinx/domains/std.py | 18 ++-- sphinx/environment/adapters/toctree.py | 4 +- sphinx/environment/collectors/toctree.py | 2 +- sphinx/ext/autodoc/preserve_defaults.py | 10 +- sphinx/ext/autodoc/type_comment.py | 11 +-- sphinx/locale/__init__.py | 98 +++++++++---------- sphinx/pycode/ast.py | 64 +++--------- sphinx/pycode/parser.py | 23 +++-- sphinx/util/inspect.py | 39 ++------ .../test-ext-autodoc/target/partialmethod.py | 1 - tests/roots/test-ext-autodoc/target/pep570.py | 11 --- tests/test_domain_py.py | 61 +++++------- tests/test_ext_autodoc.py | 8 -- tests/test_ext_autodoc_autoproperty.py | 3 - tests/test_ext_autodoc_preserve_defaults.py | 7 +- tests/test_pycode.py | 2 - tests/test_pycode_ast.py | 24 ++--- tests/test_util_inspect.py | 19 ++-- tests/test_util_typing.py | 2 - tests/typing_test_data.py | 16 +++ tox.ini | 4 +- 27 files changed, 167 insertions(+), 291 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4889bb9a81..d88c10ab6d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,8 +13,6 @@ jobs: fail-fast: false matrix: include: - - python: "3.7" - docutils: du15 - python: "3.8" docutils: du16 - python: "3.9" diff --git a/CHANGES b/CHANGES index ae031c69a4..94f8eeb70f 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,7 @@ Dependencies ------------ * #10468: Drop Python 3.6 support +* #10470: Drop Python 3.7 support. Patch by Adam Turner Incompatible changes -------------------- diff --git a/doc/usage/installation.rst b/doc/usage/installation.rst index 073cd6f148..9a60c541e8 100644 --- a/doc/usage/installation.rst +++ b/doc/usage/installation.rst @@ -12,7 +12,7 @@ Installing Sphinx Overview -------- -Sphinx is written in `Python`__ and supports Python 3.7+. It builds upon the +Sphinx is written in `Python`__ and supports Python 3.8+. It builds upon the shoulders of many third-party libraries such as `Docutils`__ and `Jinja`__, which are installed when Sphinx is installed. diff --git a/pyproject.toml b/pyproject.toml index 8c8bc361a5..9f96f0d26a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ urls.Download = "https://pypi.org/project/Sphinx/" urls.Homepage = "https://www.sphinx-doc.org/" urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues" license.text = "BSD" -requires-python = ">=3.7" +requires-python = ">=3.8" # Classifiers list: https://pypi.org/classifiers/ classifiers = [ @@ -30,7 +30,6 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -89,13 +88,11 @@ lint = [ "mypy>=0.981", "sphinx-lint", "docutils-stubs", - "types-typed-ast", "types-requests", ] test = [ "pytest>=4.6", "html5lib", - "typed_ast; python_version < '3.8'", "cython", ] @@ -144,7 +141,7 @@ disallow_incomplete_defs = true follow_imports = "skip" ignore_missing_imports = true no_implicit_optional = true -python_version = "3.7" +python_version = "3.8" show_column_numbers = true show_error_codes = true show_error_context = true diff --git a/sphinx/directives/other.py b/sphinx/directives/other.py index fa8fc191a9..5292215a30 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -77,7 +77,7 @@ def run(self) -> List[Node]: return ret def parse_content(self, toctree: addnodes.toctree) -> List[Node]: - generated_docnames = frozenset(self.env.domains['std'].initial_data['labels'].keys()) + generated_docnames = frozenset(self.env.domains['std']._virtual_doc_names) suffixes = self.config.source_suffix # glob target documents diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 7ce7fb8b3d..fe2d52eb18 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -1,9 +1,9 @@ """The Python domain.""" +import ast import builtins import inspect import re -import sys import typing from inspect import Parameter from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, Type, cast @@ -21,7 +21,6 @@ from sphinx.domains import Domain, Index, IndexEntry, ObjType from sphinx.environment import BuildEnvironment from sphinx.locale import _, __ -from sphinx.pycode.ast import ast from sphinx.pycode.ast import parse as ast_parse from sphinx.roles import XRefRole from sphinx.util import logging @@ -138,7 +137,7 @@ def unparse(node: ast.AST) -> List[Node]: return [addnodes.desc_sig_space(), addnodes.desc_sig_punctuation('', '|'), addnodes.desc_sig_space()] - elif isinstance(node, ast.Constant): # type: ignore + elif isinstance(node, ast.Constant): if node.value is Ellipsis: return [addnodes.desc_sig_punctuation('', "...")] elif isinstance(node.value, bool): @@ -204,18 +203,6 @@ def unparse(node: ast.AST) -> List[Node]: return result else: - if sys.version_info[:2] <= (3, 7): - if isinstance(node, ast.Bytes): - return [addnodes.desc_sig_literal_string('', repr(node.s))] - elif isinstance(node, ast.Ellipsis): - return [addnodes.desc_sig_punctuation('', "...")] - elif isinstance(node, ast.NameConstant): - return [nodes.Text(node.value)] - elif isinstance(node, ast.Num): - return [addnodes.desc_sig_literal_string('', repr(node.n))] - elif isinstance(node, ast.Str): - return [addnodes.desc_sig_literal_string('', repr(node.s))] - raise SyntaxError # unsupported syntax try: diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index c6a05875f0..aa6045f667 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -1,10 +1,9 @@ """The standard domain.""" import re -import sys from copy import copy -from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, - Tuple, Type, Union, cast) +from typing import (TYPE_CHECKING, Any, Callable, Dict, Final, Iterable, Iterator, List, + Optional, Tuple, Type, Union, cast) from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -29,11 +28,6 @@ logger = logging.getLogger(__name__) -if sys.version_info[:2] >= (3, 8): - from typing import Final -else: - Final = Any - # RE for option descriptions option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)') # RE for grammar tokens @@ -589,7 +583,7 @@ class StandardDomain(Domain): 'doc': XRefRole(warn_dangling=True, innernodeclass=nodes.inline), } - initial_data: Final = { + initial_data: Final = { # type: ignore[misc] 'progoptions': {}, # (program, name) -> docname, labelid 'objects': {}, # (type, name) -> docname, labelid 'labels': { # labelname -> docname, labelid, sectionname @@ -604,6 +598,12 @@ class StandardDomain(Domain): }, } + _virtual_doc_names: Dict[str, Tuple[str, str]] = { # labelname -> docname, sectionname + 'genindex': ('genindex', _('Index')), + 'modindex': ('py-modindex', _('Module Index')), + 'search': ('search', _('Search Page')), + } + dangling_warnings = { 'term': 'term not in glossary: %(target)r', 'numref': 'undefined label: %(target)r', diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index 348832efe9..c8090d84b8 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -54,7 +54,7 @@ def resolve(self, docname: str, builder: "Builder", toctree: addnodes.toctree, """ if toctree.get('hidden', False) and not includehidden: return None - generated_docnames: Dict[str, Tuple[str, str, str]] = self.env.domains['std'].initial_data['labels'].copy() # NoQA: E501 + generated_docnames: Dict[str, Tuple[str, str]] = self.env.domains['std']._virtual_doc_names.copy() # NoQA: E501 # For reading the following two helper function, it is useful to keep # in mind the node structure of a toctree (using HTML-like node names @@ -141,7 +141,7 @@ def _entries_from_toctree(toctreenode: addnodes.toctree, parents: List[str], # don't show subitems toc = nodes.bullet_list('', item) elif ref in generated_docnames: - docname, _, sectionname = generated_docnames[ref] + docname, sectionname = generated_docnames[ref] if not title: title = sectionname reference = nodes.reference('', title, internal=True, diff --git a/sphinx/environment/collectors/toctree.py b/sphinx/environment/collectors/toctree.py index 68c730504a..628f4a444f 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -236,7 +236,7 @@ def _walk_toctree(toctreenode: addnodes.toctree, depth: int) -> None: def assign_figure_numbers(self, env: BuildEnvironment) -> List[str]: """Assign a figure number to each figure under a numbered toctree.""" - generated_docnames = frozenset(env.domains['std'].initial_data['labels'].keys()) + generated_docnames = frozenset(env.domains['std']._virtual_doc_names) rewrite_needed = [] diff --git a/sphinx/ext/autodoc/preserve_defaults.py b/sphinx/ext/autodoc/preserve_defaults.py index 4230335d73..0c21c30811 100644 --- a/sphinx/ext/autodoc/preserve_defaults.py +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -6,8 +6,6 @@ import ast import inspect -import sys -from inspect import Parameter from typing import Any, Dict, List, Optional from sphinx.application import Sphinx @@ -48,8 +46,6 @@ def get_function_def(obj: Any) -> Optional[ast.FunctionDef]: def get_default_value(lines: List[str], position: ast.AST) -> Optional[str]: try: - if sys.version_info[:2] <= (3, 7): # only for py38+ - return None if position.lineno == position.end_lineno: line = lines[position.lineno - 1] return line[position.col_offset:position.end_col_offset] @@ -89,18 +85,18 @@ def update_defvalue(app: Sphinx, obj: Any, bound_method: bool) -> None: default = defaults.pop(0) value = get_default_value(lines, default) if value is None: - value = ast_unparse(default) # type: ignore + value = ast_unparse(default) parameters[i] = param.replace(default=DefaultValue(value)) else: default = kw_defaults.pop(0) value = get_default_value(lines, default) if value is None: - value = ast_unparse(default) # type: ignore + value = ast_unparse(default) parameters[i] = param.replace(default=DefaultValue(value)) if bound_method and inspect.ismethod(obj): # classmethods - cls = inspect.Parameter('cls', Parameter.POSITIONAL_OR_KEYWORD) + cls = inspect.Parameter('cls', inspect.Parameter.POSITIONAL_OR_KEYWORD) parameters.insert(0, cls) sig = sig.replace(parameters=parameters) diff --git a/sphinx/ext/autodoc/type_comment.py b/sphinx/ext/autodoc/type_comment.py index 9881ae747b..b500a08c1a 100644 --- a/sphinx/ext/autodoc/type_comment.py +++ b/sphinx/ext/autodoc/type_comment.py @@ -1,12 +1,12 @@ """Update annotations info of living objects using type_comments.""" +import ast from inspect import Parameter, Signature, getsource from typing import Any, Dict, List, cast import sphinx from sphinx.application import Sphinx from sphinx.locale import __ -from sphinx.pycode.ast import ast from sphinx.pycode.ast import parse as ast_parse from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import inspect, logging @@ -34,10 +34,9 @@ def signature_from_ast(node: ast.FunctionDef, bound_method: bool, :param bound_method: Specify *node* is a bound method or not """ params = [] - if hasattr(node.args, "posonlyargs"): # for py38+ - for arg in node.args.posonlyargs: # type: ignore - param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment) - params.append(param) + for arg in node.args.posonlyargs: + param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment) + params.append(param) for arg in node.args.args: param = Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD, @@ -80,7 +79,7 @@ def get_type_comment(obj: Any, bound_method: bool = False) -> Signature: """Get type_comment'ed FunctionDef object from living object. This tries to parse original code for living object and returns - Signature for given *obj*. It requires py38+ or typed_ast module. + Signature for given *obj*. """ try: source = getsource(obj) diff --git a/sphinx/locale/__init__.py b/sphinx/locale/__init__.py index 6d4465a094..7942079c86 100644 --- a/sphinx/locale/__init__.py +++ b/sphinx/locale/__init__.py @@ -2,29 +2,25 @@ import gettext import locale -from collections import UserString, defaultdict +from collections import defaultdict from gettext import NullTranslations from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union -class _TranslationProxy(UserString): +class _TranslationProxy: """ Class for proxy strings from gettext translations. This is a helper for the lazy_* functions from this module. The proxy implementation attempts to be as complete as possible, so that the lazy objects should mostly work as expected, for example for sorting. - - This inherits from UserString because some docutils versions use UserString - for their Text nodes, which then checks its argument for being either a - basestring or UserString, otherwise calls str() -- not unicode() -- on it. """ __slots__ = ('_func', '_args') - def __new__(cls, func: Callable, *args: str) -> object: # type: ignore + def __new__(cls, func: Callable, *args: str) -> "_TranslationProxy": if not args: # not called with "function" and "arguments", but a plain string - return str(func) + return str(func) # type: ignore[return-value] return object.__new__(cls) def __getnewargs__(self) -> Tuple[str]: @@ -34,50 +30,14 @@ def __init__(self, func: Callable, *args: str) -> None: self._func = func self._args = args - @property - def data(self) -> str: # type: ignore - return self._func(*self._args) - - # replace function from UserString; it instantiates a self.__class__ - # for the encoding result - - def encode(self, encoding: str = None, errors: str = None) -> bytes: # type: ignore - if encoding: - if errors: - return self.data.encode(encoding, errors) - else: - return self.data.encode(encoding) - else: - return self.data.encode() + def __str__(self) -> str: + return str(self._func(*self._args)) def __dir__(self) -> List[str]: return dir(str) - def __str__(self) -> str: - return str(self.data) - - def __add__(self, other: str) -> str: # type: ignore - return self.data + other - - def __radd__(self, other: str) -> str: # type: ignore - return other + self.data - - def __mod__(self, other: str) -> str: # type: ignore - return self.data % other - - def __rmod__(self, other: str) -> str: # type: ignore - return other % self.data - - def __mul__(self, other: Any) -> str: # type: ignore - return self.data * other - - def __rmul__(self, other: Any) -> str: # type: ignore - return other * self.data - def __getattr__(self, name: str) -> Any: - if name == '__members__': - return self.__dir__() - return getattr(self.data, name) + return getattr(self.__str__(), name) def __getstate__(self) -> Tuple[Callable, Tuple[str, ...]]: return self._func, self._args @@ -86,13 +46,49 @@ def __setstate__(self, tup: Tuple[Callable, Tuple[str]]) -> None: self._func, self._args = tup def __copy__(self) -> "_TranslationProxy": - return self + return _TranslationProxy(self._func, *self._args) def __repr__(self) -> str: try: - return 'i' + repr(str(self.data)) + return 'i' + repr(str(self.__str__())) except Exception: - return '<%s broken>' % self.__class__.__name__ + return f'<{self.__class__.__name__} broken>' + + def __add__(self, other: str) -> str: + return self.__str__() + other + + def __radd__(self, other: str) -> str: + return other + self.__str__() + + def __mod__(self, other: str) -> str: + return self.__str__() % other + + def __rmod__(self, other: str) -> str: + return other % self.__str__() + + def __mul__(self, other: Any) -> str: + return self.__str__() * other + + def __rmul__(self, other: Any) -> str: + return other * self.__str__() + + def __hash__(self): + return hash(self.__str__()) + + def __eq__(self, other): + return self.__str__() == other + + def __lt__(self, string): + return self.__str__() < string + + def __contains__(self, char): + return char in self.__str__() + + def __len__(self): + return len(self.__str__()) + + def __getitem__(self, index): + return self.__str__()[index] translators: Dict[Tuple[str, str], NullTranslations] = defaultdict(NullTranslations) @@ -219,7 +215,7 @@ def setup(app): def gettext(message: str, *args: Any) -> str: if not is_translator_registered(catalog, namespace): # not initialized yet - return _TranslationProxy(_lazy_translate, catalog, namespace, message) # type: ignore # NOQA + return _TranslationProxy(_lazy_translate, catalog, namespace, message) # type: ignore[return-value] # NOQA else: translator = get_translator(catalog, namespace) if len(args) <= 1: diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py index c0d6fc463e..e61b01d18e 100644 --- a/sphinx/pycode/ast.py +++ b/sphinx/pycode/ast.py @@ -1,18 +1,8 @@ """Helpers for AST (Abstract Syntax Tree).""" -import sys +import ast from typing import Dict, List, Optional, Type, overload -if sys.version_info[:2] >= (3, 8): - import ast -else: - try: - # use typed_ast module if installed - from typed_ast import ast3 as ast - except ImportError: - import ast # type: ignore - - OPERATORS: Dict[Type[ast.AST], str] = { ast.Add: "+", ast.And: "and", @@ -37,21 +27,13 @@ def parse(code: str, mode: str = 'exec') -> "ast.AST": - """Parse the *code* using the built-in ast or typed_ast libraries. - - This enables "type_comments" feature if possible. - """ + """Parse the *code* using the built-in ast module.""" try: - # type_comments parameter is available on py38+ - return ast.parse(code, mode=mode, type_comments=True) # type: ignore + return ast.parse(code, mode=mode, type_comments=True) except SyntaxError: # Some syntax error found. To ignore invalid type comments, retry parsing without # type_comments parameter (refs: https://github.com/sphinx-doc/sphinx/issues/8652). return ast.parse(code, mode=mode) - except TypeError: - # fallback to ast module. - # typed_ast is used to parse type_comments if installed. - return ast.parse(code, mode=mode) @overload @@ -102,10 +84,8 @@ def _visit_arg_with_default(self, arg: ast.arg, default: Optional[ast.AST]) -> s def visit_arguments(self, node: ast.arguments) -> str: defaults: List[Optional[ast.expr]] = list(node.defaults) positionals = len(node.args) - posonlyargs = 0 - if hasattr(node, "posonlyargs"): # for py38+ - posonlyargs += len(node.posonlyargs) # type:ignore - positionals += posonlyargs + posonlyargs = len(node.posonlyargs) + positionals += posonlyargs for _ in range(len(defaults), positionals): defaults.insert(0, None) @@ -114,12 +94,11 @@ def visit_arguments(self, node: ast.arguments) -> str: kw_defaults.insert(0, None) args: List[str] = [] - if hasattr(node, "posonlyargs"): # for py38+ - for i, arg in enumerate(node.posonlyargs): # type: ignore - args.append(self._visit_arg_with_default(arg, defaults[i])) + for i, arg in enumerate(node.posonlyargs): + args.append(self._visit_arg_with_default(arg, defaults[i])) - if node.posonlyargs: # type: ignore - args.append('/') + if node.posonlyargs: + args.append('/') for i, arg in enumerate(node.args): args.append(self._visit_arg_with_default(arg, defaults[i + posonlyargs])) @@ -155,19 +134,19 @@ def visit_Call(self, node: ast.Call) -> str: ["%s=%s" % (k.arg, self.visit(k.value)) for k in node.keywords]) return "%s(%s)" % (self.visit(node.func), ", ".join(args)) - def visit_Constant(self, node: ast.Constant) -> str: # type: ignore + def visit_Constant(self, node: ast.Constant) -> str: if node.value is Ellipsis: return "..." elif isinstance(node.value, (int, float, complex)): - if self.code and sys.version_info[:2] >= (3, 8): - return ast.get_source_segment(self.code, node) # type: ignore + if self.code: + return ast.get_source_segment(self.code, node) or repr(node.value) else: return repr(node.value) else: return repr(node.value) def visit_Dict(self, node: ast.Dict) -> str: - keys = (self.visit(k) for k in node.keys) + keys = (self.visit(k) for k in node.keys if k is not None) values = (self.visit(v) for v in node.values) items = (k + ": " + v for k, v in zip(keys, values)) return "{" + ", ".join(items) + "}" @@ -219,22 +198,5 @@ def visit_Tuple(self, node: ast.Tuple) -> str: else: return "(" + ", ".join(self.visit(e) for e in node.elts) + ")" - if sys.version_info[:2] <= (3, 7): - # these ast nodes were deprecated in python 3.8 - def visit_Bytes(self, node: ast.Bytes) -> str: - return repr(node.s) - - def visit_Ellipsis(self, node: ast.Ellipsis) -> str: - return "..." - - def visit_NameConstant(self, node: ast.NameConstant) -> str: - return repr(node.value) - - def visit_Num(self, node: ast.Num) -> str: - return repr(node.n) - - def visit_Str(self, node: ast.Str) -> str: - return repr(node.s) - def generic_visit(self, node): raise NotImplementedError('Unable to parse %s object' % type(node).__name__) diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index 1019400a82..133748cb80 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -1,4 +1,6 @@ """Utilities parsing and analyzing Python code.""" + +import ast import inspect import itertools import re @@ -9,8 +11,8 @@ from tokenize import COMMENT, NL from typing import Any, Dict, List, Optional, Tuple -from sphinx.pycode.ast import ast # for py37 or older -from sphinx.pycode.ast import parse, unparse +from sphinx.pycode.ast import parse as ast_parse +from sphinx.pycode.ast import unparse as ast_unparse comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') indent_re = re.compile('^\\s*$') @@ -266,7 +268,7 @@ def add_variable_annotation(self, name: str, annotation: ast.AST) -> None: qualname = self.get_qualname_for(name) if qualname: basename = ".".join(qualname[:-1]) - self.annotations[(basename, name)] = unparse(annotation) + self.annotations[(basename, name)] = ast_unparse(annotation) def is_final(self, decorators: List[ast.expr]) -> bool: final = [] @@ -277,7 +279,7 @@ def is_final(self, decorators: List[ast.expr]) -> bool: for decorator in decorators: try: - if unparse(decorator) in final: + if ast_unparse(decorator) in final: return True except NotImplementedError: pass @@ -293,7 +295,7 @@ def is_overload(self, decorators: List[ast.expr]) -> bool: for decorator in decorators: try: - if unparse(decorator) in overload: + if ast_unparse(decorator) in overload: return True except NotImplementedError: pass @@ -304,12 +306,9 @@ def get_self(self) -> Optional[ast.arg]: """Returns the name of the first argument if in a function.""" if self.current_function and self.current_function.args.args: return self.current_function.args.args[0] - elif (self.current_function and - getattr(self.current_function.args, 'posonlyargs', None)): - # for py38+ - return self.current_function.args.posonlyargs[0] # type: ignore - else: - return None + if self.current_function and self.current_function.args.posonlyargs: + return self.current_function.args.posonlyargs[0] + return None def get_line(self, lineno: int) -> str: """Returns specified line.""" @@ -553,7 +552,7 @@ def parse(self) -> None: def parse_comments(self) -> None: """Parse the code and pick up comments.""" - tree = parse(self.code) + tree = ast_parse(self.code) picker = VariableCommentPicker(self.code.splitlines(True), self.encoding) picker.visit(tree) self.annotations = picker.annotations diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index c3f4d1af3c..403630b936 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -1,5 +1,6 @@ """Helpers for inspecting Python modules.""" +import ast import builtins import contextlib import enum @@ -8,15 +9,15 @@ import sys import types import typing -from functools import partial, partialmethod +from functools import cached_property, partial, partialmethod, singledispatchmethod from importlib import import_module -from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA +from inspect import (Parameter, isasyncgenfunction, isclass, ismethod, # NOQA + ismethoddescriptor, ismodule) from io import StringIO from types import (ClassMethodDescriptorType, MethodDescriptorType, MethodType, ModuleType, WrapperDescriptorType) from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast -from sphinx.pycode.ast import ast # for py37 from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging from sphinx.util.typing import ForwardRef @@ -285,11 +286,7 @@ def is_singledispatch_function(obj: Any) -> bool: def is_singledispatch_method(obj: Any) -> bool: """Check if the object is singledispatch method.""" - try: - from functools import singledispatchmethod # type: ignore - return isinstance(obj, singledispatchmethod) - except ImportError: # py37 - return False + return isinstance(obj, singledispatchmethod) def isfunction(obj: Any) -> bool: @@ -329,27 +326,9 @@ def iswrappedcoroutine(obj: Any) -> bool: return False -if sys.version_info[:2] <= (3, 7): - def isasyncgenfunction(obj: Any) -> bool: - """Check if the object is async-gen function.""" - if hasattr(obj, '__code__') and inspect.isasyncgenfunction(obj): - # check obj.__code__ because isasyncgenfunction() crashes for custom method-like - # objects on python3.7 (see https://github.com/sphinx-doc/sphinx/issues/9838) - return True - else: - return False -else: - isasyncgenfunction = inspect.isasyncgenfunction - - def isproperty(obj: Any) -> bool: """Check if the object is property.""" - if sys.version_info[:2] >= (3, 8): - from functools import cached_property # cached_property is available since py3.8 - if isinstance(obj, cached_property): - return True - - return isinstance(obj, property) + return isinstance(obj, (property, cached_property)) def isgenericalias(obj: Any) -> bool: @@ -723,7 +702,7 @@ def signature_from_str(signature: str) -> inspect.Signature: """Create a Signature object from string.""" code = 'def func' + signature + ': pass' module = ast.parse(code) - function = cast(ast.FunctionDef, module.body[0]) # type: ignore + function = cast(ast.FunctionDef, module.body[0]) return signature_from_ast(function, code) @@ -734,7 +713,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu defaults = list(args.defaults) params = [] if hasattr(args, "posonlyargs"): - posonlyargs = len(args.posonlyargs) # type: ignore + posonlyargs = len(args.posonlyargs) positionals = posonlyargs + len(args.args) else: posonlyargs = 0 @@ -744,7 +723,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu defaults.insert(0, Parameter.empty) # type: ignore if hasattr(args, "posonlyargs"): - for i, arg in enumerate(args.posonlyargs): # type: ignore + for i, arg in enumerate(args.posonlyargs): if defaults[i] is Parameter.empty: default = Parameter.empty else: diff --git a/tests/roots/test-ext-autodoc/target/partialmethod.py b/tests/roots/test-ext-autodoc/target/partialmethod.py index 4966a984f6..82843461fa 100644 --- a/tests/roots/test-ext-autodoc/target/partialmethod.py +++ b/tests/roots/test-ext-autodoc/target/partialmethod.py @@ -1,4 +1,3 @@ -# for py34 or above from functools import partialmethod diff --git a/tests/roots/test-ext-autodoc/target/pep570.py b/tests/roots/test-ext-autodoc/target/pep570.py index 1a77eae93d..e69de29bb2 100644 --- a/tests/roots/test-ext-autodoc/target/pep570.py +++ b/tests/roots/test-ext-autodoc/target/pep570.py @@ -1,11 +0,0 @@ -def foo(*, a, b): - pass - -def bar(a, b, /, c, d): - pass - -def baz(a, /, *, b): - pass - -def qux(a, b, /): - pass diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index a4e276226f..c4b87c7374 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -1,7 +1,6 @@ """Tests the Python Domain""" import re -import sys from unittest.mock import Mock import docutils.utils @@ -365,7 +364,6 @@ def test_parse_annotation_suppress(app): assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="typing.Dict") -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_parse_annotation_Literal(app): doctree = _parse_annotation("Literal[True, False]", app.env) assert_node(doctree, ([pending_xref, "Literal"], @@ -451,37 +449,6 @@ def test_pyfunction_signature_full(app): [desc_sig_punctuation, ":"], desc_sig_space, [desc_sig_name, pending_xref, "str"])])]) - - -def test_pyfunction_with_unary_operators(app): - text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"], - [desc_sig_operator, "="], - [nodes.inline, "+1"])], - [desc_parameter, ([desc_sig_name, "bacon"], - [desc_sig_operator, "="], - [nodes.inline, "-1"])], - [desc_parameter, ([desc_sig_name, "sausage"], - [desc_sig_operator, "="], - [nodes.inline, "~1"])], - [desc_parameter, ([desc_sig_name, "spam"], - [desc_sig_operator, "="], - [nodes.inline, "not spam"])])]) - - -def test_pyfunction_with_binary_operators(app): - text = ".. py:function:: menu(spam=2**64)" - doctree = restructuredtext.parse(app, text) - assert_node(doctree[1][0][1], - [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"], - [desc_sig_operator, "="], - [nodes.inline, "2**64"])])]) - - -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') -def test_pyfunction_signature_full_py38(app): # case: separator at head text = ".. py:function:: hello(*, a)" doctree = restructuredtext.parse(app, text) @@ -516,7 +483,33 @@ def test_pyfunction_signature_full_py38(app): [desc_parameter, desc_sig_operator, "/"])]) -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') +def test_pyfunction_with_unary_operators(app): + text = ".. py:function:: menu(egg=+1, bacon=-1, sausage=~1, spam=not spam)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "egg"], + [desc_sig_operator, "="], + [nodes.inline, "+1"])], + [desc_parameter, ([desc_sig_name, "bacon"], + [desc_sig_operator, "="], + [nodes.inline, "-1"])], + [desc_parameter, ([desc_sig_name, "sausage"], + [desc_sig_operator, "="], + [nodes.inline, "~1"])], + [desc_parameter, ([desc_sig_name, "spam"], + [desc_sig_operator, "="], + [nodes.inline, "not spam"])])]) + + +def test_pyfunction_with_binary_operators(app): + text = ".. py:function:: menu(spam=2**64)" + doctree = restructuredtext.parse(app, text) + assert_node(doctree[1][0][1], + [desc_parameterlist, ([desc_parameter, ([desc_sig_name, "spam"], + [desc_sig_operator, "="], + [nodes.inline, "2**64"])])]) + + def test_pyfunction_with_number_literals(app): text = ".. py:function:: hello(age=0x10, height=1_6_0)" doctree = restructuredtext.parse(app, text) diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index c980b5b267..5bc12f7f5e 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1072,8 +1072,6 @@ def test_autodoc_descriptor(app): ] -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), - reason='cached_property is available since python3.8.') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_cached_property(app): options = {"members": None, @@ -2059,8 +2057,6 @@ def test_singledispatch(app): ] -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), - reason='singledispatchmethod is available since python3.8') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_singledispatchmethod(app): options = {"members": None} @@ -2088,8 +2084,6 @@ def test_singledispatchmethod(app): ] -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), - reason='singledispatchmethod is available since python3.8') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_singledispatchmethod_automethod(app): options = {} @@ -2142,8 +2136,6 @@ def test_cython(app): ] -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), - reason='typing.final is available since python3.8') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_final(app): options = {"members": None} diff --git a/tests/test_ext_autodoc_autoproperty.py b/tests/test_ext_autodoc_autoproperty.py index bf54be36c0..f982144a92 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_ext_autodoc_autoproperty.py @@ -4,8 +4,6 @@ source file translated by test_build. """ -import sys - import pytest from .test_ext_autodoc import do_autodoc @@ -40,7 +38,6 @@ def test_class_properties(app): ] -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_cached_properties(app): actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop') diff --git a/tests/test_ext_autodoc_preserve_defaults.py b/tests/test_ext_autodoc_preserve_defaults.py index b5548ad25a..d4cef7a6ea 100644 --- a/tests/test_ext_autodoc_preserve_defaults.py +++ b/tests/test_ext_autodoc_preserve_defaults.py @@ -1,7 +1,5 @@ """Test the autodoc extension.""" -import sys - import pytest from .test_ext_autodoc import do_autodoc @@ -10,10 +8,7 @@ @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_preserve_defaults': True}) def test_preserve_defaults(app): - if sys.version_info[:2] <= (3, 7): - color = "16777215" - else: - color = "0xFFFFFF" + color = "0xFFFFFF" options = {"members": None} actual = do_autodoc(app, 'module', 'target.preserve_defaults', options) diff --git a/tests/test_pycode.py b/tests/test_pycode.py index cc3966b8df..993743a2b7 100644 --- a/tests/test_pycode.py +++ b/tests/test_pycode.py @@ -185,8 +185,6 @@ def test_ModuleAnalyzer_find_attr_docs(): 'Qux.attr2': 17} -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), - reason='posonlyargs are available since python3.8.') def test_ModuleAnalyzer_find_attr_docs_for_posonlyargs_method(): code = ('class Foo(object):\n' ' def __init__(self, /):\n' diff --git a/tests/test_pycode_ast.py b/tests/test_pycode_ast.py index e79e9d99b2..85d37f184e 100644 --- a/tests/test_pycode_ast.py +++ b/tests/test_pycode_ast.py @@ -1,10 +1,10 @@ """Test pycode.ast""" -import sys +import ast import pytest -from sphinx.pycode import ast +from sphinx.pycode.ast import unparse as ast_unparse @pytest.mark.parametrize('source,expected', [ @@ -48,23 +48,15 @@ ("(1, 2, 3)", "(1, 2, 3)"), # Tuple ("()", "()"), # Tuple (empty) ("(1,)", "(1,)"), # Tuple (single item) + ("lambda x=0, /, y=1, *args, z, **kwargs: x + y + z", + "lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs + ("0x1234", "0x1234"), # Constant + ("1_000_000", "1_000_000"), # Constant ]) def test_unparse(source, expected): module = ast.parse(source) - assert ast.unparse(module.body[0].value, source) == expected + assert ast_unparse(module.body[0].value, source) == expected def test_unparse_None(): - assert ast.unparse(None) is None - - -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') -@pytest.mark.parametrize('source,expected', [ - ("lambda x=0, /, y=1, *args, z, **kwargs: x + y + z", - "lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs - ("0x1234", "0x1234"), # Constant - ("1_000_000", "1_000_000"), # Constant -]) -def test_unparse_py38(source, expected): - module = ast.parse(source) - assert ast.unparse(module.body[0].value, source) == expected + assert ast_unparse(None) is None diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 3c20908387..bb43fb30d8 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -151,7 +151,8 @@ def meth2(self, arg1, arg2): def test_signature_annotations(): from .typing_test_data import (Node, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, - f13, f14, f15, f16, f17, f18, f19, f20, f21) + f13, f14, f15, f16, f17, f18, f19, f20, f21, f22, f23, f24, + f25) # Class annotations sig = inspect.signature(f0) @@ -272,25 +273,19 @@ def test_signature_annotations(): else: assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' - -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') -@pytest.mark.sphinx(testroot='ext-autodoc') -def test_signature_annotations_py38(app): - from target.pep570 import bar, baz, foo, qux - # case: separator at head - sig = inspect.signature(foo) + sig = inspect.signature(f22) assert stringify_signature(sig) == '(*, a, b)' # case: separator in the middle - sig = inspect.signature(bar) + sig = inspect.signature(f23) assert stringify_signature(sig) == '(a, b, /, c, d)' - sig = inspect.signature(baz) + sig = inspect.signature(f24) assert stringify_signature(sig) == '(a, /, *, b)' # case: separator at tail - sig = inspect.signature(qux) + sig = inspect.signature(f25) assert stringify_signature(sig) == '(a, b, /)' @@ -373,8 +368,6 @@ def test_signature_from_str_kwonly_args(): assert sig.parameters['b'].default == Parameter.empty -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), - reason='python-3.8 or above is required') def test_signature_from_str_positionaly_only_args(): sig = inspect.signature_from_str('(a, b=0, /, c=1)') assert list(sig.parameters.keys()) == ['a', 'b', 'c'] diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 658aa71680..b1d9921f96 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -162,7 +162,6 @@ def test_restify_type_ForwardRef(): assert restify(ForwardRef("myint")) == ":py:class:`myint`" -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_restify_type_Literal(): from typing import Literal # type: ignore assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" @@ -408,7 +407,6 @@ def test_stringify_type_hints_alias(): assert stringify(MyTuple, "smart") == "~typing.Tuple[str, str]" # type: ignore -@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason='python 3.8+ is required.') def test_stringify_type_Literal(): from typing import Literal # type: ignore assert stringify(Literal[1, "2", "\r"]) == "Literal[1, '2', '\\r']" diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index c2db7d95ba..6cbd7fb07f 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -105,6 +105,22 @@ def f21(arg1='whatever', arg2=Signature.empty): pass +def f22(*, a, b): + pass + + +def f23(a, b, /, c, d): + pass + + +def f24(a, /, *, b): + pass + + +def f25(a, b, /): + pass + + class Node: def __init__(self, parent: Optional['Node']) -> None: pass diff --git a/tox.ini b/tox.ini index 31b7c5b31c..59a7c14a92 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 2.4.0 -envlist = docs,flake8,mypy,twine,py{37,38,39,310,311},du{14,15,16,17,18,19} +envlist = docs,flake8,mypy,twine,py{38,39,310,311},du{14,15,16,17,18,19} isolated_build = True [testenv] @@ -16,7 +16,7 @@ passenv = EPUBCHECK_PATH TERM description = - py{37,38,39,310,311}: Run unit tests against {envname}. + py{38,39,310,311}: Run unit tests against {envname}. du{14,15,16,17,18,19}: Run unit tests with the given version of docutils. deps = du14: docutils==0.14.* From fd7ec9ce09d2de26977e587cc495084ff89167d5 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 3/4] Increase minimum Docutils to 0.17 --- .github/workflows/main.yml | 4 +- CHANGES | 3 +- 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 +-- 16 files changed, 95 insertions(+), 211 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d88c10ab6d..08fb55c856 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/CHANGES b/CHANGES index 94f8eeb70f..e02e7ab982 100644 --- a/CHANGES +++ b/CHANGES @@ -5,7 +5,8 @@ Dependencies ------------ * #10468: Drop Python 3.6 support -* #10470: Drop Python 3.7 support. Patch by Adam Turner +* #10470: Drop Python 3.7, Docutils 0.14, Docutils 0.15, and Docutils 0.16 + support. Patch by Adam Turner Incompatible changes -------------------- diff --git a/pyproject.toml b/pyproject.toml index 9f96f0d26a..58a0dd4d55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,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 e22ce5fa20..4abddf2057 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 876683ad0c..a0bf262620 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 2d6a027e31..9ca95a73fc 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 160c7f35d2..fd32f0dc17 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 cde09058e4..63c9a06efc 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 0646b44e64..fdbf94fe65 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 25aee0c612..2185fb8db1 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 072f187ba9..10fdbdf79f 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 649cf1861a..28591674b1 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 4f99ef5da9..7b1ea2b569 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 796d95bcc1..9febfe0da2 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 f15761c5e5..4978a501dc 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 59a7c14a92..4d1c80188a 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,311},du{14,15,16,17,18,19} +envlist = docs,flake8,mypy,twine,py{38,39,310,311},du{17,18,19} isolated_build = True [testenv] @@ -17,11 +17,8 @@ passenv = TERM description = py{38,39,310,311}: 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.* From 4684a4255404d55581808d87d580f30e5d87abc1 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 22 May 2022 21:09:39 +0100 Subject: [PATCH 4/4] Increase minimum Docutils to 0.18 --- .github/workflows/main.yml | 6 +-- CHANGES | 4 +- pyproject.toml | 2 +- sphinx/addnodes.py | 29 +++++------- sphinx/directives/patches.py | 28 +----------- sphinx/environment/__init__.py | 3 -- sphinx/search/__init__.py | 4 +- sphinx/themes/basic/static/basic.css_t | 45 ------------------- sphinx/themes/bizstyle/static/bizstyle.css_t | 2 - sphinx/themes/classic/static/classic.css_t | 2 - sphinx/themes/epub/static/epub.css_t | 2 - sphinx/themes/nature/static/nature.css_t | 2 - sphinx/themes/nonav/static/nonav.css_t | 2 - sphinx/themes/pyramid/static/epub.css_t | 2 - sphinx/themes/pyramid/static/pyramid.css_t | 2 - .../themes/sphinxdoc/static/sphinxdoc.css_t | 2 - .../traditional/static/traditional.css_t | 2 - sphinx/transforms/i18n.py | 9 +--- sphinx/util/docutils.py | 10 ----- sphinx/util/nodes.py | 26 +---------- tests/test_build_html.py | 36 +-------------- tests/test_smartquotes.py | 6 +-- tests/test_versioning.py | 10 ----- tox.ini | 5 +-- 24 files changed, 30 insertions(+), 211 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08fb55c856..78483810cc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -14,11 +14,11 @@ jobs: matrix: include: - python: "3.8" - docutils: du17 - - python: "3.9" docutils: du18 + - python: "3.9" + docutils: du19 - python: "3.10" - docutils: du18 + docutils: du19 - python: "3.10" docutils: du19 - python: "3.11-dev" diff --git a/CHANGES b/CHANGES index e02e7ab982..545bfb7684 100644 --- a/CHANGES +++ b/CHANGES @@ -5,8 +5,8 @@ Dependencies ------------ * #10468: Drop Python 3.6 support -* #10470: Drop Python 3.7, Docutils 0.14, Docutils 0.15, and Docutils 0.16 - support. Patch by Adam Turner +* #10470: Drop Python 3.7, Docutils 0.14, Docutils 0.15, Docutils 0.16, and + Docutils 0.17 support. Patch by Adam Turner Incompatible changes -------------------- diff --git a/pyproject.toml b/pyproject.toml index 58a0dd4d55..dd11048a1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ dependencies = [ "sphinxcontrib-qthelp", "Jinja2>=3.0", "Pygments>=2.12", - "docutils>=0.17,<0.20", + "docutils>=0.18,<0.20", "snowballstemmer>=2.0", "babel>=2.9", "alabaster>=0.7,<0.8", diff --git a/sphinx/addnodes.py b/sphinx/addnodes.py index 4abddf2057..fea9c0edd7 100644 --- a/sphinx/addnodes.py +++ b/sphinx/addnodes.py @@ -2,19 +2,24 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional, 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): @@ -557,9 +555,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..833a9ff0be 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 # type: ignore[attr-defined] 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 59904e26f1..6c956ecddd 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -9,7 +9,6 @@ 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 @@ -52,8 +51,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. diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 5330d7e7c7..45d9389493 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -189,8 +189,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/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t index 2effcd9630..401824a5cb 100644 --- a/sphinx/themes/basic/static/basic.css_t +++ b/sphinx/themes/basic/static/basic.css_t @@ -237,18 +237,6 @@ a.headerlink { visibility: hidden; } -{%- if docutils_version_info[:2] < (0, 18) %} -a.brackets:before, -span.brackets > a:before{ - content: "["; -} - -a.brackets:after, -span.brackets > a:after { - content: "]"; -} -{% endif %} - h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, @@ -337,20 +325,16 @@ p.sidebar-title { font-weight: bold; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{%- endif %} div.admonition, div.topic, blockquote { clear: left; } /* -- topics ---------------------------------------------------------------- */ -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{%- endif %} div.topic { border: 1px solid #ccc; padding: 7px; @@ -389,10 +373,8 @@ div.body p.centered { div.sidebar > :last-child, aside.sidebar > :last-child, -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents > :last-child, aside.topic > :last-child, -{%- endif %} div.topic > :last-child, div.admonition > :last-child { margin-bottom: 0; @@ -400,10 +382,8 @@ div.admonition > :last-child { div.sidebar::after, aside.sidebar::after, -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents::after, aside.topic::after, -{%- endif %} div.topic::after, div.admonition::after, blockquote::after { @@ -629,24 +609,6 @@ ul.simple p { margin-bottom: 0; } -{%- if docutils_version_info[:2] < (0, 18) %} -dl.footnote > dt, -dl.citation > dt { - float: left; - margin-right: 0.5em; -} - -dl.footnote > dd, -dl.citation > dd { - margin-bottom: 0em; -} - -dl.footnote > dd:after, -dl.citation > dd:after { - content: ""; - clear: both; -} -{%- elif docutils_version_info[:2] >= (0, 18) %} aside.footnote > span, div.citation > span { float: left; @@ -670,7 +632,6 @@ div.citation > p:last-of-type:after { content: ""; clear: both; } -{%- endif %} dl.field-list { display: grid; @@ -684,12 +645,6 @@ dl.field-list > dt { padding-right: 5px; } -{%- if docutils_version_info[:2] < (0, 18) %} -dl.field-list > dt:after { - content: ":"; -} -{% endif %} - dl.field-list > dd { padding-left: 0.5em; margin-top: 0em; diff --git a/sphinx/themes/bizstyle/static/bizstyle.css_t b/sphinx/themes/bizstyle/static/bizstyle.css_t index a524345f9a..a96aef5772 100644 --- a/sphinx/themes/bizstyle/static/bizstyle.css_t +++ b/sphinx/themes/bizstyle/static/bizstyle.css_t @@ -306,10 +306,8 @@ div.quotebar { border: 1px solid #ccc; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background-color: #f8f8f8; } diff --git a/sphinx/themes/classic/static/classic.css_t b/sphinx/themes/classic/static/classic.css_t index 789bec8112..a15e7eeae7 100644 --- a/sphinx/themes/classic/static/classic.css_t +++ b/sphinx/themes/classic/static/classic.css_t @@ -290,10 +290,8 @@ div.seealso { border: 1px solid #ff6; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background-color: #eee; } diff --git a/sphinx/themes/epub/static/epub.css_t b/sphinx/themes/epub/static/epub.css_t index a30344431a..245582f619 100644 --- a/sphinx/themes/epub/static/epub.css_t +++ b/sphinx/themes/epub/static/epub.css_t @@ -245,10 +245,8 @@ p.sidebar-title { /* -- topics ---------------------------------------------------------------- */ -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { border: 1px solid #ccc; padding: 7px 7px 0 7px; diff --git a/sphinx/themes/nature/static/nature.css_t b/sphinx/themes/nature/static/nature.css_t index 93f9a59448..57e1ba7cfc 100644 --- a/sphinx/themes/nature/static/nature.css_t +++ b/sphinx/themes/nature/static/nature.css_t @@ -194,10 +194,8 @@ div.seealso { border: 1px solid #ff6; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background-color: #eee; } diff --git a/sphinx/themes/nonav/static/nonav.css_t b/sphinx/themes/nonav/static/nonav.css_t index 933365e073..c66ae99ad1 100644 --- a/sphinx/themes/nonav/static/nonav.css_t +++ b/sphinx/themes/nonav/static/nonav.css_t @@ -234,10 +234,8 @@ p.sidebar-title { /* -- topics ---------------------------------------------------------------- */ -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { border: 1px solid #ccc; padding: 7px 7px 0 7px; diff --git a/sphinx/themes/pyramid/static/epub.css_t b/sphinx/themes/pyramid/static/epub.css_t index 98741d0b8a..12cb97487d 100644 --- a/sphinx/themes/pyramid/static/epub.css_t +++ b/sphinx/themes/pyramid/static/epub.css_t @@ -254,10 +254,8 @@ div.seealso { border: 1px solid #ff6; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background-color: #eee; } diff --git a/sphinx/themes/pyramid/static/pyramid.css_t b/sphinx/themes/pyramid/static/pyramid.css_t index 0ced6b29f8..46b613c2bd 100644 --- a/sphinx/themes/pyramid/static/pyramid.css_t +++ b/sphinx/themes/pyramid/static/pyramid.css_t @@ -245,10 +245,8 @@ div.seealso { padding: 10px 20px 10px 60px; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background: #eeeeee; border: 2px solid #C6C9CB; diff --git a/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t b/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t index 1817c48bcf..cfd16e3179 100644 --- a/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t +++ b/sphinx/themes/sphinxdoc/static/sphinxdoc.css_t @@ -266,10 +266,8 @@ div.quotebar { border: 1px solid #ccc; } -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background-color: #f8f8f8; } diff --git a/sphinx/themes/traditional/static/traditional.css_t b/sphinx/themes/traditional/static/traditional.css_t index 8a2f0712fb..2202ba8070 100644 --- a/sphinx/themes/traditional/static/traditional.css_t +++ b/sphinx/themes/traditional/static/traditional.css_t @@ -506,10 +506,8 @@ p.rubric { /* "Topics" */ -{%- if docutils_version_info[:2] >= (0, 18) %} nav.contents, aside.topic, -{% endif %} div.topic { background-color: #eee; border: 1px solid #ccc; diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index fd32f0dc17..188655845a 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 @@ -254,12 +254,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 63c9a06efc..d27ad6ba05 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -588,16 +588,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 to docutils up to 0.18 inclusive to provide 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__: Tuple["Values", Reporter] diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index fdbf94fe65..3549dd190f 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 10fdbdf79f..6b18bac1c5 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 pytest from html5lib import HTMLParser @@ -407,39 +406,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'), @@ -465,7 +431,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 4d1c80188a..1a45e61925 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,311},du{17,18,19} +envlist = docs,flake8,mypy,twine,py{38,39,310,311},du{18,19} isolated_build = True [testenv] @@ -17,9 +17,8 @@ passenv = TERM description = py{38,39,310,311}: Run unit tests against {envname}. - du{17,18,19}: Run unit tests with the given version of docutils. + du{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 =