Skip to content

Commit

Permalink
Drop support for Python 3.8 (#850)
Browse files Browse the repository at this point in the history
  • Loading branch information
tristanlatr authored Dec 9, 2024
1 parent f63d816 commit aedb970
Show file tree
Hide file tree
Showing 15 changed files with 49 additions and 183 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/unit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ What's New?
in development
^^^^^^^^^^^^^^

* Drop support for Python 3.8.

pydoctor 24.11.1
^^^^^^^^^^^^^^^^

Expand Down
1 change: 0 additions & 1 deletion pydoctor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
23 changes: 4 additions & 19 deletions pydoctor/astbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
61 changes: 11 additions & 50 deletions pydoctor/astutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down
5 changes: 1 addition & 4 deletions pydoctor/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
6 changes: 1 addition & 5 deletions pydoctor/epydoc/markup/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
43 changes: 6 additions & 37 deletions pydoctor/epydoc/markup/_pyval_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)):
Expand Down Expand Up @@ -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 <type>
state.result.append(self.UNKNOWN_REPR)
Expand Down
6 changes: 1 addition & 5 deletions pydoctor/extensions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions pydoctor/test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 10 additions & 27 deletions pydoctor/test/epydoc/test_pyval_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,14 +1020,8 @@ def test_ast_slice() -> None:
o
[
<wbr>
x:y, (z)
]\n""" if sys.version_info < (3,9) else """<document source="pyval_repr">
<obj_reference refuri="o">
o
[
<wbr>
x:y
,
,
<wbr>
<obj_reference refuri="z">
z
Expand Down Expand Up @@ -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}")

Expand Down
Loading

0 comments on commit aedb970

Please sign in to comment.