Skip to content

Commit fe29bb7

Browse files
authored
Run without warnings on python 3.12 and 3.13 (#769)
* Drop the requirement on astor starting at python version 3.9. * Fix deprecations by coping over a part of astor code and adjusting it not not trigger warnings
1 parent 38b4795 commit fe29bb7

File tree

9 files changed

+215
-74
lines changed

9 files changed

+215
-74
lines changed

.github/workflows/unit.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
python-version: [pypy-3.7, 3.7, 3.8, 3.9, '3.10', 3.11, '3.12.0-rc.3']
17+
python-version: [pypy-3.7, 3.7, 3.8, 3.9, '3.10', 3.11, '3.12', '3.13-dev']
1818
os: [ubuntu-20.04]
1919
include:
2020
- os: windows-latest

README.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ in development
7878

7979
This is the last major release to support Python 3.7.
8080

81-
* Drop support for Python 3.6
82-
* Add support for Python 3.12
81+
* Drop support for Python 3.6.
82+
* Add support for Python 3.12 and Python 3.13.
83+
* Astor is no longer a requirement starting at Python 3.9.
8384
* `ExtRegistrar.register_post_processor()` now supports a `priority` argument that is an int.
8485
Highest priority callables will be called first during post-processing.
8586
* Fix too noisy ``--verbose`` mode (suppres some ambiguous annotations warnings).

pydoctor/astbuilder.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,11 @@
1313
Type, TypeVar, Union, cast
1414
)
1515

16-
import astor
1716
from pydoctor import epydoc2stan, model, node2stan, extensions, linker
1817
from pydoctor.epydoc.markup._pyval_repr import colorize_inline_pyval
1918
from pydoctor.astutils import (is_none_literal, is_typing_annotation, is_using_annotations, is_using_typing_final, node2dottedname, node2fullname,
2019
is__name__equals__main__, unstring_annotation, iterassign, extract_docstring_linenum, infer_type, get_parents,
21-
get_docstring_node, NodeVisitor, Parentage, Str)
20+
get_docstring_node, unparse, NodeVisitor, Parentage, Str)
2221

2322

2423
def parseFile(path: Path) -> ast.Module:
@@ -230,8 +229,8 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
230229
name_node = base_node.value
231230

232231
str_base = '.'.join(node2dottedname(name_node) or \
233-
# Fallback on astor if the expression is unknown by node2dottedname().
234-
[astor.to_source(base_node).strip()])
232+
# Fallback on unparse() if the expression is unknown by node2dottedname().
233+
[unparse(base_node).strip()])
235234

236235
# Store the base as string and as ast.expr in rawbases list.
237236
rawbases += [(str_base, base_node)]

pydoctor/astutils.py

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,28 @@
77
import platform
88
import sys
99
from numbers import Number
10-
from typing import Any, Iterator, Optional, List, Iterable, Sequence, TYPE_CHECKING, Tuple, Union, cast
10+
from typing import Any, Callable, Collection, Iterator, Optional, List, Iterable, Sequence, TYPE_CHECKING, Tuple, Union, cast
1111
from inspect import BoundArguments, Signature
1212
import ast
1313

14+
if sys.version_info >= (3, 9):
15+
from ast import unparse as _unparse
16+
else:
17+
from astor import to_source as _unparse
18+
1419
from pydoctor import visitor
1520

1621
if TYPE_CHECKING:
1722
from pydoctor import model
1823

24+
def unparse(node:ast.AST) -> str:
25+
"""
26+
This function convert a node tree back into python sourcecode.
27+
28+
Uses L{ast.unparse} or C{astor.to_source} for python versions before 3.9.
29+
"""
30+
return _unparse(node)
31+
1932
# AST visitors
2033

2134
def iter_values(node: ast.AST) -> Iterator[ast.AST]:
@@ -250,7 +263,7 @@ def visit_Subscript(self, node: ast.Subscript) -> ast.Subscript:
250263
else:
251264
# Other subscript; unstring the slice.
252265
slice = self.visit(node.slice)
253-
return ast.copy_location(ast.Subscript(value, slice, node.ctx), node)
266+
return ast.copy_location(ast.Subscript(value=value, slice=slice, ctx=node.ctx), node)
254267

255268
# For Python >= 3.8:
256269

@@ -488,13 +501,14 @@ def _annotation_for_value(value: object) -> Optional[ast.expr]:
488501
if ann_value is None:
489502
ann_elem = None
490503
elif ann_elem is not None:
491-
ann_elem = ast.Tuple(elts=[ann_elem, ann_value])
504+
ann_elem = ast.Tuple(elts=[ann_elem, ann_value], ctx=ast.Load())
492505
if ann_elem is not None:
493506
if name == 'tuple':
494-
ann_elem = ast.Tuple(elts=[ann_elem, ast.Constant(value=...)])
495-
return ast.Subscript(value=ast.Name(id=name),
496-
slice=ast.Index(value=ann_elem))
497-
return ast.Name(id=name)
507+
ann_elem = ast.Tuple(elts=[ann_elem, ast.Constant(value=...)], ctx=ast.Load())
508+
return ast.Subscript(value=ast.Name(id=name, ctx=ast.Load()),
509+
slice=ann_elem,
510+
ctx=ast.Load())
511+
return ast.Name(id=name, ctx=ast.Load())
498512

499513
def _annotation_for_elements(sequence: Iterable[object]) -> Optional[ast.expr]:
500514
names = set()
@@ -507,7 +521,7 @@ def _annotation_for_elements(sequence: Iterable[object]) -> Optional[ast.expr]:
507521
return None
508522
if len(names) == 1:
509523
name = names.pop()
510-
return ast.Name(id=name)
524+
return ast.Name(id=name, ctx=ast.Load())
511525
else:
512526
# Empty sequence or no uniform type.
513527
return None
@@ -540,3 +554,118 @@ def _yield_parents(n:Optional[ast.AST]) -> Iterator[ast.AST]:
540554
yield from _yield_parents(p)
541555
yield from _yield_parents(getattr(node, 'parent', None))
542556

557+
#Part of the astor library for Python AST manipulation.
558+
#License: 3-clause BSD
559+
#Copyright (c) 2015 Patrick Maupin
560+
_op_data = """
561+
GeneratorExp 1
562+
563+
Assign 1
564+
AnnAssign 1
565+
AugAssign 0
566+
Expr 0
567+
Yield 1
568+
YieldFrom 0
569+
If 1
570+
For 0
571+
AsyncFor 0
572+
While 0
573+
Return 1
574+
575+
Slice 1
576+
Subscript 0
577+
Index 1
578+
ExtSlice 1
579+
comprehension_target 1
580+
Tuple 0
581+
FormattedValue 0
582+
583+
Comma 1
584+
NamedExpr 1
585+
Assert 0
586+
Raise 0
587+
call_one_arg 1
588+
589+
Lambda 1
590+
IfExp 0
591+
592+
comprehension 1
593+
Or or 1
594+
And and 1
595+
Not not 1
596+
597+
Eq == 1
598+
Gt > 0
599+
GtE >= 0
600+
In in 0
601+
Is is 0
602+
NotEq != 0
603+
Lt < 0
604+
LtE <= 0
605+
NotIn not in 0
606+
IsNot is not 0
607+
608+
BitOr | 1
609+
BitXor ^ 1
610+
BitAnd & 1
611+
LShift << 1
612+
RShift >> 0
613+
Add + 1
614+
Sub - 0
615+
Mult * 1
616+
Div / 0
617+
Mod % 0
618+
FloorDiv // 0
619+
MatMult @ 0
620+
PowRHS 1
621+
Invert ~ 1
622+
UAdd + 0
623+
USub - 0
624+
Pow ** 1
625+
Await 1
626+
Num 1
627+
Constant 1
628+
"""
629+
630+
_op_data = [x.split() for x in _op_data.splitlines()] # type:ignore
631+
_op_data = [[x[0], ' '.join(x[1:-1]), int(x[-1])] for x in _op_data if x] # type:ignore
632+
for _index in range(1, len(_op_data)):
633+
_op_data[_index][2] *= 2 # type:ignore
634+
_op_data[_index][2] += _op_data[_index - 1][2] # type:ignore
635+
636+
_deprecated: Collection[str] = ()
637+
if sys.version_info >= (3, 12):
638+
_deprecated = ('Num', 'Str', 'Bytes', 'Ellipsis', 'NameConstant')
639+
_precedence_data = dict((getattr(ast, x, None), z) for x, y, z in _op_data if x not in _deprecated) # type:ignore
640+
_symbol_data = dict((getattr(ast, x, None), y) for x, y, z in _op_data if x not in _deprecated) # type:ignore
641+
642+
class op_util:
643+
"""
644+
This class provides data and functions for mapping
645+
AST nodes to symbols and precedences.
646+
"""
647+
@classmethod
648+
def get_op_symbol(cls, obj:ast.operator|ast.boolop|ast.cmpop|ast.unaryop,
649+
fmt:str='%s',
650+
symbol_data:dict[type[ast.AST]|None, str]=_symbol_data,
651+
type:Callable[[object], type[Any]]=type) -> str:
652+
"""Given an AST node object, returns a string containing the symbol.
653+
"""
654+
return fmt % symbol_data[type(obj)]
655+
@classmethod
656+
def get_op_precedence(cls, obj:ast.operator|ast.boolop|ast.cmpop|ast.unaryop,
657+
precedence_data:dict[type[ast.AST]|None, int]=_precedence_data,
658+
type:Callable[[object], type[Any]]=type) -> int:
659+
"""Given an AST node object, returns the precedence.
660+
"""
661+
return precedence_data[type(obj)]
662+
663+
if not TYPE_CHECKING:
664+
class Precedence(object):
665+
vars().update((cast(str, x), z) for x, _, z in _op_data)
666+
highest = max(cast(int, z) for _, _, z in _op_data) + 2
667+
else:
668+
Precedence: Any
669+
670+
del _op_data, _index, _precedence_data, _symbol_data, _deprecated
671+
# This was part of the astor library for Python AST manipulation.

pydoctor/epydoc/markup/_pyval_repr.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,14 @@
4343
from typing import Any, AnyStr, Union, Callable, Dict, Iterable, Sequence, Optional, List, Tuple, cast
4444

4545
import attr
46-
import astor.op_util
4746
from docutils import nodes
4847
from twisted.web.template import Tag
4948

5049
from pydoctor.epydoc import sre_parse36, sre_constants36 as sre_constants
5150
from pydoctor.epydoc.markup import DocstringLinker
5251
from pydoctor.epydoc.markup.restructuredtext import ParsedRstDocstring
5352
from pydoctor.epydoc.docutils import set_node_attributes, wbr, obj_reference, new_document
54-
from pydoctor.astutils import node2dottedname, bind_args, Parentage, get_parents
53+
from pydoctor.astutils import node2dottedname, bind_args, Parentage, get_parents, unparse, op_util
5554

5655
def decode_with_backslashreplace(s: bytes) -> str:
5756
r"""
@@ -76,6 +75,7 @@ class _MarkedColorizerState:
7675
charpos: int
7776
lineno: int
7877
linebreakok: bool
78+
stacklength: int
7979

8080
class _ColorizerState:
8181
"""
@@ -87,18 +87,20 @@ class _ColorizerState:
8787
then fall back on a multi-line output if that fails.
8888
"""
8989
def __init__(self) -> None:
90-
self.result: List[nodes.Node] = []
90+
self.result: list[nodes.Node] = []
9191
self.charpos = 0
9292
self.lineno = 1
9393
self.linebreakok = True
94-
self.warnings: List[str] = []
94+
self.warnings: list[str] = []
95+
self.stack: list[ast.AST] = []
9596

9697
def mark(self) -> _MarkedColorizerState:
9798
return _MarkedColorizerState(
9899
length=len(self.result),
99100
charpos=self.charpos,
100101
lineno=self.lineno,
101-
linebreakok=self.linebreakok)
102+
linebreakok=self.linebreakok,
103+
stacklength=len(self.stack))
102104

103105
def restore(self, mark: _MarkedColorizerState) -> List[nodes.Node]:
104106
"""
@@ -109,16 +111,17 @@ def restore(self, mark: _MarkedColorizerState) -> List[nodes.Node]:
109111
mark.linebreakok)
110112
trimmed = self.result[mark.length:]
111113
del self.result[mark.length:]
114+
del self.stack[mark.stacklength:]
112115
return trimmed
113116

114117
# TODO: add support for comparators when needed.
115118
# _OperatorDelimitier is needed for:
116-
# - IfExp
117-
# - UnaryOp
118-
# - BinOp, needs special handling for power operator
119-
# - Compare
120-
# - BoolOp
121-
# - Lambda
119+
# - IfExp (TODO)
120+
# - UnaryOp (DONE)
121+
# - BinOp, needs special handling for power operator (DONE)
122+
# - Compare (TODO)
123+
# - BoolOp (DONE)
124+
# - Lambda (TODO)
122125
class _OperatorDelimiter:
123126
"""
124127
A context manager that can add enclosing delimiters to nested operators when needed.
@@ -145,14 +148,14 @@ def __init__(self, colorizer: 'PyvalColorizer', state: _ColorizerState,
145148

146149
# avoid needless parenthesis, since we now collect parents for every nodes
147150
if isinstance(parent_node, (ast.expr, ast.keyword, ast.comprehension)):
148-
precedence = astor.op_util.get_op_precedence(node.op)
151+
precedence = op_util.get_op_precedence(node.op)
149152
if isinstance(parent_node, (ast.UnaryOp, ast.BinOp, ast.BoolOp)):
150-
parent_precedence = astor.op_util.get_op_precedence(parent_node.op)
153+
parent_precedence = op_util.get_op_precedence(parent_node.op)
151154
if isinstance(parent_node.op, ast.Pow) or isinstance(parent_node, ast.BoolOp):
152155
parent_precedence+=1
153156
else:
154157
parent_precedence = colorizer.explicit_precedence.get(
155-
node, astor.op_util.Precedence.highest)
158+
node, op_util.Precedence.highest)
156159

157160
if precedence < parent_precedence:
158161
self.discard = False
@@ -460,7 +463,7 @@ def _colorize_ast_dict(self, items: Iterable[Tuple[Optional[ast.AST], ast.AST]],
460463
self._insert_comma(indent, state)
461464
state.result.append(self.WORD_BREAK_OPPORTUNITY)
462465
if key:
463-
self._set_precedence(astor.op_util.Precedence.Comma, val)
466+
self._set_precedence(op_util.Precedence.Comma, val)
464467
self._colorize(key, state)
465468
self._output(': ', self.COLON_TAG, state)
466469
else:
@@ -545,6 +548,7 @@ def _colorize_ast_constant(self, pyval: ast.AST, state: _ColorizerState) -> None
545548
self._output('...', self.ELLIPSIS_TAG, state)
546549

547550
def _colorize_ast(self, pyval: ast.AST, state: _ColorizerState) -> None:
551+
state.stack.append(pyval)
548552
# Set nodes parent in order to check theirs precedences and add delimiters when needed.
549553
try:
550554
next(get_parents(pyval))
@@ -588,6 +592,7 @@ def _colorize_ast(self, pyval: ast.AST, state: _ColorizerState) -> None:
588592
self._colorize_ast(pyval.value, state)
589593
else:
590594
self._colorize_ast_generic(pyval, state)
595+
assert state.stack.pop() is pyval
591596

592597
def _colorize_ast_unary_op(self, pyval: ast.UnaryOp, state: _ColorizerState) -> None:
593598
with _OperatorDelimiter(self, state, pyval):
@@ -761,7 +766,13 @@ def _colorize_ast_re(self, node:ast.Call, state: _ColorizerState) -> None:
761766

762767
def _colorize_ast_generic(self, pyval: ast.AST, state: _ColorizerState) -> None:
763768
try:
764-
source = astor.to_source(pyval).strip()
769+
# Always wrap the expression inside parenthesis because we can't be sure
770+
# if there are required since we don;t have support for all operators
771+
# See TODO comment in _OperatorDelimiter.
772+
source = unparse(pyval).strip()
773+
if sys.version_info > (3,9) and isinstance(pyval,
774+
(ast.IfExp, ast.Compare, ast.Lambda)) and len(state.stack)>1:
775+
source = f'({source})'
765776
except Exception: # No defined handler for node of type <type>
766777
state.result.append(self.UNKNOWN_REPR)
767778
else:

pydoctor/templatewriter/__init__.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Render pydoctor data as HTML."""
22
from __future__ import annotations
33

4-
from typing import Any, Iterable, Iterator, Optional, Union, TYPE_CHECKING
4+
from typing import Iterable, Iterator, Optional, Union, TYPE_CHECKING
55
if TYPE_CHECKING:
66
from typing_extensions import Protocol, runtime_checkable
77
else:
@@ -11,15 +11,11 @@ def runtime_checkable(f):
1111
import abc
1212
from pathlib import Path, PurePath
1313
import warnings
14-
import sys
1514
from xml.dom import minidom
1615

1716
# Newer APIs from importlib_resources should arrive to stdlib importlib.resources in Python 3.9.
1817
if TYPE_CHECKING:
19-
if sys.version_info >= (3, 9):
20-
from importlib.resources.abc import Traversable
21-
else:
22-
Traversable = Any
18+
from importlib.resources.abc import Traversable
2319
else:
2420
Traversable = object
2521

0 commit comments

Comments
 (0)