From aedb970e69e9b05edd958f15e3ba85e1b90775e1 Mon Sep 17 00:00:00 2001 From: tristanlatr <19967168+tristanlatr@users.noreply.github.com> Date: Mon, 9 Dec 2024 13:45:25 -0500 Subject: [PATCH] Drop support for Python 3.8 (#850) --- .github/workflows/unit.yaml | 4 +- README.rst | 2 + pydoctor/__init__.py | 1 - pydoctor/astbuilder.py | 23 ++-------- pydoctor/astutils.py | 61 +++++-------------------- pydoctor/driver.py | 5 +- pydoctor/epydoc/markup/__init__.py | 6 +-- pydoctor/epydoc/markup/_pyval_repr.py | 43 +++-------------- pydoctor/extensions/__init__.py | 6 +-- pydoctor/test/__init__.py | 4 -- pydoctor/test/epydoc/test_pyval_repr.py | 37 ++++----------- pydoctor/test/test_astbuilder.py | 15 +++--- pydoctor/test/test_templatewriter.py | 14 ++---- pydoctor/themes/__init__.py | 6 +-- setup.cfg | 5 +- 15 files changed, 49 insertions(+), 183 deletions(-) diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 4b25a8531..784515784 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -19,8 +19,8 @@ jobs: strategy: matrix: - python-version: ['pypy-3.8', 'pypy-3.9', 'pypy-3.10', - '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['pypy-3.9', 'pypy-3.10', + '3.9', '3.10', '3.11', '3.12', '3.13'] os: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/README.rst b/README.rst index c6c12d8c5..c8a99a9d2 100644 --- a/README.rst +++ b/README.rst @@ -73,6 +73,8 @@ What's New? in development ^^^^^^^^^^^^^^ +* Drop support for Python 3.8. + pydoctor 24.11.1 ^^^^^^^^^^^^^^^^ diff --git a/pydoctor/__init__.py b/pydoctor/__init__.py index 6abd366e2..c8f38d605 100644 --- a/pydoctor/__init__.py +++ b/pydoctor/__init__.py @@ -3,7 +3,6 @@ Warning: PyDoctor's API isn't stable YET, custom builds are prone to break! """ -# On Python 3.8+, use importlib.metadata from the standard library. import importlib.metadata as importlib_metadata __version__ = importlib_metadata.version('pydoctor') diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py index 149d43c6d..d350aa908 100644 --- a/pydoctor/astbuilder.py +++ b/pydoctor/astbuilder.py @@ -148,24 +148,14 @@ def is_attribute_overridden(obj: model.Attribute, new_value: Optional[ast.expr]) """ return obj.value is not None and new_value is not None -def _extract_annotation_subscript(annotation: ast.Subscript) -> ast.AST: - """ - Extract the "str, bytes" part from annotations like "Union[str, bytes]". - """ - ann_slice = annotation.slice - if sys.version_info < (3,9) and isinstance(ann_slice, ast.Index): - return ann_slice.value - else: - return ann_slice - def extract_final_subscript(annotation: ast.Subscript) -> ast.expr: """ Extract the "str" part from annotations like "Final[str]". @raises ValueError: If the "Final" annotation is not valid. """ - ann_slice = _extract_annotation_subscript(annotation) - if isinstance(ann_slice, (ast.ExtSlice, ast.Slice, ast.Tuple)): + ann_slice = annotation.slice + if isinstance(ann_slice, (ast.Slice, ast.Tuple)): raise ValueError("Annotation is invalid, it should not contain slices.") else: assert isinstance(ann_slice, ast.expr) @@ -1031,8 +1021,7 @@ def _handleFunctionDef(self, elif is_classmethod: func.kind = model.DocumentableKind.CLASS_METHOD - # Position-only arguments were introduced in Python 3.8. - posonlyargs: Sequence[ast.arg] = getattr(node.args, 'posonlyargs', ()) + posonlyargs: Sequence[ast.arg] = node.args.posonlyargs num_pos_args = len(posonlyargs) + len(node.args.args) defaults = node.args.defaults @@ -1138,11 +1127,7 @@ def _annotations_from_function( """ def _get_all_args() -> Iterator[ast.arg]: base_args = func.args - # New on Python 3.8 -- handle absence gracefully - try: - yield from base_args.posonlyargs - except AttributeError: - pass + yield from base_args.posonlyargs yield from base_args.args varargs = base_args.vararg if varargs: diff --git a/pydoctor/astutils.py b/pydoctor/astutils.py index 203735ff0..d8e14f1b7 100644 --- a/pydoctor/astutils.py +++ b/pydoctor/astutils.py @@ -11,24 +11,13 @@ from inspect import BoundArguments, Signature import ast -if sys.version_info >= (3, 9): - from ast import unparse as _unparse -else: - from astor import to_source as _unparse +unparse = ast.unparse from pydoctor import visitor if TYPE_CHECKING: from pydoctor import model -def unparse(node:ast.AST) -> str: - """ - This function convert a node tree back into python sourcecode. - - Uses L{ast.unparse} or C{astor.to_source} for python versions before 3.9. - """ - return _unparse(node) - # AST visitors def iter_values(node: ast.AST) -> Iterator[ast.AST]: @@ -146,32 +135,16 @@ def bind_args(sig: Signature, call: ast.Call) -> BoundArguments: return sig.bind(*call.args, **kwargs) - -if sys.version_info[:2] >= (3, 8): - # Since Python 3.8 "foo" is parsed as ast.Constant. - def get_str_value(expr:ast.expr) -> Optional[str]: - if isinstance(expr, ast.Constant) and isinstance(expr.value, str): - return expr.value - return None - def get_num_value(expr:ast.expr) -> Optional[Number]: - if isinstance(expr, ast.Constant) and isinstance(expr.value, Number): - return expr.value - return None - def _is_str_constant(expr: ast.expr, s: str) -> bool: - return isinstance(expr, ast.Constant) and expr.value == s -else: - # Before Python 3.8 "foo" was parsed as ast.Str. - # TODO: remove me when python3.7 is not supported anymore - def get_str_value(expr:ast.expr) -> Optional[str]: - if isinstance(expr, ast.Str): - return expr.s - return None - def get_num_value(expr:ast.expr) -> Optional[Number]: - if isinstance(expr, ast.Num): - return expr.n - return None - def _is_str_constant(expr: ast.expr, s: str) -> bool: - return isinstance(expr, ast.Str) and expr.s == s +def get_str_value(expr:ast.expr) -> Optional[str]: + if isinstance(expr, ast.Constant) and isinstance(expr.value, str): + return expr.value + return None +def get_num_value(expr:ast.expr) -> Optional[Number]: + if isinstance(expr, ast.Constant) and isinstance(expr.value, Number): + return expr.value + return None +def _is_str_constant(expr: ast.expr, s: str) -> bool: + return isinstance(expr, ast.Constant) and expr.value == s def get_int_value(expr: ast.expr) -> Optional[int]: num = get_num_value(expr) @@ -321,8 +294,6 @@ def visit_fast(self, node: ast.expr) -> ast.expr: visit_Attribute = visit_Name = visit_fast - # For Python >= 3.8: - def visit_Constant(self, node: ast.Constant) -> ast.expr: value = node.value if isinstance(value, str): @@ -332,12 +303,6 @@ def visit_Constant(self, node: ast.Constant) -> ast.expr: assert isinstance(const, ast.Constant), const return const - # For Python < 3.8: - if sys.version_info < (3,8): - # TODO: remove me when python3.7 is not supported anymore - def visit_Str(self, node: ast.Str) -> ast.expr: - return ast.copy_location(self._parse_string(node.s), node) - def upgrade_annotation(node: ast.expr, ctx: model.Documentable, section:str='annotation') -> ast.expr: """ Transform the annotation to use python 3.10+ syntax. @@ -384,8 +349,6 @@ def visit_Subscript(self, node: ast.Subscript) -> ast.expr: # tuple of types, includea single element tuple, which is the same # as the directly using the type: Union[x] == Union[(x,)] == x slice_ = node.slice - if sys.version_info <= (3,9) and isinstance(slice_, ast.Index): # Compat - slice_ = slice_.value if isinstance(slice_, ast.Tuple): args = slice_.elts if len(args) > 1: @@ -398,8 +361,6 @@ def visit_Subscript(self, node: ast.Subscript) -> ast.expr: elif fullName == 'typing.Optional': # typing.Optional requires a single type, so we don't process when slice is a tuple. slice_ = node.slice - if sys.version_info <= (3,9) and isinstance(slice_, ast.Index): # Compat - slice_ = slice_.value if isinstance(slice_, (ast.Attribute, ast.Name, ast.Subscript, ast.BinOp)): return self._union_args_to_bitor([slice_, ast.Constant(value=None)], node) diff --git a/pydoctor/driver.py b/pydoctor/driver.py index 89ac7f418..221d7de52 100644 --- a/pydoctor/driver.py +++ b/pydoctor/driver.py @@ -15,10 +15,7 @@ # In newer Python versions, use importlib.resources from the standard library. # On older versions, a compatibility package must be installed from PyPI. -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources +import importlib.resources as importlib_resources def get_system(options: model.Options) -> model.System: """ diff --git a/pydoctor/epydoc/markup/__init__.py b/pydoctor/epydoc/markup/__init__.py index 04ba9ba05..613443aed 100644 --- a/pydoctor/epydoc/markup/__init__.py +++ b/pydoctor/epydoc/markup/__init__.py @@ -35,7 +35,6 @@ from typing import Callable, ContextManager, List, Optional, Sequence, Iterator, TYPE_CHECKING import abc -import sys import re from importlib import import_module from inspect import getmodulename @@ -49,10 +48,7 @@ # In newer Python versions, use importlib.resources from the standard library. # On older versions, a compatibility package must be installed from PyPI. -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources +import importlib.resources as importlib_resources if TYPE_CHECKING: from twisted.web.template import Flattenable diff --git a/pydoctor/epydoc/markup/_pyval_repr.py b/pydoctor/epydoc/markup/_pyval_repr.py index 7691b79a1..1275385cb 100644 --- a/pydoctor/epydoc/markup/_pyval_repr.py +++ b/pydoctor/epydoc/markup/_pyval_repr.py @@ -38,7 +38,6 @@ import re import ast import functools -import sys from inspect import signature from typing import Any, AnyStr, Union, Callable, Dict, Iterable, Sequence, Optional, List, Tuple, cast @@ -517,35 +516,9 @@ def _colorize_str(self, pyval: AnyStr, state: _ColorizerState, prefix: AnyStr, # comparators, # generator expressions, # Slice and ExtSlice - - @staticmethod - def _is_ast_constant(node: ast.AST) -> bool: - if sys.version_info[:2] >= (3, 8): - return isinstance(node, ast.Constant) - else: - # TODO: remove me when python3.7 is not supported anymore - return isinstance(node, (ast.Num, ast.Str, ast.Bytes, - ast.Constant, ast.NameConstant, ast.Ellipsis)) - @staticmethod - def _get_ast_constant_val(node: ast.AST) -> Any: - # Deprecated since version 3.8: Replaced by Constant - if sys.version_info[:2] >= (3, 8): - if isinstance(node, ast.Constant): - return node.value - else: - # TODO: remove me when python3.7 is not supported anymore - if isinstance(node, ast.Num): - return(node.n) - if isinstance(node, (ast.Str, ast.Bytes)): - return(node.s) - if isinstance(node, (ast.Constant, ast.NameConstant)): - return(node.value) - if isinstance(node, ast.Ellipsis): - return(...) - raise RuntimeError(f'expected a constant: {ast.dump(node)}') - def _colorize_ast_constant(self, pyval: ast.AST, state: _ColorizerState) -> None: - val = self._get_ast_constant_val(pyval) + def _colorize_ast_constant(self, pyval: ast.Constant, state: _ColorizerState) -> None: + val = pyval.value # Handle elipsis if val != ...: self._colorize(val, state) @@ -560,7 +533,7 @@ def _colorize_ast(self, pyval: ast.AST, state: _ColorizerState) -> None: except StopIteration: Parentage().visit(pyval) - if self._is_ast_constant(pyval): + if isinstance(pyval, ast.Constant): self._colorize_ast_constant(pyval, state) elif isinstance(pyval, ast.UnaryOp): self._colorize_ast_unary_op(pyval, state) @@ -666,9 +639,6 @@ def _colorize_ast_subscript(self, node: ast.Subscript, state: _ColorizerState) - self._colorize(node.value, state) sub: ast.AST = node.slice - if sys.version_info < (3,9) and isinstance(sub, ast.Index): - # In Python < 3.9, non-slices are always wrapped in an Index node. - sub = sub.value self._output('[', self.GROUP_TAG, state) self._set_precedence(op_util.Precedence.Subscript, node) self._set_precedence(op_util.Precedence.Index, sub) @@ -712,11 +682,11 @@ def _colorize_ast_re(self, node:ast.Call, state: _ColorizerState) -> None: ast_pattern = args.arguments['pattern'] # Cannot colorize regex - if not self._is_ast_constant(ast_pattern): + if not isinstance(ast_pattern, ast.Constant): self._colorize_ast_call_generic(node, state) return - pat = self._get_ast_constant_val(ast_pattern) + pat = ast_pattern.value # Just in case regex pattern is not valid type if not isinstance(pat, (bytes, str)): @@ -755,8 +725,7 @@ def _colorize_ast_generic(self, pyval: ast.AST, state: _ColorizerState) -> None: # if there are required since we don;t have support for all operators # See TODO comment in _OperatorDelimiter. source = unparse(pyval).strip() - if sys.version_info > (3,9) and isinstance(pyval, - (ast.IfExp, ast.Compare, ast.Lambda)) and len(state.stack)>1: + if isinstance(pyval, (ast.IfExp, ast.Compare, ast.Lambda)) and len(state.stack)>1: source = f'({source})' except Exception: # No defined handler for node of type state.result.append(self.UNKNOWN_REPR) diff --git a/pydoctor/extensions/__init__.py b/pydoctor/extensions/__init__.py index 83965d838..53b8ad256 100644 --- a/pydoctor/extensions/__init__.py +++ b/pydoctor/extensions/__init__.py @@ -6,15 +6,11 @@ from __future__ import annotations import importlib -import sys from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union, TYPE_CHECKING, cast # In newer Python versions, use importlib.resources from the standard library. # On older versions, a compatibility package must be installed from PyPI. -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources +import importlib.resources as importlib_resources if TYPE_CHECKING: from pydoctor import astbuilder, model diff --git a/pydoctor/test/__init__.py b/pydoctor/test/__init__.py index d6a07e9f9..370870253 100644 --- a/pydoctor/test/__init__.py +++ b/pydoctor/test/__init__.py @@ -2,16 +2,12 @@ from logging import LogRecord from typing import Iterable, TYPE_CHECKING, Sequence -import sys -import pytest from pathlib import Path from pydoctor import epydoc2stan, model from pydoctor.templatewriter import IWriter, TemplateLookup from pydoctor.linker import NotFoundLinker -posonlyargs = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8") -typecomment = pytest.mark.skipif(sys.version_info < (3, 8), reason="requires python 3.8") NotFoundLinker = NotFoundLinker # Because pytest 6.1 does not yet export types for fixtures, we define diff --git a/pydoctor/test/epydoc/test_pyval_repr.py b/pydoctor/test/epydoc/test_pyval_repr.py index 50e88adf6..f34d7e144 100644 --- a/pydoctor/test/epydoc/test_pyval_repr.py +++ b/pydoctor/test/epydoc/test_pyval_repr.py @@ -1020,14 +1020,8 @@ def test_ast_slice() -> None: o [ - x:y, (z) - ]\n""" if sys.version_info < (3,9) else """ - - o - [ - x:y - , + , z @@ -1535,32 +1529,21 @@ def test_expressions_parens(subtests:Any) -> None: check_src("(x if x else y).C") check_src("not (x == y)") - if sys.version_info>=(3,8): - check_src("(a := b)") + check_src("(a := b)") if sys.version_info >= (3,11): check_src("(lambda: int)()") else: check_src("(lambda : int)()") - if sys.version_info > (3,9): - check_src("3 .__abs__()") - check_src("await x") - check_src("x if x else y") - check_src("lambda x: x") - check_src("x == (not y)") - check_src("P * V if P and V else n * R * T") - check_src("lambda P, V, n: P * V == n * R * T") - else: - check_src("(3).__abs__()") - if sys.version_info>=(3,7): - check_src("(await x)") - check_src("(x if x else y)") - check_src("(lambda x: x)") - check_src("(x == (not y))") - check_src("(P * V if P and V else n * R * T)") - check_src("(lambda P, V, n: P * V == n * R * T)") - + check_src("3 .__abs__()") + check_src("await x") + check_src("x if x else y") + check_src("lambda x: x") + check_src("x == (not y)") + check_src("P * V if P and V else n * R * T") + check_src("lambda P, V, n: P * V == n * R * T") + check_src("f(**x)") check_src("{**x}") diff --git a/pydoctor/test/test_astbuilder.py b/pydoctor/test/test_astbuilder.py index 5678753f4..60b3320ee 100644 --- a/pydoctor/test/test_astbuilder.py +++ b/pydoctor/test/test_astbuilder.py @@ -1,6 +1,5 @@ from typing import Optional, Tuple, Type, List, overload, cast import ast -import sys from pydoctor import astbuilder, astutils, model from pydoctor import epydoc2stan @@ -12,7 +11,7 @@ from pydoctor.test.test_packages import processPackage from pydoctor.utils import partialclass -from . import CapSys, NotFoundLinker, posonlyargs, typecomment +from . import CapSys, NotFoundLinker import pytest class SimpleSystem(model.System): @@ -235,7 +234,6 @@ def test_function_signature(signature: str, systemcls: Type[model.System]) -> No text = flatten_text(html2stan(str(docfunc.signature))) assert text == signature -@posonlyargs @pytest.mark.parametrize('signature', ( '(x, y, /)', '(x, y=0, /)', @@ -1361,7 +1359,6 @@ def __init__(self): assert m.docstring == """module-level""" assert type2html(m) == 'bytes' -@typecomment @systemcls_param def test_type_comment(systemcls: Type[model.System], capsys: CapSys) -> None: mod = fromText(''' @@ -2941,7 +2938,7 @@ def test_augmented_assignment(systemcls: Type[model.System]) -> None: attr = mod.contents['var'] assert isinstance(attr, model.Attribute) assert attr.value - assert astutils.unparse(attr.value).strip() == '1 + 3' if sys.version_info >= (3,9) else '(1 + 3)' + assert astutils.unparse(attr.value).strip() == '1 + 3' @systemcls_param def test_augmented_assignment_in_class(systemcls: Type[model.System]) -> None: @@ -2953,7 +2950,7 @@ class c: attr = mod.contents['c'].contents['var'] assert isinstance(attr, model.Attribute) assert attr.value - assert astutils.unparse(attr.value).strip() == '1 + 3' if sys.version_info >= (3,9) else '(1 + 3)' + assert astutils.unparse(attr.value).strip() == '1 + 3' @systemcls_param @@ -2971,7 +2968,7 @@ def test_augmented_assignment_conditionnal_else_ignored(systemcls: Type[model.Sy attr = mod.contents['var'] assert isinstance(attr, model.Attribute) assert attr.value - assert astutils.unparse(attr.value).strip() == '1 + 3' if sys.version_info >= (3,9) else '(1 + 3)' + assert astutils.unparse(attr.value).strip() == '1 + 3' @systemcls_param def test_augmented_assignment_conditionnal_multiple_assignments(systemcls: Type[model.System]) -> None: @@ -2989,7 +2986,7 @@ def test_augmented_assignment_conditionnal_multiple_assignments(systemcls: Type[ attr = mod.contents['var'] assert isinstance(attr, model.Attribute) assert attr.value - assert astutils.unparse(attr.value).strip() == '1 + 3 + 4' if sys.version_info >= (3,9) else '(1 + 3 + 4)' + assert astutils.unparse(attr.value).strip() == '1 + 3 + 4' @systemcls_param def test_augmented_assignment_instance_var(systemcls: Type[model.System]) -> None: @@ -3005,7 +3002,7 @@ def __init__(self, var): attr = mod.contents['c'].contents['var'] assert isinstance(attr, model.Attribute) assert attr.value - assert astutils.unparse(attr.value).strip() == '1' if sys.version_info >= (3,9) else '(1)' + assert astutils.unparse(attr.value).strip() == '1' @systemcls_param def test_augmented_assignment_not_suitable_for_inline_docstring(systemcls: Type[model.System]) -> None: diff --git a/pydoctor/test/test_templatewriter.py b/pydoctor/test/test_templatewriter.py index 2e899c411..fb2c6d27d 100644 --- a/pydoctor/test/test_templatewriter.py +++ b/pydoctor/test/test_templatewriter.py @@ -1,9 +1,8 @@ from io import BytesIO import re -from typing import Callable, Union, Any, cast, Type, TYPE_CHECKING +from typing import Callable, Union, cast, Type, TYPE_CHECKING import pytest import warnings -import sys import tempfile import os from pathlib import Path, PurePath @@ -24,19 +23,12 @@ if TYPE_CHECKING: from twisted.web.template import Flattenable - # Newer APIs from importlib_resources should arrive to stdlib importlib.resources in Python 3.9. - if sys.version_info >= (3, 9): - from importlib.abc import Traversable - else: - Traversable = Any + from importlib.abc import Traversable else: Traversable = object -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources +import importlib.resources as importlib_resources template_dir = importlib_resources.files("pydoctor.themes") / "base" diff --git a/pydoctor/themes/__init__.py b/pydoctor/themes/__init__.py index 241a4984d..6c14494fd 100644 --- a/pydoctor/themes/__init__.py +++ b/pydoctor/themes/__init__.py @@ -5,15 +5,11 @@ >>> template_lookup = TemplateLookup(importlib_resources.files('pydoctor.themes') / 'base') """ -import sys from typing import Iterator # In newer Python versions, use importlib.resources from the standard library. # On older versions, a compatibility package must be installed from PyPI. -if sys.version_info < (3, 9): - import importlib_resources -else: - import importlib.resources as importlib_resources +import importlib.resources as importlib_resources def get_themes() -> Iterator[str]: """ diff --git a/setup.cfg b/setup.cfg index b8f69278a..abccb809e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,6 @@ classifiers = License :: OSI Approved :: MIT License Operating System :: OS Independent Programming Language :: Python :: 3 - Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 @@ -32,7 +31,7 @@ classifiers = [options] packages = find: -python_requires = >=3.8 +python_requires = >=3.9 install_requires = ; New requirements are OK but since pydotor is published as a debian package, ; we should mak sure requirements already exists in repository https://tracker.debian.org/. @@ -47,8 +46,6 @@ install_requires = configargparse toml; python_version < "3.11" - astor; python_version < "3.9" - importlib_resources; python_version < "3.9" [options.extras_require] docs =