diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4889bb9a8..d88c10ab6 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 ae031c69a..94f8eeb70 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 073cd6f14..9a60c541e 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 8c8bc361a..9f96f0d26 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 fa8fc191a..5292215a3 100644 --- a/sphinx/directives/other.py +++ b/sphinx/directives/other.py @@ -77,7 +77,7 @@ class TocTree(SphinxDirective): 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 7ce7fb8b3..fe2d52eb1 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.directives import ObjectDescription 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 _parse_annotation(annotation: str, env: BuildEnvironment) -> 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 _parse_annotation(annotation: str, env: BuildEnvironment) -> 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 c6a05875f..aa6045f66 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 @@ if TYPE_CHECKING: 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 348832efe..c8090d84b 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -54,7 +54,7 @@ class 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 @@ class TocTree: # 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 68c730504..628f4a444 100644 --- a/sphinx/environment/collectors/toctree.py +++ b/sphinx/environment/collectors/toctree.py @@ -236,7 +236,7 @@ class TocTreeCollector(EnvironmentCollector): 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 4230335d7..0c21c3081 100644 --- a/sphinx/ext/autodoc/preserve_defaults.py +++ b/sphinx/ext/autodoc/preserve_defaults.py @@ -6,8 +6,6 @@ and keep them not evaluated for readability. 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 9881ae747..b500a08c1 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 6d4465a09..7942079c8 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 @@ class _TranslationProxy(UserString): 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 @@ class _TranslationProxy(UserString): 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 get_translation(catalog: str, namespace: str = 'general') -> Callable[[str], 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 c0d6fc463..e61b01d18 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 @@ OPERATORS: Dict[Type[ast.AST], str] = { 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 @@ class _UnparseVisitor(ast.NodeVisitor): 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 @@ class _UnparseVisitor(ast.NodeVisitor): 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 @@ class _UnparseVisitor(ast.NodeVisitor): ["%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 @@ class _UnparseVisitor(ast.NodeVisitor): 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 1019400a8..133748cb8 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 token import DEDENT, INDENT, NAME, NEWLINE, NUMBER, OP, STRING 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 @@ class VariableCommentPicker(ast.NodeVisitor): 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 @@ class VariableCommentPicker(ast.NodeVisitor): 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 @@ class VariableCommentPicker(ast.NodeVisitor): 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 @@ class VariableCommentPicker(ast.NodeVisitor): """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 @@ class Parser: 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 c3f4d1af3..403630b93 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 re 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 iscoroutinefunction(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 4966a984f..82843461f 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 1a77eae93..e69de29bb 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 a4e276226..c4b87c737 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 c980b5b26..5bc12f7f5 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 bf54be36c..f982144a9 100644 --- a/tests/test_ext_autodoc_autoproperty.py +++ b/tests/test_ext_autodoc_autoproperty.py @@ -4,8 +4,6 @@ This tests mainly the Documenters; the auto directives are tested in a test 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 b5548ad25..d4cef7a6e 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 @@ from .test_ext_autodoc import do_autodoc @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 cc3966b8d..993743a2b 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 e79e9d99b..85d37f184 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 @@ from sphinx.pycode import ast ("(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 3c2090838..bb43fb30d 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -151,7 +151,8 @@ def test_signature_partialmethod(): 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 658aa7168..b1d9921f9 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 c2db7d95b..6cbd7fb07 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 31b7c5b31..59a7c14a9 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.*