Drop Python 3.7

This commit is contained in:
Adam Turner 2022-04-18 17:33:56 +01:00
parent 7649eb1505
commit 4660b62de0
27 changed files with 167 additions and 291 deletions

View File

@ -13,8 +13,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- python: "3.7"
docutils: du15
- python: "3.8" - python: "3.8"
docutils: du16 docutils: du16
- python: "3.9" - python: "3.9"

View File

@ -5,6 +5,7 @@ Dependencies
------------ ------------
* #10468: Drop Python 3.6 support * #10468: Drop Python 3.6 support
* #10470: Drop Python 3.7 support. Patch by Adam Turner
Incompatible changes Incompatible changes
-------------------- --------------------

View File

@ -12,7 +12,7 @@ Installing Sphinx
Overview 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`__, shoulders of many third-party libraries such as `Docutils`__ and `Jinja`__,
which are installed when Sphinx is installed. which are installed when Sphinx is installed.

View File

@ -13,7 +13,7 @@ urls.Download = "https://pypi.org/project/Sphinx/"
urls.Homepage = "https://www.sphinx-doc.org/" urls.Homepage = "https://www.sphinx-doc.org/"
urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues" urls."Issue tracker" = "https://github.com/sphinx-doc/sphinx/issues"
license.text = "BSD" license.text = "BSD"
requires-python = ">=3.7" requires-python = ">=3.8"
# Classifiers list: https://pypi.org/classifiers/ # Classifiers list: https://pypi.org/classifiers/
classifiers = [ classifiers = [
@ -30,7 +30,6 @@ classifiers = [
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
@ -89,13 +88,11 @@ lint = [
"mypy>=0.981", "mypy>=0.981",
"sphinx-lint", "sphinx-lint",
"docutils-stubs", "docutils-stubs",
"types-typed-ast",
"types-requests", "types-requests",
] ]
test = [ test = [
"pytest>=4.6", "pytest>=4.6",
"html5lib", "html5lib",
"typed_ast; python_version < '3.8'",
"cython", "cython",
] ]
@ -144,7 +141,7 @@ disallow_incomplete_defs = true
follow_imports = "skip" follow_imports = "skip"
ignore_missing_imports = true ignore_missing_imports = true
no_implicit_optional = true no_implicit_optional = true
python_version = "3.7" python_version = "3.8"
show_column_numbers = true show_column_numbers = true
show_error_codes = true show_error_codes = true
show_error_context = true show_error_context = true

View File

@ -77,7 +77,7 @@ class TocTree(SphinxDirective):
return ret return ret
def parse_content(self, toctree: addnodes.toctree) -> List[Node]: 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 suffixes = self.config.source_suffix
# glob target documents # glob target documents

View File

@ -1,9 +1,9 @@
"""The Python domain.""" """The Python domain."""
import ast
import builtins import builtins
import inspect import inspect
import re import re
import sys
import typing import typing
from inspect import Parameter from inspect import Parameter
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, Type, cast 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.domains import Domain, Index, IndexEntry, ObjType
from sphinx.environment import BuildEnvironment from sphinx.environment import BuildEnvironment
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.pycode.ast import ast
from sphinx.pycode.ast import parse as ast_parse from sphinx.pycode.ast import parse as ast_parse
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging from sphinx.util import logging
@ -138,7 +137,7 @@ def _parse_annotation(annotation: str, env: BuildEnvironment) -> List[Node]:
return [addnodes.desc_sig_space(), return [addnodes.desc_sig_space(),
addnodes.desc_sig_punctuation('', '|'), addnodes.desc_sig_punctuation('', '|'),
addnodes.desc_sig_space()] addnodes.desc_sig_space()]
elif isinstance(node, ast.Constant): # type: ignore elif isinstance(node, ast.Constant):
if node.value is Ellipsis: if node.value is Ellipsis:
return [addnodes.desc_sig_punctuation('', "...")] return [addnodes.desc_sig_punctuation('', "...")]
elif isinstance(node.value, bool): elif isinstance(node.value, bool):
@ -204,18 +203,6 @@ def _parse_annotation(annotation: str, env: BuildEnvironment) -> List[Node]:
return result return result
else: 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 raise SyntaxError # unsupported syntax
try: try:

View File

@ -1,10 +1,9 @@
"""The standard domain.""" """The standard domain."""
import re import re
import sys
from copy import copy from copy import copy
from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, from typing import (TYPE_CHECKING, Any, Callable, Dict, Final, Iterable, Iterator, List,
Tuple, Type, Union, cast) Optional, Tuple, Type, Union, cast)
from docutils import nodes from docutils import nodes
from docutils.nodes import Element, Node, system_message from docutils.nodes import Element, Node, system_message
@ -29,11 +28,6 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if sys.version_info[:2] >= (3, 8):
from typing import Final
else:
Final = Any
# RE for option descriptions # RE for option descriptions
option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)') option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)')
# RE for grammar tokens # RE for grammar tokens
@ -589,7 +583,7 @@ class StandardDomain(Domain):
'doc': XRefRole(warn_dangling=True, innernodeclass=nodes.inline), 'doc': XRefRole(warn_dangling=True, innernodeclass=nodes.inline),
} }
initial_data: Final = { initial_data: Final = { # type: ignore[misc]
'progoptions': {}, # (program, name) -> docname, labelid 'progoptions': {}, # (program, name) -> docname, labelid
'objects': {}, # (type, name) -> docname, labelid 'objects': {}, # (type, name) -> docname, labelid
'labels': { # labelname -> docname, labelid, sectionname '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 = { dangling_warnings = {
'term': 'term not in glossary: %(target)r', 'term': 'term not in glossary: %(target)r',
'numref': 'undefined label: %(target)r', 'numref': 'undefined label: %(target)r',

View File

@ -54,7 +54,7 @@ class TocTree:
""" """
if toctree.get('hidden', False) and not includehidden: if toctree.get('hidden', False) and not includehidden:
return None 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 # 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 # in mind the node structure of a toctree (using HTML-like node names
@ -141,7 +141,7 @@ class TocTree:
# don't show subitems # don't show subitems
toc = nodes.bullet_list('', item) toc = nodes.bullet_list('', item)
elif ref in generated_docnames: elif ref in generated_docnames:
docname, _, sectionname = generated_docnames[ref] docname, sectionname = generated_docnames[ref]
if not title: if not title:
title = sectionname title = sectionname
reference = nodes.reference('', title, internal=True, reference = nodes.reference('', title, internal=True,

View File

@ -236,7 +236,7 @@ class TocTreeCollector(EnvironmentCollector):
def assign_figure_numbers(self, env: BuildEnvironment) -> List[str]: def assign_figure_numbers(self, env: BuildEnvironment) -> List[str]:
"""Assign a figure number to each figure under a numbered toctree.""" """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 = [] rewrite_needed = []

View File

@ -6,8 +6,6 @@ and keep them not evaluated for readability.
import ast import ast
import inspect import inspect
import sys
from inspect import Parameter
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sphinx.application import Sphinx 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]: def get_default_value(lines: List[str], position: ast.AST) -> Optional[str]:
try: try:
if sys.version_info[:2] <= (3, 7): # only for py38+
return None
if position.lineno == position.end_lineno: if position.lineno == position.end_lineno:
line = lines[position.lineno - 1] line = lines[position.lineno - 1]
return line[position.col_offset:position.end_col_offset] 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) default = defaults.pop(0)
value = get_default_value(lines, default) value = get_default_value(lines, default)
if value is None: if value is None:
value = ast_unparse(default) # type: ignore value = ast_unparse(default)
parameters[i] = param.replace(default=DefaultValue(value)) parameters[i] = param.replace(default=DefaultValue(value))
else: else:
default = kw_defaults.pop(0) default = kw_defaults.pop(0)
value = get_default_value(lines, default) value = get_default_value(lines, default)
if value is None: if value is None:
value = ast_unparse(default) # type: ignore value = ast_unparse(default)
parameters[i] = param.replace(default=DefaultValue(value)) parameters[i] = param.replace(default=DefaultValue(value))
if bound_method and inspect.ismethod(obj): if bound_method and inspect.ismethod(obj):
# classmethods # classmethods
cls = inspect.Parameter('cls', Parameter.POSITIONAL_OR_KEYWORD) cls = inspect.Parameter('cls', inspect.Parameter.POSITIONAL_OR_KEYWORD)
parameters.insert(0, cls) parameters.insert(0, cls)
sig = sig.replace(parameters=parameters) sig = sig.replace(parameters=parameters)

View File

@ -1,12 +1,12 @@
"""Update annotations info of living objects using type_comments.""" """Update annotations info of living objects using type_comments."""
import ast
from inspect import Parameter, Signature, getsource from inspect import Parameter, Signature, getsource
from typing import Any, Dict, List, cast from typing import Any, Dict, List, cast
import sphinx import sphinx
from sphinx.application import Sphinx from sphinx.application import Sphinx
from sphinx.locale import __ from sphinx.locale import __
from sphinx.pycode.ast import ast
from sphinx.pycode.ast import parse as ast_parse from sphinx.pycode.ast import parse as ast_parse
from sphinx.pycode.ast import unparse as ast_unparse from sphinx.pycode.ast import unparse as ast_unparse
from sphinx.util import inspect, logging from sphinx.util import inspect, logging
@ -34,8 +34,7 @@ def signature_from_ast(node: ast.FunctionDef, bound_method: bool,
:param bound_method: Specify *node* is a bound method or not :param bound_method: Specify *node* is a bound method or not
""" """
params = [] params = []
if hasattr(node.args, "posonlyargs"): # for py38+ for arg in node.args.posonlyargs:
for arg in node.args.posonlyargs: # type: ignore
param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment) param = Parameter(arg.arg, Parameter.POSITIONAL_ONLY, annotation=arg.type_comment)
params.append(param) params.append(param)
@ -80,7 +79,7 @@ def get_type_comment(obj: Any, bound_method: bool = False) -> Signature:
"""Get type_comment'ed FunctionDef object from living object. """Get type_comment'ed FunctionDef object from living object.
This tries to parse original code for living object and returns 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: try:
source = getsource(obj) source = getsource(obj)

View File

@ -2,29 +2,25 @@
import gettext import gettext
import locale import locale
from collections import UserString, defaultdict from collections import defaultdict
from gettext import NullTranslations from gettext import NullTranslations
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union 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 Class for proxy strings from gettext translations. This is a helper for the
lazy_* functions from this module. lazy_* functions from this module.
The proxy implementation attempts to be as complete as possible, so that The proxy implementation attempts to be as complete as possible, so that
the lazy objects should mostly work as expected, for example for sorting. 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') __slots__ = ('_func', '_args')
def __new__(cls, func: Callable, *args: str) -> object: # type: ignore def __new__(cls, func: Callable, *args: str) -> "_TranslationProxy":
if not args: if not args:
# not called with "function" and "arguments", but a plain string # not called with "function" and "arguments", but a plain string
return str(func) return str(func) # type: ignore[return-value]
return object.__new__(cls) return object.__new__(cls)
def __getnewargs__(self) -> Tuple[str]: def __getnewargs__(self) -> Tuple[str]:
@ -34,50 +30,14 @@ class _TranslationProxy(UserString):
self._func = func self._func = func
self._args = args self._args = args
@property def __str__(self) -> str:
def data(self) -> str: # type: ignore return str(self._func(*self._args))
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 __dir__(self) -> List[str]: def __dir__(self) -> List[str]:
return dir(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: def __getattr__(self, name: str) -> Any:
if name == '__members__': return getattr(self.__str__(), name)
return self.__dir__()
return getattr(self.data, name)
def __getstate__(self) -> Tuple[Callable, Tuple[str, ...]]: def __getstate__(self) -> Tuple[Callable, Tuple[str, ...]]:
return self._func, self._args return self._func, self._args
@ -86,13 +46,49 @@ class _TranslationProxy(UserString):
self._func, self._args = tup self._func, self._args = tup
def __copy__(self) -> "_TranslationProxy": def __copy__(self) -> "_TranslationProxy":
return self return _TranslationProxy(self._func, *self._args)
def __repr__(self) -> str: def __repr__(self) -> str:
try: try:
return 'i' + repr(str(self.data)) return 'i' + repr(str(self.__str__()))
except Exception: 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) 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: def gettext(message: str, *args: Any) -> str:
if not is_translator_registered(catalog, namespace): if not is_translator_registered(catalog, namespace):
# not initialized yet # 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: else:
translator = get_translator(catalog, namespace) translator = get_translator(catalog, namespace)
if len(args) <= 1: if len(args) <= 1:

View File

@ -1,17 +1,7 @@
"""Helpers for AST (Abstract Syntax Tree).""" """Helpers for AST (Abstract Syntax Tree)."""
import sys
from typing import Dict, List, Optional, Type, overload
if sys.version_info[:2] >= (3, 8):
import ast import ast
else: from typing import Dict, List, Optional, Type, overload
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] = { OPERATORS: Dict[Type[ast.AST], str] = {
ast.Add: "+", ast.Add: "+",
@ -37,21 +27,13 @@ OPERATORS: Dict[Type[ast.AST], str] = {
def parse(code: str, mode: str = 'exec') -> "ast.AST": def parse(code: str, mode: str = 'exec') -> "ast.AST":
"""Parse the *code* using the built-in ast or typed_ast libraries. """Parse the *code* using the built-in ast module."""
This enables "type_comments" feature if possible.
"""
try: try:
# type_comments parameter is available on py38+ return ast.parse(code, mode=mode, type_comments=True)
return ast.parse(code, mode=mode, type_comments=True) # type: ignore
except SyntaxError: except SyntaxError:
# Some syntax error found. To ignore invalid type comments, retry parsing without # Some syntax error found. To ignore invalid type comments, retry parsing without
# type_comments parameter (refs: https://github.com/sphinx-doc/sphinx/issues/8652). # type_comments parameter (refs: https://github.com/sphinx-doc/sphinx/issues/8652).
return ast.parse(code, mode=mode) 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 @overload
@ -102,9 +84,7 @@ class _UnparseVisitor(ast.NodeVisitor):
def visit_arguments(self, node: ast.arguments) -> str: def visit_arguments(self, node: ast.arguments) -> str:
defaults: List[Optional[ast.expr]] = list(node.defaults) defaults: List[Optional[ast.expr]] = list(node.defaults)
positionals = len(node.args) positionals = len(node.args)
posonlyargs = 0 posonlyargs = len(node.posonlyargs)
if hasattr(node, "posonlyargs"): # for py38+
posonlyargs += len(node.posonlyargs) # type:ignore
positionals += posonlyargs positionals += posonlyargs
for _ in range(len(defaults), positionals): for _ in range(len(defaults), positionals):
defaults.insert(0, None) defaults.insert(0, None)
@ -114,11 +94,10 @@ class _UnparseVisitor(ast.NodeVisitor):
kw_defaults.insert(0, None) kw_defaults.insert(0, None)
args: List[str] = [] args: List[str] = []
if hasattr(node, "posonlyargs"): # for py38+ for i, arg in enumerate(node.posonlyargs):
for i, arg in enumerate(node.posonlyargs): # type: ignore
args.append(self._visit_arg_with_default(arg, defaults[i])) args.append(self._visit_arg_with_default(arg, defaults[i]))
if node.posonlyargs: # type: ignore if node.posonlyargs:
args.append('/') args.append('/')
for i, arg in enumerate(node.args): for i, arg in enumerate(node.args):
@ -155,19 +134,19 @@ class _UnparseVisitor(ast.NodeVisitor):
["%s=%s" % (k.arg, self.visit(k.value)) for k in node.keywords]) ["%s=%s" % (k.arg, self.visit(k.value)) for k in node.keywords])
return "%s(%s)" % (self.visit(node.func), ", ".join(args)) 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: if node.value is Ellipsis:
return "..." return "..."
elif isinstance(node.value, (int, float, complex)): elif isinstance(node.value, (int, float, complex)):
if self.code and sys.version_info[:2] >= (3, 8): if self.code:
return ast.get_source_segment(self.code, node) # type: ignore return ast.get_source_segment(self.code, node) or repr(node.value)
else: else:
return repr(node.value) return repr(node.value)
else: else:
return repr(node.value) return repr(node.value)
def visit_Dict(self, node: ast.Dict) -> str: 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) values = (self.visit(v) for v in node.values)
items = (k + ": " + v for k, v in zip(keys, values)) items = (k + ": " + v for k, v in zip(keys, values))
return "{" + ", ".join(items) + "}" return "{" + ", ".join(items) + "}"
@ -219,22 +198,5 @@ class _UnparseVisitor(ast.NodeVisitor):
else: else:
return "(" + ", ".join(self.visit(e) for e in node.elts) + ")" 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): def generic_visit(self, node):
raise NotImplementedError('Unable to parse %s object' % type(node).__name__) raise NotImplementedError('Unable to parse %s object' % type(node).__name__)

View File

@ -1,4 +1,6 @@
"""Utilities parsing and analyzing Python code.""" """Utilities parsing and analyzing Python code."""
import ast
import inspect import inspect
import itertools import itertools
import re import re
@ -9,8 +11,8 @@ from token import DEDENT, INDENT, NAME, NEWLINE, NUMBER, OP, STRING
from tokenize import COMMENT, NL from tokenize import COMMENT, NL
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from sphinx.pycode.ast import ast # for py37 or older from sphinx.pycode.ast import parse as ast_parse
from sphinx.pycode.ast import parse, unparse from sphinx.pycode.ast import unparse as ast_unparse
comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$') comment_re = re.compile('^\\s*#: ?(.*)\r?\n?$')
indent_re = re.compile('^\\s*$') indent_re = re.compile('^\\s*$')
@ -266,7 +268,7 @@ class VariableCommentPicker(ast.NodeVisitor):
qualname = self.get_qualname_for(name) qualname = self.get_qualname_for(name)
if qualname: if qualname:
basename = ".".join(qualname[:-1]) 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: def is_final(self, decorators: List[ast.expr]) -> bool:
final = [] final = []
@ -277,7 +279,7 @@ class VariableCommentPicker(ast.NodeVisitor):
for decorator in decorators: for decorator in decorators:
try: try:
if unparse(decorator) in final: if ast_unparse(decorator) in final:
return True return True
except NotImplementedError: except NotImplementedError:
pass pass
@ -293,7 +295,7 @@ class VariableCommentPicker(ast.NodeVisitor):
for decorator in decorators: for decorator in decorators:
try: try:
if unparse(decorator) in overload: if ast_unparse(decorator) in overload:
return True return True
except NotImplementedError: except NotImplementedError:
pass pass
@ -304,11 +306,8 @@ class VariableCommentPicker(ast.NodeVisitor):
"""Returns the name of the first argument if in a function.""" """Returns the name of the first argument if in a function."""
if self.current_function and self.current_function.args.args: if self.current_function and self.current_function.args.args:
return self.current_function.args.args[0] return self.current_function.args.args[0]
elif (self.current_function and if self.current_function and self.current_function.args.posonlyargs:
getattr(self.current_function.args, 'posonlyargs', None)): return self.current_function.args.posonlyargs[0]
# for py38+
return self.current_function.args.posonlyargs[0] # type: ignore
else:
return None return None
def get_line(self, lineno: int) -> str: def get_line(self, lineno: int) -> str:
@ -553,7 +552,7 @@ class Parser:
def parse_comments(self) -> None: def parse_comments(self) -> None:
"""Parse the code and pick up comments.""" """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 = VariableCommentPicker(self.code.splitlines(True), self.encoding)
picker.visit(tree) picker.visit(tree)
self.annotations = picker.annotations self.annotations = picker.annotations

View File

@ -1,5 +1,6 @@
"""Helpers for inspecting Python modules.""" """Helpers for inspecting Python modules."""
import ast
import builtins import builtins
import contextlib import contextlib
import enum import enum
@ -8,15 +9,15 @@ import re
import sys import sys
import types import types
import typing import typing
from functools import partial, partialmethod from functools import cached_property, partial, partialmethod, singledispatchmethod
from importlib import import_module 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 io import StringIO
from types import (ClassMethodDescriptorType, MethodDescriptorType, MethodType, ModuleType, from types import (ClassMethodDescriptorType, MethodDescriptorType, MethodType, ModuleType,
WrapperDescriptorType) WrapperDescriptorType)
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast 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.pycode.ast import unparse as ast_unparse
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.typing import ForwardRef from sphinx.util.typing import ForwardRef
@ -285,11 +286,7 @@ def is_singledispatch_function(obj: Any) -> bool:
def is_singledispatch_method(obj: Any) -> bool: def is_singledispatch_method(obj: Any) -> bool:
"""Check if the object is singledispatch method.""" """Check if the object is singledispatch method."""
try:
from functools import singledispatchmethod # type: ignore
return isinstance(obj, singledispatchmethod) return isinstance(obj, singledispatchmethod)
except ImportError: # py37
return False
def isfunction(obj: Any) -> bool: def isfunction(obj: Any) -> bool:
@ -329,27 +326,9 @@ def iscoroutinefunction(obj: Any) -> bool:
return False 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: def isproperty(obj: Any) -> bool:
"""Check if the object is property.""" """Check if the object is property."""
if sys.version_info[:2] >= (3, 8): return isinstance(obj, (property, cached_property))
from functools import cached_property # cached_property is available since py3.8
if isinstance(obj, cached_property):
return True
return isinstance(obj, property)
def isgenericalias(obj: Any) -> bool: def isgenericalias(obj: Any) -> bool:
@ -723,7 +702,7 @@ def signature_from_str(signature: str) -> inspect.Signature:
"""Create a Signature object from string.""" """Create a Signature object from string."""
code = 'def func' + signature + ': pass' code = 'def func' + signature + ': pass'
module = ast.parse(code) 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) 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) defaults = list(args.defaults)
params = [] params = []
if hasattr(args, "posonlyargs"): if hasattr(args, "posonlyargs"):
posonlyargs = len(args.posonlyargs) # type: ignore posonlyargs = len(args.posonlyargs)
positionals = posonlyargs + len(args.args) positionals = posonlyargs + len(args.args)
else: else:
posonlyargs = 0 posonlyargs = 0
@ -744,7 +723,7 @@ def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signatu
defaults.insert(0, Parameter.empty) # type: ignore defaults.insert(0, Parameter.empty) # type: ignore
if hasattr(args, "posonlyargs"): 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: if defaults[i] is Parameter.empty:
default = Parameter.empty default = Parameter.empty
else: else:

View File

@ -1,4 +1,3 @@
# for py34 or above
from functools import partialmethod from functools import partialmethod

View File

@ -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

View File

@ -1,7 +1,6 @@
"""Tests the Python Domain""" """Tests the Python Domain"""
import re import re
import sys
from unittest.mock import Mock from unittest.mock import Mock
import docutils.utils 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") 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): def test_parse_annotation_Literal(app):
doctree = _parse_annotation("Literal[True, False]", app.env) doctree = _parse_annotation("Literal[True, False]", app.env)
assert_node(doctree, ([pending_xref, "Literal"], assert_node(doctree, ([pending_xref, "Literal"],
@ -451,37 +449,6 @@ def test_pyfunction_signature_full(app):
[desc_sig_punctuation, ":"], [desc_sig_punctuation, ":"],
desc_sig_space, desc_sig_space,
[desc_sig_name, pending_xref, "str"])])]) [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 # case: separator at head
text = ".. py:function:: hello(*, a)" text = ".. py:function:: hello(*, a)"
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)
@ -516,7 +483,33 @@ def test_pyfunction_signature_full_py38(app):
[desc_parameter, desc_sig_operator, "/"])]) [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): def test_pyfunction_with_number_literals(app):
text = ".. py:function:: hello(age=0x10, height=1_6_0)" text = ".. py:function:: hello(age=0x10, height=1_6_0)"
doctree = restructuredtext.parse(app, text) doctree = restructuredtext.parse(app, text)

View File

@ -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') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_cached_property(app): def test_autodoc_cached_property(app):
options = {"members": None, 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_singledispatchmethod(app): def test_singledispatchmethod(app):
options = {"members": None} 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_singledispatchmethod_automethod(app): def test_singledispatchmethod_automethod(app):
options = {} 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_final(app): def test_final(app):
options = {"members": None} options = {"members": None}

View File

@ -4,8 +4,6 @@ This tests mainly the Documenters; the auto directives are tested in a test
source file translated by test_build. source file translated by test_build.
""" """
import sys
import pytest import pytest
from .test_ext_autodoc import do_autodoc 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_cached_properties(app): def test_cached_properties(app):
actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop') actual = do_autodoc(app, 'property', 'target.cached_property.Foo.prop')

View File

@ -1,7 +1,5 @@
"""Test the autodoc extension.""" """Test the autodoc extension."""
import sys
import pytest import pytest
from .test_ext_autodoc import do_autodoc from .test_ext_autodoc import do_autodoc
@ -10,9 +8,6 @@ from .test_ext_autodoc import do_autodoc
@pytest.mark.sphinx('html', testroot='ext-autodoc', @pytest.mark.sphinx('html', testroot='ext-autodoc',
confoverrides={'autodoc_preserve_defaults': True}) confoverrides={'autodoc_preserve_defaults': True})
def test_preserve_defaults(app): def test_preserve_defaults(app):
if sys.version_info[:2] <= (3, 7):
color = "16777215"
else:
color = "0xFFFFFF" color = "0xFFFFFF"
options = {"members": None} options = {"members": None}

View File

@ -185,8 +185,6 @@ def test_ModuleAnalyzer_find_attr_docs():
'Qux.attr2': 17} '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(): def test_ModuleAnalyzer_find_attr_docs_for_posonlyargs_method():
code = ('class Foo(object):\n' code = ('class Foo(object):\n'
' def __init__(self, /):\n' ' def __init__(self, /):\n'

View File

@ -1,10 +1,10 @@
"""Test pycode.ast""" """Test pycode.ast"""
import sys import ast
import pytest import pytest
from sphinx.pycode import ast from sphinx.pycode.ast import unparse as ast_unparse
@pytest.mark.parametrize('source,expected', [ @pytest.mark.parametrize('source,expected', [
@ -48,23 +48,15 @@ from sphinx.pycode import ast
("(1, 2, 3)", "(1, 2, 3)"), # Tuple ("(1, 2, 3)", "(1, 2, 3)"), # Tuple
("()", "()"), # Tuple (empty) ("()", "()"), # Tuple (empty)
("(1,)", "(1,)"), # Tuple (single item) ("(1,)", "(1,)"), # Tuple (single item)
])
def test_unparse(source, expected):
module = ast.parse(source)
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: x + y + z",
"lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs "lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs
("0x1234", "0x1234"), # Constant ("0x1234", "0x1234"), # Constant
("1_000_000", "1_000_000"), # Constant ("1_000_000", "1_000_000"), # Constant
]) ])
def test_unparse_py38(source, expected): def test_unparse(source, expected):
module = ast.parse(source) 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

View File

@ -151,7 +151,8 @@ def test_signature_partialmethod():
def test_signature_annotations(): def test_signature_annotations():
from .typing_test_data import (Node, f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, 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 # Class annotations
sig = inspect.signature(f0) sig = inspect.signature(f0)
@ -272,25 +273,19 @@ def test_signature_annotations():
else: else:
assert stringify_signature(sig, unqualified_typehints=True) == '(x: int = None, y: dict = {}) -> None' 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 # case: separator at head
sig = inspect.signature(foo) sig = inspect.signature(f22)
assert stringify_signature(sig) == '(*, a, b)' assert stringify_signature(sig) == '(*, a, b)'
# case: separator in the middle # case: separator in the middle
sig = inspect.signature(bar) sig = inspect.signature(f23)
assert stringify_signature(sig) == '(a, b, /, c, d)' assert stringify_signature(sig) == '(a, b, /, c, d)'
sig = inspect.signature(baz) sig = inspect.signature(f24)
assert stringify_signature(sig) == '(a, /, *, b)' assert stringify_signature(sig) == '(a, /, *, b)'
# case: separator at tail # case: separator at tail
sig = inspect.signature(qux) sig = inspect.signature(f25)
assert stringify_signature(sig) == '(a, b, /)' assert stringify_signature(sig) == '(a, b, /)'
@ -373,8 +368,6 @@ def test_signature_from_str_kwonly_args():
assert sig.parameters['b'].default == Parameter.empty 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(): def test_signature_from_str_positionaly_only_args():
sig = inspect.signature_from_str('(a, b=0, /, c=1)') sig = inspect.signature_from_str('(a, b=0, /, c=1)')
assert list(sig.parameters.keys()) == ['a', 'b', 'c'] assert list(sig.parameters.keys()) == ['a', 'b', 'c']

View File

@ -162,7 +162,6 @@ def test_restify_type_ForwardRef():
assert restify(ForwardRef("myint")) == ":py:class:`myint`" 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(): def test_restify_type_Literal():
from typing import Literal # type: ignore from typing import Literal # type: ignore
assert restify(Literal[1, "2", "\r"]) == ":py:obj:`~typing.Literal`\\ [1, '2', '\\r']" 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 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(): def test_stringify_type_Literal():
from typing import Literal # type: ignore from typing import Literal # type: ignore
assert stringify(Literal[1, "2", "\r"]) == "Literal[1, '2', '\\r']" assert stringify(Literal[1, "2", "\r"]) == "Literal[1, '2', '\\r']"

View File

@ -105,6 +105,22 @@ def f21(arg1='whatever', arg2=Signature.empty):
pass 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: class Node:
def __init__(self, parent: Optional['Node']) -> None: def __init__(self, parent: Optional['Node']) -> None:
pass pass

View File

@ -1,6 +1,6 @@
[tox] [tox]
minversion = 2.4.0 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 isolated_build = True
[testenv] [testenv]
@ -16,7 +16,7 @@ passenv =
EPUBCHECK_PATH EPUBCHECK_PATH
TERM TERM
description = 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. du{14,15,16,17,18,19}: Run unit tests with the given version of docutils.
deps = deps =
du14: docutils==0.14.* du14: docutils==0.14.*