pyflyby 1.9.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyflyby might be problematic. Click here for more details.
- pyflyby/__init__.py +56 -0
- pyflyby/__main__.py +9 -0
- pyflyby/_autoimp.py +2114 -0
- pyflyby/_cmdline.py +531 -0
- pyflyby/_comms.py +221 -0
- pyflyby/_dbg.py +1339 -0
- pyflyby/_docxref.py +379 -0
- pyflyby/_file.py +738 -0
- pyflyby/_flags.py +230 -0
- pyflyby/_format.py +182 -0
- pyflyby/_idents.py +233 -0
- pyflyby/_import_sorting.py +165 -0
- pyflyby/_importclns.py +642 -0
- pyflyby/_importdb.py +588 -0
- pyflyby/_imports2s.py +639 -0
- pyflyby/_importstmt.py +662 -0
- pyflyby/_interactive.py +2605 -0
- pyflyby/_livepatch.py +793 -0
- pyflyby/_log.py +199 -0
- pyflyby/_modules.py +515 -0
- pyflyby/_parse.py +1441 -0
- pyflyby/_py.py +2078 -0
- pyflyby/_util.py +459 -0
- pyflyby/_version.py +7 -0
- pyflyby/autoimport.py +20 -0
- pyflyby/importdb.py +19 -0
- pyflyby-1.9.4.data/data/etc/pyflyby/canonical.py +10 -0
- pyflyby-1.9.4.data/data/etc/pyflyby/common.py +27 -0
- pyflyby-1.9.4.data/data/etc/pyflyby/forget.py +10 -0
- pyflyby-1.9.4.data/data/etc/pyflyby/mandatory.py +10 -0
- pyflyby-1.9.4.data/data/etc/pyflyby/numpy.py +156 -0
- pyflyby-1.9.4.data/data/etc/pyflyby/std.py +335 -0
- pyflyby-1.9.4.data/data/libexec/pyflyby/colordiff +34 -0
- pyflyby-1.9.4.data/data/libexec/pyflyby/diff-colorize +148 -0
- pyflyby-1.9.4.data/data/share/doc/pyflyby/LICENSE.txt +23 -0
- pyflyby-1.9.4.data/data/share/doc/pyflyby/TODO.txt +115 -0
- pyflyby-1.9.4.data/data/share/doc/pyflyby/testing.txt +13 -0
- pyflyby-1.9.4.data/data/share/emacs/site-lisp/pyflyby.el +108 -0
- pyflyby-1.9.4.data/scripts/collect-exports +76 -0
- pyflyby-1.9.4.data/scripts/collect-imports +58 -0
- pyflyby-1.9.4.data/scripts/find-import +38 -0
- pyflyby-1.9.4.data/scripts/list-bad-xrefs +34 -0
- pyflyby-1.9.4.data/scripts/prune-broken-imports +34 -0
- pyflyby-1.9.4.data/scripts/pyflyby-diff +34 -0
- pyflyby-1.9.4.data/scripts/reformat-imports +27 -0
- pyflyby-1.9.4.data/scripts/replace-star-imports +37 -0
- pyflyby-1.9.4.data/scripts/tidy-imports +191 -0
- pyflyby-1.9.4.data/scripts/transform-imports +47 -0
- pyflyby-1.9.4.dist-info/LICENSE.txt +23 -0
- pyflyby-1.9.4.dist-info/METADATA +507 -0
- pyflyby-1.9.4.dist-info/RECORD +54 -0
- pyflyby-1.9.4.dist-info/WHEEL +5 -0
- pyflyby-1.9.4.dist-info/entry_points.txt +3 -0
- pyflyby-1.9.4.dist-info/top_level.txt +1 -0
pyflyby/_parse.py
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
# pyflyby/_parse.py.
|
|
2
|
+
# Copyright (C) 2011, 2012, 2013, 2014, 2015, 2018 Karl Chen.
|
|
3
|
+
# License: MIT http://opensource.org/licenses/MIT
|
|
4
|
+
from __future__ import annotations, print_function
|
|
5
|
+
|
|
6
|
+
import ast
|
|
7
|
+
from ast import AsyncFunctionDef, TypeIgnore
|
|
8
|
+
|
|
9
|
+
from collections import namedtuple
|
|
10
|
+
from doctest import DocTestParser
|
|
11
|
+
from functools import total_ordering
|
|
12
|
+
from itertools import groupby
|
|
13
|
+
|
|
14
|
+
from pyflyby._file import FilePos, FileText, Filename
|
|
15
|
+
from pyflyby._flags import CompilerFlags
|
|
16
|
+
from pyflyby._log import logger
|
|
17
|
+
from pyflyby._util import cached_attribute, cmp
|
|
18
|
+
|
|
19
|
+
import re
|
|
20
|
+
import sys
|
|
21
|
+
from textwrap import dedent
|
|
22
|
+
import types
|
|
23
|
+
from typing import Any, List, Optional, Tuple, Union, cast
|
|
24
|
+
import warnings
|
|
25
|
+
|
|
26
|
+
_sentinel = object()
|
|
27
|
+
|
|
28
|
+
if sys.version_info < (3, 10):
|
|
29
|
+
|
|
30
|
+
class MatchAs:
|
|
31
|
+
name: str
|
|
32
|
+
pattern: ast.AST
|
|
33
|
+
|
|
34
|
+
class MatchMapping:
|
|
35
|
+
keys: List[ast.AST]
|
|
36
|
+
patterns: List[MatchAs]
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
from ast import MatchAs, MatchMapping
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_comment_or_blank(line, /):
|
|
43
|
+
"""
|
|
44
|
+
Returns whether a line of python code contains only a comment is blank.
|
|
45
|
+
|
|
46
|
+
>>> _is_comment_or_blank("foo\\n")
|
|
47
|
+
False
|
|
48
|
+
|
|
49
|
+
>>> _is_comment_or_blank(" # blah\\n")
|
|
50
|
+
True
|
|
51
|
+
"""
|
|
52
|
+
return re.sub("#.*", "", line).rstrip() == ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _is_ast_str_or_byte(node) -> bool:
|
|
56
|
+
"""
|
|
57
|
+
utility function that test if node is an ast.Str|ast.Bytes in Python < 3.12,
|
|
58
|
+
and if it is a ast.Constant, with node.value being a str in newer version.
|
|
59
|
+
"""
|
|
60
|
+
return _is_ast_str(node) or _is_ast_bytes(node)
|
|
61
|
+
|
|
62
|
+
def _is_ast_bytes(node) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
utility function that test if node is an ast.Str in Python < 3.12,
|
|
65
|
+
and if it is a ast.Constant, with node.value being a str in newer version.
|
|
66
|
+
"""
|
|
67
|
+
if sys.version_info < (3,12):
|
|
68
|
+
return isinstance(node, ast.Bytes)
|
|
69
|
+
else:
|
|
70
|
+
return (isinstance(node, ast.Constant) and isinstance(node.value , bytes))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _is_ast_str(node) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
utility function that test if node is an ast.Str in Python < 3.12,
|
|
76
|
+
and if it is a ast.Constant, with node.value being a str in newer version.
|
|
77
|
+
"""
|
|
78
|
+
if sys.version_info < (3,12):
|
|
79
|
+
return isinstance(node, ast.Str)
|
|
80
|
+
else:
|
|
81
|
+
return (isinstance(node, ast.Constant) and isinstance(node.value , str))
|
|
82
|
+
|
|
83
|
+
def _ast_str_literal_value(node):
|
|
84
|
+
if _is_ast_str_or_byte(node):
|
|
85
|
+
return node.s
|
|
86
|
+
if isinstance(node, ast.Expr) and _is_ast_str_or_byte(node.value):
|
|
87
|
+
if sys.version_info > (3,10):
|
|
88
|
+
return node.value.value
|
|
89
|
+
else:
|
|
90
|
+
return node.value.s
|
|
91
|
+
else:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _flatten_ast_nodes(arg):
|
|
96
|
+
if arg is None:
|
|
97
|
+
pass
|
|
98
|
+
elif isinstance(arg, ast.AST):
|
|
99
|
+
yield arg
|
|
100
|
+
elif isinstance(arg, str):
|
|
101
|
+
#FunctionDef type_comments
|
|
102
|
+
yield arg
|
|
103
|
+
elif isinstance(arg, (tuple, list, types.GeneratorType)):
|
|
104
|
+
for x in arg:
|
|
105
|
+
for y in _flatten_ast_nodes(x):
|
|
106
|
+
yield y
|
|
107
|
+
else:
|
|
108
|
+
raise TypeError(
|
|
109
|
+
"_flatten_ast_nodes: unexpected %s" % (type(arg).__name__,))
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _iter_child_nodes_in_order(node):
|
|
113
|
+
"""
|
|
114
|
+
Yield all direct child nodes of ``node``, that is, all fields that are nodes
|
|
115
|
+
and all items of fields that are lists of nodes.
|
|
116
|
+
|
|
117
|
+
``_iter_child_nodes_in_order`` yields nodes in the same order that they
|
|
118
|
+
appear in the source.
|
|
119
|
+
|
|
120
|
+
``ast.iter_child_nodes`` does the same thing, but not in source order.
|
|
121
|
+
e.g. for ``Dict`` s, it yields all key nodes before all value nodes.
|
|
122
|
+
"""
|
|
123
|
+
return _flatten_ast_nodes(_iter_child_nodes_in_order_internal_1(node))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _iter_child_nodes_in_order_internal_1(node):
|
|
127
|
+
if isinstance(node, str):
|
|
128
|
+
# this happen for type comments which are not ast nodes but str
|
|
129
|
+
# they do not have children. We yield nothing.
|
|
130
|
+
yield []
|
|
131
|
+
return
|
|
132
|
+
if not isinstance(node, ast.AST):
|
|
133
|
+
raise TypeError
|
|
134
|
+
if isinstance(node, ast.Dict):
|
|
135
|
+
assert node._fields == ("keys", "values")
|
|
136
|
+
yield list(zip(node.keys, node.values))
|
|
137
|
+
elif isinstance(node, (ast.FunctionDef, AsyncFunctionDef)):
|
|
138
|
+
if sys.version_info < (3,12):
|
|
139
|
+
assert node._fields == (
|
|
140
|
+
"name",
|
|
141
|
+
"args",
|
|
142
|
+
"body",
|
|
143
|
+
"decorator_list",
|
|
144
|
+
"returns",
|
|
145
|
+
"type_comment",
|
|
146
|
+
), node._fields
|
|
147
|
+
res = (
|
|
148
|
+
node.type_comment,
|
|
149
|
+
node.decorator_list,
|
|
150
|
+
node.args,
|
|
151
|
+
node.returns,
|
|
152
|
+
node.body,
|
|
153
|
+
)
|
|
154
|
+
yield res
|
|
155
|
+
else:
|
|
156
|
+
assert node._fields == (
|
|
157
|
+
"name",
|
|
158
|
+
"args",
|
|
159
|
+
"body",
|
|
160
|
+
"decorator_list",
|
|
161
|
+
"returns",
|
|
162
|
+
"type_comment",
|
|
163
|
+
"type_params"
|
|
164
|
+
), node._fields
|
|
165
|
+
res = (
|
|
166
|
+
node.type_comment,
|
|
167
|
+
node.decorator_list,
|
|
168
|
+
node.args,
|
|
169
|
+
node.returns,
|
|
170
|
+
node.body,
|
|
171
|
+
node.type_params
|
|
172
|
+
)
|
|
173
|
+
yield res
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# node.name is a string, not an AST node
|
|
177
|
+
elif isinstance(node, ast.arguments):
|
|
178
|
+
assert node._fields == ('posonlyargs', 'args', 'vararg', 'kwonlyargs',
|
|
179
|
+
'kw_defaults', 'kwarg', 'defaults'), node._fields
|
|
180
|
+
args = node.posonlyargs + node.args
|
|
181
|
+
defaults = node.defaults or ()
|
|
182
|
+
num_no_default = len(args) - len(defaults)
|
|
183
|
+
yield args[:num_no_default]
|
|
184
|
+
yield list(zip(args[num_no_default:], defaults))
|
|
185
|
+
# node.varags and node.kwarg are strings, not AST nodes.
|
|
186
|
+
elif isinstance(node, ast.IfExp):
|
|
187
|
+
assert node._fields == ('test', 'body', 'orelse')
|
|
188
|
+
yield node.body, node.test, node.orelse
|
|
189
|
+
elif isinstance(node, ast.Call):
|
|
190
|
+
# call arguments order are lost by ast, re-order them
|
|
191
|
+
yield node.func
|
|
192
|
+
args = sorted([(k.value.lineno, k.value.col_offset, k) for k in node.keywords]+
|
|
193
|
+
[(k.lineno,k.col_offset, k) for k in node.args])
|
|
194
|
+
yield [a[2] for a in args]
|
|
195
|
+
elif isinstance(node, ast.ClassDef):
|
|
196
|
+
if sys.version_info > (3, 12):
|
|
197
|
+
assert node._fields == ('name', 'bases', 'keywords', 'body', 'decorator_list', 'type_params'), node._fields
|
|
198
|
+
yield node.decorator_list, node.bases, node.body, node.type_params
|
|
199
|
+
else:
|
|
200
|
+
assert node._fields == ('name', 'bases', 'keywords', 'body', 'decorator_list'), node._fields
|
|
201
|
+
yield node.decorator_list, node.bases, node.body
|
|
202
|
+
# node.name is a string, not an AST node
|
|
203
|
+
elif isinstance(node, ast.FormattedValue):
|
|
204
|
+
assert node._fields == ('value', 'conversion', 'format_spec')
|
|
205
|
+
yield node.value,
|
|
206
|
+
elif isinstance(node, MatchAs):
|
|
207
|
+
yield node.pattern
|
|
208
|
+
yield node.name,
|
|
209
|
+
elif isinstance(node, MatchMapping):
|
|
210
|
+
for k, p in zip(node.keys, node.patterns):
|
|
211
|
+
yield k, p
|
|
212
|
+
else:
|
|
213
|
+
# Default behavior.
|
|
214
|
+
yield ast.iter_child_nodes(node)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _walk_ast_nodes_in_order(node):
|
|
218
|
+
"""
|
|
219
|
+
Recursively yield all child nodes of ``node``, in the same order that the
|
|
220
|
+
node appears in the source.
|
|
221
|
+
|
|
222
|
+
``ast.walk`` does the same thing, but yields nodes in an arbitrary order.
|
|
223
|
+
"""
|
|
224
|
+
# The implementation is basically the same as ``ast.walk``, but:
|
|
225
|
+
# 1. Use a stack instead of a deque. (I.e., depth-first search instead
|
|
226
|
+
# of breadth-first search.)
|
|
227
|
+
# 2. Use _iter_child_nodes_in_order instead of ``ast.iter_child_nodes``.
|
|
228
|
+
todo = [node]
|
|
229
|
+
while todo:
|
|
230
|
+
node = todo.pop()
|
|
231
|
+
yield node
|
|
232
|
+
todo.extend(reversed(list(_iter_child_nodes_in_order(node))))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _flags_to_try(source:str, flags, auto_flags, mode):
|
|
236
|
+
"""
|
|
237
|
+
Flags to try for ``auto_flags``.
|
|
238
|
+
|
|
239
|
+
If ``auto_flags`` is False, then only yield ``flags``.
|
|
240
|
+
If ``auto_flags`` is True, then yield ``flags`` and ``flags ^ print_function``.
|
|
241
|
+
"""
|
|
242
|
+
flags = CompilerFlags(flags)
|
|
243
|
+
if re.search(r"# *type:", source):
|
|
244
|
+
flags = flags | CompilerFlags('type_comments')
|
|
245
|
+
yield flags
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _parse_ast_nodes(text:FileText, flags:CompilerFlags, auto_flags:bool, mode:str):
|
|
250
|
+
"""
|
|
251
|
+
Parse a block of lines into an AST.
|
|
252
|
+
|
|
253
|
+
Also annotate ``input_flags``, ``source_flags``, and ``flags`` on the
|
|
254
|
+
resulting ast node.
|
|
255
|
+
|
|
256
|
+
:type text:
|
|
257
|
+
``FileText``
|
|
258
|
+
:type flags:
|
|
259
|
+
``CompilerFlags``
|
|
260
|
+
:type auto_flags:
|
|
261
|
+
``bool``
|
|
262
|
+
:param auto_flags:
|
|
263
|
+
Whether to guess different flags if ``text`` can't be parsed with
|
|
264
|
+
``flags``.
|
|
265
|
+
:param mode:
|
|
266
|
+
Compilation mode: "exec", "single", or "eval".
|
|
267
|
+
:rtype:
|
|
268
|
+
``ast.Module``
|
|
269
|
+
"""
|
|
270
|
+
assert isinstance(text, FileText)
|
|
271
|
+
filename = str(text.filename) if text.filename else "<unknown>"
|
|
272
|
+
source = text.joined
|
|
273
|
+
source = dedent(source)
|
|
274
|
+
if not source.endswith("\n"):
|
|
275
|
+
# Ensure that the last line ends with a newline (``ast`` barfs
|
|
276
|
+
# otherwise).
|
|
277
|
+
source += "\n"
|
|
278
|
+
exp = None
|
|
279
|
+
for flags in _flags_to_try(source, flags, auto_flags, mode):
|
|
280
|
+
cflags = ast.PyCF_ONLY_AST | int(flags)
|
|
281
|
+
try:
|
|
282
|
+
result = compile(
|
|
283
|
+
source, filename, mode, flags=cflags, dont_inherit=True)
|
|
284
|
+
except SyntaxError as e:
|
|
285
|
+
exp = e
|
|
286
|
+
pass
|
|
287
|
+
else:
|
|
288
|
+
# Attach flags to the result.
|
|
289
|
+
result.input_flags = flags
|
|
290
|
+
result.source_flags = CompilerFlags.from_ast(result)
|
|
291
|
+
result.flags = result.input_flags | result.source_flags
|
|
292
|
+
result.text = text
|
|
293
|
+
return result
|
|
294
|
+
# None, would be unraisable and Mypy would complains below
|
|
295
|
+
assert exp is not None
|
|
296
|
+
raise exp
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _test_parse_string_literal(text:str, flags:CompilerFlags):
|
|
301
|
+
r"""
|
|
302
|
+
Attempt to parse ``text``. If it parses cleanly to a single string
|
|
303
|
+
literal, return its value. Otherwise return ``None``.
|
|
304
|
+
|
|
305
|
+
>>> _test_parse_string_literal(r'"foo\n" r"\nbar"', None)
|
|
306
|
+
'foo\n\\nbar'
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
filetext = FileText(text)
|
|
310
|
+
try:
|
|
311
|
+
module_node = _parse_ast_nodes(filetext, flags, False, "eval")
|
|
312
|
+
except SyntaxError:
|
|
313
|
+
return None
|
|
314
|
+
body = module_node.body
|
|
315
|
+
if not _is_ast_str_or_byte(body):
|
|
316
|
+
return None
|
|
317
|
+
if sys.version_info < (3 ,9):
|
|
318
|
+
return body.s
|
|
319
|
+
else:
|
|
320
|
+
return body.value
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
AstNodeContext = namedtuple("AstNodeContext", "parent field index")
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _annotate_ast_nodes(ast_node: ast.AST) -> AnnotatedAst:
|
|
327
|
+
"""
|
|
328
|
+
Annotate AST with:
|
|
329
|
+
- startpos and endpos
|
|
330
|
+
- [disabled for now: context as `AstNodeContext` ]
|
|
331
|
+
|
|
332
|
+
:type ast_node:
|
|
333
|
+
``ast.AST``
|
|
334
|
+
:param ast_node:
|
|
335
|
+
AST node returned by `_parse_ast_nodes`
|
|
336
|
+
:return:
|
|
337
|
+
``None``
|
|
338
|
+
"""
|
|
339
|
+
aast_node: AnnotatedAst = ast_node # type: ignore
|
|
340
|
+
text = aast_node.text
|
|
341
|
+
flags = aast_node.flags
|
|
342
|
+
startpos = text.startpos
|
|
343
|
+
_annotate_ast_startpos(aast_node, None, startpos, text, flags)
|
|
344
|
+
# Not used for now:
|
|
345
|
+
# ast_node.context = AstNodeContext(None, None, None)
|
|
346
|
+
# _annotate_ast_context(ast_node)
|
|
347
|
+
return aast_node
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _annotate_ast_startpos(
|
|
351
|
+
ast_node: ast.AST, parent_ast_node, minpos, text, flags
|
|
352
|
+
) -> bool:
|
|
353
|
+
r"""
|
|
354
|
+
Annotate ``ast_node``. Set ``ast_node.startpos`` to the starting position
|
|
355
|
+
of the node within ``text``.
|
|
356
|
+
|
|
357
|
+
For "typical" nodes, i.e. those other than multiline strings, this is
|
|
358
|
+
simply FilePos(ast_node.lineno, ast_node.col_offset+1), but taking
|
|
359
|
+
``text.startpos`` into account.
|
|
360
|
+
|
|
361
|
+
For multiline string nodes, this function works by trying to parse all
|
|
362
|
+
possible subranges of lines until finding the range that is syntactically
|
|
363
|
+
valid and matches ``value``. The candidate range is
|
|
364
|
+
text[min_start_lineno:lineno+text.startpos.lineno+1].
|
|
365
|
+
|
|
366
|
+
This function is unfortunately necessary because of a flaw in the output
|
|
367
|
+
produced by the Python built-in parser. For some crazy reason, the
|
|
368
|
+
``ast_node.lineno`` attribute represents something different for multiline
|
|
369
|
+
string literals versus all other statements. For multiline string literal
|
|
370
|
+
nodes and statements that are just a string expression (or more generally,
|
|
371
|
+
nodes where the first descendant leaf node is a multiline string literal),
|
|
372
|
+
the compiler attaches the ending line number as the value of the ``lineno``
|
|
373
|
+
attribute. For all other than AST nodes, the compiler attaches the
|
|
374
|
+
starting line number as the value of the ``lineno`` attribute. This means
|
|
375
|
+
e.g. the statement "'''foo\nbar'''" has a lineno value of 2, but the
|
|
376
|
+
statement "x='''foo\nbar'''" has a lineno value of 1.
|
|
377
|
+
|
|
378
|
+
:type ast_node:
|
|
379
|
+
``ast.AST``
|
|
380
|
+
:type minpos:
|
|
381
|
+
``FilePos``
|
|
382
|
+
:param minpos:
|
|
383
|
+
Earliest position to check, in the number space of ``text``.
|
|
384
|
+
:type text:
|
|
385
|
+
``FileText``
|
|
386
|
+
:param text:
|
|
387
|
+
Source text that was used to parse the AST, whose ``startpos`` should be
|
|
388
|
+
used in interpreting ``ast_node.lineno`` (which always starts at 1 for
|
|
389
|
+
the subset that was parsed).
|
|
390
|
+
:type flags:
|
|
391
|
+
``CompilerFlags``
|
|
392
|
+
:param flags:
|
|
393
|
+
Compiler flags to use when re-compiling code.
|
|
394
|
+
:return:
|
|
395
|
+
``True`` if this node is a multiline string literal or the first child is
|
|
396
|
+
such a node (recursively); ``False`` otherwise.
|
|
397
|
+
:raise ValueError:
|
|
398
|
+
Could not find the starting line number.
|
|
399
|
+
"""
|
|
400
|
+
assert isinstance(ast_node, (ast.AST, str, TypeIgnore)), ast_node
|
|
401
|
+
aast_node: AnnotatedAst = cast(AnnotatedAst, ast_node)
|
|
402
|
+
|
|
403
|
+
# First, traverse child nodes. If the first child node (recursively) is a
|
|
404
|
+
# multiline string, then we need to transfer its information to this node.
|
|
405
|
+
# Walk all nodes/fields of the AST. We implement this as a custom
|
|
406
|
+
# depth-first search instead of using ast.walk() or ast.NodeVisitor
|
|
407
|
+
# so that we can easily keep track of the preceding node's lineno.
|
|
408
|
+
child_minpos = minpos
|
|
409
|
+
is_first_child = True
|
|
410
|
+
leftstr_node = None
|
|
411
|
+
for child_node in _iter_child_nodes_in_order(aast_node):
|
|
412
|
+
leftstr = _annotate_ast_startpos(
|
|
413
|
+
child_node, aast_node, child_minpos, text, flags
|
|
414
|
+
)
|
|
415
|
+
if is_first_child and leftstr:
|
|
416
|
+
leftstr_node = child_node
|
|
417
|
+
if hasattr(child_node, 'lineno') and not isinstance(child_node, TypeIgnore):
|
|
418
|
+
if child_node.startpos < child_minpos:
|
|
419
|
+
raise AssertionError(
|
|
420
|
+
"Got out-of-order AST node(s):\n"
|
|
421
|
+
" parent minpos=%s\n" % minpos
|
|
422
|
+
+ " node: %s\n" % ast.dump(aast_node)
|
|
423
|
+
+ " fields: %s\n" % (" ".join(aast_node._fields))
|
|
424
|
+
+ " children:\n"
|
|
425
|
+
+ "".join(
|
|
426
|
+
" %s %9s: %s\n"
|
|
427
|
+
% (
|
|
428
|
+
("==>" if cn is child_node else " "),
|
|
429
|
+
getattr(cn, "startpos", ""),
|
|
430
|
+
ast.dump(cn),
|
|
431
|
+
)
|
|
432
|
+
for cn in _iter_child_nodes_in_order(aast_node)
|
|
433
|
+
)
|
|
434
|
+
+ "\n"
|
|
435
|
+
"This indicates a bug in pyflyby._\n"
|
|
436
|
+
"\n"
|
|
437
|
+
"pyflyby developer: Check if there's a bug or missing ast node handler in "
|
|
438
|
+
"pyflyby._parse._iter_child_nodes_in_order() - "
|
|
439
|
+
"probably the handler for ast.%s." % type(aast_node).__name__
|
|
440
|
+
)
|
|
441
|
+
child_minpos = child_node.startpos
|
|
442
|
+
is_first_child = False
|
|
443
|
+
|
|
444
|
+
# If the node has no lineno at all, then skip it. This should only happen
|
|
445
|
+
# for nodes we don't care about, e.g. ``ast.Module`` or ``ast.alias``.
|
|
446
|
+
if not hasattr(aast_node, "lineno") or isinstance(aast_node, TypeIgnore):
|
|
447
|
+
return False
|
|
448
|
+
# If col_offset is set then the lineno should be correct also.
|
|
449
|
+
if aast_node.col_offset >= 0:
|
|
450
|
+
# In Python 3.8+, FunctionDef.lineno is the line with the def. To
|
|
451
|
+
# account for decorators, we need the lineno of the first decorator
|
|
452
|
+
if (
|
|
453
|
+
isinstance(aast_node, (ast.FunctionDef, ast.ClassDef, AsyncFunctionDef))
|
|
454
|
+
and aast_node.decorator_list
|
|
455
|
+
):
|
|
456
|
+
delta = (
|
|
457
|
+
aast_node.decorator_list[0].lineno - 1,
|
|
458
|
+
# The col_offset doesn't include the @
|
|
459
|
+
aast_node.decorator_list[0].col_offset - 1,
|
|
460
|
+
)
|
|
461
|
+
else:
|
|
462
|
+
delta = (aast_node.lineno - 1, aast_node.col_offset)
|
|
463
|
+
|
|
464
|
+
# Not a multiline string literal. (I.e., it could be a non-string or
|
|
465
|
+
# a single-line string.)
|
|
466
|
+
# Easy.
|
|
467
|
+
startpos = text.startpos + delta
|
|
468
|
+
|
|
469
|
+
# Special case for 'with' statements. Consider the code:
|
|
470
|
+
# with X: pass
|
|
471
|
+
# ^0 ^5
|
|
472
|
+
# Since 'Y's col_offset isn't the beginning of the line, the authors
|
|
473
|
+
# of Python presumably changed 'X's col_offset to also not be the
|
|
474
|
+
# beginning of the line. If they had made the With ast node support
|
|
475
|
+
# multiple clauses, they wouldn't have needed to do that, but then
|
|
476
|
+
# that would introduce an API change in the AST. So it's
|
|
477
|
+
# understandable that they did that.
|
|
478
|
+
# Since we use startpos for breaking lines, we need to set startpos to
|
|
479
|
+
# the beginning of the line.
|
|
480
|
+
# In Python 3, the col_offset for the with is 0 again.
|
|
481
|
+
aast_node.startpos = startpos
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
assert aast_node.col_offset == -1
|
|
485
|
+
if leftstr_node:
|
|
486
|
+
# This is an ast node where the leftmost deepest leaf is a
|
|
487
|
+
# multiline string. The bug that multiline strings have broken
|
|
488
|
+
# lineno/col_offset infects ancestors up the tree.
|
|
489
|
+
#
|
|
490
|
+
# If the leftmost leaf is a multi-line string, then ``lineno``
|
|
491
|
+
# contains the ending line number, and col_offset is -1:
|
|
492
|
+
# >>> ast.parse("""'''foo\nbar'''+blah""").body[0].lineno
|
|
493
|
+
# 2
|
|
494
|
+
# But if the leftmost leaf is not a multi-line string, then
|
|
495
|
+
# ``lineno`` contains the starting line number:
|
|
496
|
+
# >>> ast.parse("""'''foobar'''+blah""").body[0].lineno
|
|
497
|
+
# 1
|
|
498
|
+
# >>> ast.parse("""blah+'''foo\nbar'''+blah""").body[0].lineno
|
|
499
|
+
# 1
|
|
500
|
+
#
|
|
501
|
+
# To fix that, we copy start_lineno and start_colno from the Str
|
|
502
|
+
# node once we've corrected the values.
|
|
503
|
+
assert not _is_ast_str_or_byte(aast_node)
|
|
504
|
+
assert leftstr_node.lineno == aast_node.lineno
|
|
505
|
+
assert leftstr_node.col_offset == -1
|
|
506
|
+
aast_node.startpos = leftstr_node.startpos
|
|
507
|
+
return True
|
|
508
|
+
|
|
509
|
+
# a large chunk of what look like unreachable code has been removed from here
|
|
510
|
+
# as the type annotation say many things were impossible (slices indexed by FilePos
|
|
511
|
+
# instead of integers.
|
|
512
|
+
raise ValueError("Couldn't find exact position of %s" % (ast.dump(ast_node)))
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _annotate_ast_context(ast_node):
|
|
516
|
+
"""
|
|
517
|
+
Recursively annotate ``context`` on ast nodes, setting ``context`` to
|
|
518
|
+
a `AstNodeContext` named tuple with values
|
|
519
|
+
``(parent, field, index)``.
|
|
520
|
+
Each aast_node satisfies ``parent.<field>[<index>] is ast_node``.
|
|
521
|
+
|
|
522
|
+
For non-list fields, the index part is ``None``.
|
|
523
|
+
"""
|
|
524
|
+
assert isinstance(ast_node, ast.AST)
|
|
525
|
+
for field_name, field_value in ast.iter_fields(ast_node):
|
|
526
|
+
if isinstance(field_value, ast.AST):
|
|
527
|
+
child_node = field_value
|
|
528
|
+
child_node.context = AstNodeContext(ast_node, field_name, None)
|
|
529
|
+
_annotate_ast_context(child_node)
|
|
530
|
+
elif isinstance(field_value, list):
|
|
531
|
+
for i, item in enumerate(field_value):
|
|
532
|
+
if isinstance(item, ast.AST):
|
|
533
|
+
child_node = item
|
|
534
|
+
child_node.context = AstNodeContext(ast_node, field_name, i)
|
|
535
|
+
_annotate_ast_context(child_node)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _split_code_lines(ast_nodes, text):
|
|
539
|
+
"""
|
|
540
|
+
Split the given ``ast_nodes`` and corresponding ``text`` by code/noncode
|
|
541
|
+
statement.
|
|
542
|
+
|
|
543
|
+
Yield tuples of (nodes, subtext). ``nodes`` is a list of ``ast.AST`` nodes,
|
|
544
|
+
length 0 or 1; ``subtext`` is a `FileText` sliced from ``text``.
|
|
545
|
+
|
|
546
|
+
FileText(...))} for code lines and ``(None, FileText(...))`` for non-code
|
|
547
|
+
lines (comments and blanks).
|
|
548
|
+
|
|
549
|
+
:type ast_nodes:
|
|
550
|
+
sequence of ``ast.AST`` nodes
|
|
551
|
+
:type text:
|
|
552
|
+
`FileText`
|
|
553
|
+
"""
|
|
554
|
+
if not ast_nodes:
|
|
555
|
+
yield ([], text)
|
|
556
|
+
return
|
|
557
|
+
assert text.startpos <= ast_nodes[0].startpos
|
|
558
|
+
assert ast_nodes[-1].startpos < text.endpos
|
|
559
|
+
if text.startpos != ast_nodes[0].startpos:
|
|
560
|
+
# Starting noncode lines.
|
|
561
|
+
yield ([], text[text.startpos:ast_nodes[0].startpos])
|
|
562
|
+
end_sentinel = _DummyAst_Node()
|
|
563
|
+
end_sentinel.startpos = text.endpos
|
|
564
|
+
for node, next_node in zip(ast_nodes, ast_nodes[1:] + [end_sentinel]):
|
|
565
|
+
startpos = node.startpos
|
|
566
|
+
next_startpos = next_node.startpos
|
|
567
|
+
assert startpos < next_startpos
|
|
568
|
+
# We have the start position of this node. Figure out the end
|
|
569
|
+
# position, excluding noncode lines (standalone comments and blank
|
|
570
|
+
# lines).
|
|
571
|
+
if hasattr(node, 'endpos'):
|
|
572
|
+
# We have an endpos for the node because this was a multi-line
|
|
573
|
+
# string. Start with the node endpos.
|
|
574
|
+
endpos = node.endpos
|
|
575
|
+
assert startpos < endpos <= next_startpos
|
|
576
|
+
# enpos points to the character *after* the ending quote, so we
|
|
577
|
+
# know that this is never at the beginning of the line.
|
|
578
|
+
assert endpos.colno != 1
|
|
579
|
+
# Advance past whitespace an inline comment, if any. Do NOT
|
|
580
|
+
# advance past other code that could be on the same line, nor past
|
|
581
|
+
# blank lines and comments on subsequent lines.
|
|
582
|
+
line = text[endpos : min(text.endpos, FilePos(endpos.lineno+1,1))]
|
|
583
|
+
if _is_comment_or_blank(line):
|
|
584
|
+
endpos = FilePos(endpos.lineno+1, 1)
|
|
585
|
+
else:
|
|
586
|
+
endpos = next_startpos
|
|
587
|
+
assert endpos <= text.endpos
|
|
588
|
+
# We don't have an endpos yet; what we do have is the next node's
|
|
589
|
+
# startpos (or the position at the end of the text). Start there
|
|
590
|
+
# and work backward.
|
|
591
|
+
if endpos.colno != 1:
|
|
592
|
+
if endpos == text.endpos:
|
|
593
|
+
# There could be a comment on the last line and no
|
|
594
|
+
# trailing newline.
|
|
595
|
+
# TODO: do this in a more principled way.
|
|
596
|
+
if _is_comment_or_blank(text[endpos.lineno]):
|
|
597
|
+
assert startpos.lineno < endpos.lineno
|
|
598
|
+
if not text[endpos.lineno-1].endswith("\\"):
|
|
599
|
+
endpos = FilePos(endpos.lineno,1)
|
|
600
|
+
else:
|
|
601
|
+
# We're not at end of file, yet the next node starts in
|
|
602
|
+
# the middle of the line. This should only happen with if
|
|
603
|
+
# we're not looking at a comment. [The first character in
|
|
604
|
+
# the line could still be "#" if we're inside a multiline
|
|
605
|
+
# string that's the last child of the parent node.
|
|
606
|
+
# Therefore we don't assert 'not
|
|
607
|
+
# _is_comment_or_blank(...)'.]
|
|
608
|
+
pass
|
|
609
|
+
if endpos.colno == 1:
|
|
610
|
+
while (endpos.lineno-1 > startpos.lineno and
|
|
611
|
+
_is_comment_or_blank(text[endpos.lineno-1]) and
|
|
612
|
+
(not text[endpos.lineno-2].endswith("\\") or
|
|
613
|
+
_is_comment_or_blank(text[endpos.lineno-2]))):
|
|
614
|
+
endpos = FilePos(endpos.lineno-1, 1)
|
|
615
|
+
assert startpos < endpos <= next_startpos
|
|
616
|
+
yield ([node], text[startpos:endpos])
|
|
617
|
+
if endpos != next_startpos:
|
|
618
|
+
yield ([], text[endpos:next_startpos])
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _ast_node_is_in_docstring_position(ast_node):
|
|
622
|
+
"""
|
|
623
|
+
Given a ``Str`` AST node, return whether its position within the AST makes
|
|
624
|
+
it eligible as a docstring.
|
|
625
|
+
|
|
626
|
+
The main way a ``Str`` can be a docstring is if it is a standalone string
|
|
627
|
+
at the beginning of a ``Module``, ``FunctionDef``, ``AsyncFucntionDef``
|
|
628
|
+
or ``ClassDef``.
|
|
629
|
+
|
|
630
|
+
We also support variable docstrings per Epydoc:
|
|
631
|
+
|
|
632
|
+
- If a variable assignment statement is immediately followed by a bare
|
|
633
|
+
string literal, then that assignment is treated as a docstring for
|
|
634
|
+
that variable.
|
|
635
|
+
|
|
636
|
+
:type ast_node:
|
|
637
|
+
``ast.Str``
|
|
638
|
+
:param ast_node:
|
|
639
|
+
AST node that has been annotated by ``_annotate_ast_nodes``.
|
|
640
|
+
:rtype:
|
|
641
|
+
``bool``
|
|
642
|
+
:return:
|
|
643
|
+
Whether this string ast node is in docstring position.
|
|
644
|
+
"""
|
|
645
|
+
if not _is_ast_str_or_byte(ast_node):
|
|
646
|
+
raise TypeError
|
|
647
|
+
expr_node = ast_node.context.parent
|
|
648
|
+
if not isinstance(expr_node, ast.Expr):
|
|
649
|
+
return False
|
|
650
|
+
assert ast_node.context.field == 'value'
|
|
651
|
+
assert ast_node.context.index is None
|
|
652
|
+
expr_ctx = expr_node.context
|
|
653
|
+
if expr_ctx.field != 'body':
|
|
654
|
+
return False
|
|
655
|
+
parent_node = expr_ctx.parent
|
|
656
|
+
if not isinstance(parent_node, (ast.FunctionDef, ast.ClassDef, ast.Module, AsyncFunctionDef)):
|
|
657
|
+
return False
|
|
658
|
+
if expr_ctx.index == 0:
|
|
659
|
+
return True
|
|
660
|
+
prev_sibling_node = parent_node.body[expr_ctx.index-1]
|
|
661
|
+
if isinstance(prev_sibling_node, ast.Assign):
|
|
662
|
+
return True
|
|
663
|
+
return False
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def infer_compile_mode(arg):
|
|
667
|
+
"""
|
|
668
|
+
Infer the mode needed to compile ``arg``.
|
|
669
|
+
|
|
670
|
+
:type arg:
|
|
671
|
+
``ast.AST``
|
|
672
|
+
:rtype:
|
|
673
|
+
``str``
|
|
674
|
+
"""
|
|
675
|
+
# Infer mode from ast object.
|
|
676
|
+
if isinstance(arg, ast.Module):
|
|
677
|
+
mode = "exec"
|
|
678
|
+
elif isinstance(arg, ast.Expression):
|
|
679
|
+
mode = "eval"
|
|
680
|
+
elif isinstance(arg, ast.Interactive):
|
|
681
|
+
mode = "single"
|
|
682
|
+
else:
|
|
683
|
+
raise TypeError(
|
|
684
|
+
"Expected Module/Expression/Interactive ast node; got %s"
|
|
685
|
+
% (type(arg).__name__))
|
|
686
|
+
return mode
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class _DummyAst_Node(object):
|
|
690
|
+
pass
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
class PythonStatement:
|
|
694
|
+
r"""
|
|
695
|
+
Representation of a top-level Python statement or consecutive
|
|
696
|
+
comments/blank lines.
|
|
697
|
+
|
|
698
|
+
>>> PythonStatement('print("x",\n file=None)\n') #doctest: +SKIP
|
|
699
|
+
PythonStatement('print("x",\n file=None)\n')
|
|
700
|
+
|
|
701
|
+
Implemented as a wrapper around a `PythonBlock` containing at most one
|
|
702
|
+
top-level AST node.
|
|
703
|
+
"""
|
|
704
|
+
|
|
705
|
+
block: "PythonBlock"
|
|
706
|
+
|
|
707
|
+
def __new__(cls, arg:Union[FileText, str], filename=None, startpos=None):
|
|
708
|
+
|
|
709
|
+
if not isinstance(arg, (FileText, str)):
|
|
710
|
+
raise TypeError("PythonStatement: unexpected %s" % type(arg).__name__)
|
|
711
|
+
|
|
712
|
+
block = PythonBlock(arg, filename=filename, startpos=startpos)
|
|
713
|
+
|
|
714
|
+
return cls.from_block(block)
|
|
715
|
+
|
|
716
|
+
@classmethod
|
|
717
|
+
def from_statement(cls, statement):
|
|
718
|
+
assert isinstance(statement, cls), (statement, cls)
|
|
719
|
+
return statement
|
|
720
|
+
|
|
721
|
+
@classmethod
|
|
722
|
+
def from_block(cls, block:PythonBlock) -> PythonStatement:
|
|
723
|
+
"""
|
|
724
|
+
Return a statement from a PythonBlock
|
|
725
|
+
|
|
726
|
+
This assume the PythonBlock is a single statement and check the comments
|
|
727
|
+
to not start with newlines.
|
|
728
|
+
"""
|
|
729
|
+
statements = block.statements
|
|
730
|
+
if len(statements) != 1:
|
|
731
|
+
raise ValueError(
|
|
732
|
+
"Code contains %d statements instead of exactly 1: %r"
|
|
733
|
+
% (len(statements), block)
|
|
734
|
+
)
|
|
735
|
+
(statement,) = statements
|
|
736
|
+
assert isinstance(statement, cls)
|
|
737
|
+
if statement.is_comment:
|
|
738
|
+
assert not statement.text.joined.startswith("\n")
|
|
739
|
+
return statement
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
@classmethod
|
|
743
|
+
def _construct_from_block(cls, block:PythonBlock):
|
|
744
|
+
# Only to be used by PythonBlock.
|
|
745
|
+
assert isinstance(block, PythonBlock), repr(block)
|
|
746
|
+
self = object.__new__(cls)
|
|
747
|
+
self.block = block
|
|
748
|
+
if self.is_comment:
|
|
749
|
+
assert not self.text.joined.startswith("\n"), self.text.joined
|
|
750
|
+
return self
|
|
751
|
+
|
|
752
|
+
@property
|
|
753
|
+
def text(self) -> FileText:
|
|
754
|
+
"""
|
|
755
|
+
:rtype:
|
|
756
|
+
`FileText`
|
|
757
|
+
"""
|
|
758
|
+
return self.block.text
|
|
759
|
+
|
|
760
|
+
@property
|
|
761
|
+
def filename(self) -> Optional[Filename]:
|
|
762
|
+
"""
|
|
763
|
+
:rtype:
|
|
764
|
+
`Filename`
|
|
765
|
+
"""
|
|
766
|
+
return self.text.filename
|
|
767
|
+
|
|
768
|
+
@property
|
|
769
|
+
def startpos(self):
|
|
770
|
+
"""
|
|
771
|
+
:rtype:
|
|
772
|
+
`FilePos`
|
|
773
|
+
"""
|
|
774
|
+
return self.text.startpos
|
|
775
|
+
|
|
776
|
+
@property
|
|
777
|
+
def flags(self):
|
|
778
|
+
"""
|
|
779
|
+
:rtype:
|
|
780
|
+
`CompilerFlags`
|
|
781
|
+
"""
|
|
782
|
+
return self.block.flags
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def ast_node(self):
|
|
786
|
+
"""
|
|
787
|
+
A single AST node representing this statement, or ``None`` if this
|
|
788
|
+
object only represents comments/blanks.
|
|
789
|
+
|
|
790
|
+
:rtype:
|
|
791
|
+
``ast.AST`` or ``NoneType``
|
|
792
|
+
"""
|
|
793
|
+
ast_nodes = self.block.ast_node.body
|
|
794
|
+
if len(ast_nodes) == 0:
|
|
795
|
+
return None
|
|
796
|
+
if len(ast_nodes) == 1:
|
|
797
|
+
return ast_nodes[0]
|
|
798
|
+
raise AssertionError("More than one AST node in block")
|
|
799
|
+
|
|
800
|
+
@property
|
|
801
|
+
def is_blank(self):
|
|
802
|
+
return self.ast_node is None and self.text.joined.strip() == ''
|
|
803
|
+
|
|
804
|
+
@property
|
|
805
|
+
def is_comment(self):
|
|
806
|
+
return self.ast_node is None and self.text.joined.strip() != ''
|
|
807
|
+
|
|
808
|
+
@property
|
|
809
|
+
def is_comment_or_blank(self):
|
|
810
|
+
return self.is_comment or self.is_blank
|
|
811
|
+
|
|
812
|
+
@property
|
|
813
|
+
def is_comment_or_blank_or_string_literal(self):
|
|
814
|
+
return (self.is_comment_or_blank
|
|
815
|
+
or _ast_str_literal_value(self.ast_node) is not None)
|
|
816
|
+
|
|
817
|
+
@property
|
|
818
|
+
def is_import(self):
|
|
819
|
+
return isinstance(self.ast_node, (ast.Import, ast.ImportFrom))
|
|
820
|
+
|
|
821
|
+
@property
|
|
822
|
+
def is_single_assign(self):
|
|
823
|
+
n = self.ast_node
|
|
824
|
+
return isinstance(n, ast.Assign) and len(n.targets) == 1
|
|
825
|
+
|
|
826
|
+
def get_assignment_literal_value(self):
|
|
827
|
+
"""
|
|
828
|
+
If the statement is an assignment, return the name and literal value.
|
|
829
|
+
|
|
830
|
+
>>> PythonStatement('foo = {1: {2: 3}}').get_assignment_literal_value()
|
|
831
|
+
('foo', {1: {2: 3}})
|
|
832
|
+
|
|
833
|
+
:return:
|
|
834
|
+
(target, literal_value)
|
|
835
|
+
"""
|
|
836
|
+
if not self.is_single_assign:
|
|
837
|
+
raise ValueError(
|
|
838
|
+
"Statement is not an assignment to a single name: %s" % self)
|
|
839
|
+
n = self.ast_node
|
|
840
|
+
target_name = n.targets[0].id
|
|
841
|
+
literal_value = ast.literal_eval(n.value)
|
|
842
|
+
return (target_name, literal_value)
|
|
843
|
+
|
|
844
|
+
def __repr__(self):
|
|
845
|
+
r = repr(self.block)
|
|
846
|
+
assert r.startswith("PythonBlock(")
|
|
847
|
+
r = "PythonStatement(" + r[12:]
|
|
848
|
+
return r
|
|
849
|
+
|
|
850
|
+
def __eq__(self, other):
|
|
851
|
+
if self is other:
|
|
852
|
+
return True
|
|
853
|
+
if not isinstance(other, PythonStatement):
|
|
854
|
+
return NotImplemented
|
|
855
|
+
return self.block == other.block
|
|
856
|
+
|
|
857
|
+
def __ne__(self, other):
|
|
858
|
+
return not (self == other)
|
|
859
|
+
|
|
860
|
+
# The rest are defined by total_ordering
|
|
861
|
+
def __lt__(self, other):
|
|
862
|
+
if not isinstance(other, PythonStatement):
|
|
863
|
+
return NotImplemented
|
|
864
|
+
return self.block < other.block
|
|
865
|
+
|
|
866
|
+
def __cmp__(self, other):
|
|
867
|
+
if self is other:
|
|
868
|
+
return 0
|
|
869
|
+
if not isinstance(other, PythonStatement):
|
|
870
|
+
return NotImplemented
|
|
871
|
+
return cmp(self.block, other.block)
|
|
872
|
+
|
|
873
|
+
def __hash__(self):
|
|
874
|
+
return hash(self.block)
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
class AnnotatedAst(ast.AST):
|
|
878
|
+
text: FileText
|
|
879
|
+
flags: str
|
|
880
|
+
source_flags: CompilerFlags
|
|
881
|
+
startpos: FilePos
|
|
882
|
+
endpos: FilePos
|
|
883
|
+
lieneno: int
|
|
884
|
+
col_offset: int
|
|
885
|
+
value: AnnotatedAst
|
|
886
|
+
s: str
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
class AnnotatedModule(ast.Module, AnnotatedAst):
|
|
890
|
+
source_flags: CompilerFlags
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
@total_ordering
|
|
894
|
+
class PythonBlock:
|
|
895
|
+
r"""
|
|
896
|
+
Representation of a sequence of consecutive top-level
|
|
897
|
+
`PythonStatement` (s).
|
|
898
|
+
|
|
899
|
+
>>> source_code = '# 1\nprint(2)\n# 3\n# 4\nprint(5)\nx=[6,\n 7]\n# 8\n'
|
|
900
|
+
>>> codeblock = PythonBlock(source_code)
|
|
901
|
+
>>> for stmt in PythonBlock(codeblock).statements:
|
|
902
|
+
... print(stmt)
|
|
903
|
+
PythonStatement('# 1\n')
|
|
904
|
+
PythonStatement('print(2)\n', startpos=(2,1))
|
|
905
|
+
PythonStatement('# 3\n# 4\n', startpos=(3,1))
|
|
906
|
+
PythonStatement('print(5)\n', startpos=(5,1))
|
|
907
|
+
PythonStatement('x=[6,\n 7]\n', startpos=(6,1))
|
|
908
|
+
PythonStatement('# 8\n', startpos=(8,1))
|
|
909
|
+
|
|
910
|
+
A ``PythonBlock`` has a ``flags`` attribute that gives the compiler_flags
|
|
911
|
+
associated with the __future__ features using which the code should be
|
|
912
|
+
parsed.
|
|
913
|
+
|
|
914
|
+
"""
|
|
915
|
+
|
|
916
|
+
text: FileText
|
|
917
|
+
_auto_flags: bool
|
|
918
|
+
_input_flags: Union[int,CompilerFlags]
|
|
919
|
+
|
|
920
|
+
def __new__(cls, arg:Any, filename=None, startpos=None, flags=None,
|
|
921
|
+
auto_flags=None):
|
|
922
|
+
if isinstance(arg, PythonStatement):
|
|
923
|
+
arg = arg.block
|
|
924
|
+
# Fall through
|
|
925
|
+
if isinstance(arg, cls):
|
|
926
|
+
if filename is startpos is flags is None:
|
|
927
|
+
return arg
|
|
928
|
+
flags = CompilerFlags(flags, arg.flags)
|
|
929
|
+
arg = arg.text
|
|
930
|
+
# Fall through
|
|
931
|
+
if isinstance(arg, (FileText, Filename, str)):
|
|
932
|
+
return cls.from_text(
|
|
933
|
+
arg, filename=filename, startpos=startpos,
|
|
934
|
+
flags=flags, auto_flags=auto_flags)
|
|
935
|
+
raise TypeError("%r: unexpected %r"
|
|
936
|
+
% (cls.__name__, type(arg).__name__,))
|
|
937
|
+
|
|
938
|
+
@classmethod
|
|
939
|
+
def from_filename(cls, filename):
|
|
940
|
+
return cls.from_text(Filename(filename))
|
|
941
|
+
|
|
942
|
+
@classmethod
|
|
943
|
+
def from_text(cls, text, filename=None, startpos=None, flags=None,
|
|
944
|
+
auto_flags=False):
|
|
945
|
+
"""
|
|
946
|
+
:type text:
|
|
947
|
+
`FileText` or convertible
|
|
948
|
+
:type filename:
|
|
949
|
+
``Filename``
|
|
950
|
+
:param filename:
|
|
951
|
+
Filename, if not already given by ``text``.
|
|
952
|
+
:type startpos:
|
|
953
|
+
``FilePos``
|
|
954
|
+
:param startpos:
|
|
955
|
+
Starting position, if not already given by ``text``.
|
|
956
|
+
:type flags:
|
|
957
|
+
``CompilerFlags``
|
|
958
|
+
:param flags:
|
|
959
|
+
Input compiler flags.
|
|
960
|
+
:param auto_flags:
|
|
961
|
+
Whether to try other flags if ``flags`` fails.
|
|
962
|
+
:rtype:
|
|
963
|
+
`PythonBlock`
|
|
964
|
+
"""
|
|
965
|
+
text = FileText(text, filename=filename, startpos=startpos)
|
|
966
|
+
self = object.__new__(cls)
|
|
967
|
+
self.text = text
|
|
968
|
+
self._input_flags = CompilerFlags(flags)
|
|
969
|
+
self._auto_flags = auto_flags
|
|
970
|
+
return self
|
|
971
|
+
|
|
972
|
+
@classmethod
|
|
973
|
+
def __construct_from_annotated_ast(cls, annotated_ast_nodes, text:FileText, flags):
|
|
974
|
+
# Constructor for internal use by _split_by_statement() or
|
|
975
|
+
# concatenate().
|
|
976
|
+
ast_node = AnnotatedModule(annotated_ast_nodes)
|
|
977
|
+
ast_node.text = text
|
|
978
|
+
ast_node.flags = flags
|
|
979
|
+
if not hasattr(ast_node, "source_flags"):
|
|
980
|
+
ast_node.source_flags = CompilerFlags.from_ast(annotated_ast_nodes)
|
|
981
|
+
self = object.__new__(cls)
|
|
982
|
+
self._ast_node_or_parse_exception = ast_node
|
|
983
|
+
self.ast_node = ast_node
|
|
984
|
+
self.annotated_ast_node = ast_node
|
|
985
|
+
self.text = text
|
|
986
|
+
self.flags = flags
|
|
987
|
+
self._input_flags = flags
|
|
988
|
+
self._auto_flags = False
|
|
989
|
+
return self
|
|
990
|
+
|
|
991
|
+
@classmethod
|
|
992
|
+
def concatenate(cls, blocks, assume_contiguous=_sentinel):
|
|
993
|
+
"""
|
|
994
|
+
Concatenate a bunch of blocks into one block.
|
|
995
|
+
|
|
996
|
+
:type blocks:
|
|
997
|
+
sequence of `PythonBlock` s and/or `PythonStatement` s
|
|
998
|
+
:param assume_contiguous:
|
|
999
|
+
Deprecated, always True
|
|
1000
|
+
Whether to assume, without checking, that the input blocks were
|
|
1001
|
+
originally all contiguous. This must be set to True to indicate the
|
|
1002
|
+
caller understands the assumption; False is not implemented.
|
|
1003
|
+
"""
|
|
1004
|
+
if assume_contiguous is not _sentinel:
|
|
1005
|
+
warnings.warn('`assume_continuous` is deprecated and considered always `True`')
|
|
1006
|
+
assume_contiguous = True
|
|
1007
|
+
if not assume_contiguous:
|
|
1008
|
+
raise NotImplementedError
|
|
1009
|
+
blocks2 = []
|
|
1010
|
+
for b in blocks:
|
|
1011
|
+
if isinstance(b, PythonStatement):
|
|
1012
|
+
b = b.block
|
|
1013
|
+
if not isinstance(b, PythonBlock):
|
|
1014
|
+
b = PythonBlock(b)
|
|
1015
|
+
blocks2.append(b)
|
|
1016
|
+
blocks = blocks2
|
|
1017
|
+
if len(blocks) == 1:
|
|
1018
|
+
return blocks[0]
|
|
1019
|
+
assert blocks
|
|
1020
|
+
text = FileText.concatenate([b.text for b in blocks])
|
|
1021
|
+
# The contiguous assumption is important here because ``ast_node``
|
|
1022
|
+
# contains line information that would otherwise be wrong.
|
|
1023
|
+
ast_nodes = [n for b in blocks for n in b.annotated_ast_node.body]
|
|
1024
|
+
flags = blocks[0].flags
|
|
1025
|
+
return cls.__construct_from_annotated_ast(ast_nodes, text, flags)
|
|
1026
|
+
|
|
1027
|
+
@property
|
|
1028
|
+
def filename(self):
|
|
1029
|
+
return self.text.filename
|
|
1030
|
+
|
|
1031
|
+
@property
|
|
1032
|
+
def startpos(self):
|
|
1033
|
+
return self.text.startpos
|
|
1034
|
+
|
|
1035
|
+
@property
|
|
1036
|
+
def endpos(self):
|
|
1037
|
+
return self.text.endpos
|
|
1038
|
+
|
|
1039
|
+
@cached_attribute
|
|
1040
|
+
def _ast_node_or_parse_exception(self):
|
|
1041
|
+
"""
|
|
1042
|
+
Attempt to parse this block of code into an abstract syntax tree.
|
|
1043
|
+
Cached (including exception case).
|
|
1044
|
+
|
|
1045
|
+
:return:
|
|
1046
|
+
Either ast_node or exception.
|
|
1047
|
+
"""
|
|
1048
|
+
# This attribute may also be set by __construct_from_annotated_ast(),
|
|
1049
|
+
# in which case this code does not run.
|
|
1050
|
+
try:
|
|
1051
|
+
return _parse_ast_nodes(
|
|
1052
|
+
self.text, self._input_flags, self._auto_flags, "exec")
|
|
1053
|
+
except Exception as e:
|
|
1054
|
+
# Add the filename to the exception message to be nicer.
|
|
1055
|
+
if self.text.filename:
|
|
1056
|
+
try:
|
|
1057
|
+
e = type(e)("While parsing %s: %s" % (self.text.filename, e))
|
|
1058
|
+
except TypeError:
|
|
1059
|
+
# Exception takes more than one argument
|
|
1060
|
+
pass
|
|
1061
|
+
# Cache the exception to avoid re-attempting while debugging.
|
|
1062
|
+
return e
|
|
1063
|
+
|
|
1064
|
+
@cached_attribute
|
|
1065
|
+
def parsable(self):
|
|
1066
|
+
"""
|
|
1067
|
+
Whether the contents of this ``PythonBlock`` are parsable as Python
|
|
1068
|
+
code, using the given flags.
|
|
1069
|
+
|
|
1070
|
+
:rtype:
|
|
1071
|
+
``bool``
|
|
1072
|
+
"""
|
|
1073
|
+
return isinstance(self._ast_node_or_parse_exception, ast.AST)
|
|
1074
|
+
|
|
1075
|
+
@cached_attribute
|
|
1076
|
+
def parsable_as_expression(self):
|
|
1077
|
+
"""
|
|
1078
|
+
Whether the contents of this ``PythonBlock`` are parsable as a single
|
|
1079
|
+
Python expression, using the given flags.
|
|
1080
|
+
|
|
1081
|
+
:rtype:
|
|
1082
|
+
``bool``
|
|
1083
|
+
"""
|
|
1084
|
+
return self.parsable and self.expression_ast_node is not None
|
|
1085
|
+
|
|
1086
|
+
@cached_attribute
|
|
1087
|
+
def ast_node(self):
|
|
1088
|
+
"""
|
|
1089
|
+
Parse this block of code into an abstract syntax tree.
|
|
1090
|
+
|
|
1091
|
+
The returned object type is the kind of AST as returned by the
|
|
1092
|
+
``compile`` built-in (rather than as returned by the older, deprecated
|
|
1093
|
+
``compiler`` module). The code is parsed using mode="exec".
|
|
1094
|
+
|
|
1095
|
+
The result is a ``ast.Module`` node, even if this block represents only
|
|
1096
|
+
a subset of the entire file.
|
|
1097
|
+
|
|
1098
|
+
:rtype:
|
|
1099
|
+
``ast.Module``
|
|
1100
|
+
"""
|
|
1101
|
+
r = self._ast_node_or_parse_exception
|
|
1102
|
+
if isinstance(r, ast.AST):
|
|
1103
|
+
return r
|
|
1104
|
+
else:
|
|
1105
|
+
raise r
|
|
1106
|
+
|
|
1107
|
+
@cached_attribute
|
|
1108
|
+
def annotated_ast_node(self) -> AnnotatedAst:
|
|
1109
|
+
"""
|
|
1110
|
+
Return ``self.ast_node``, annotated in place with positions.
|
|
1111
|
+
|
|
1112
|
+
All nodes are annotated with ``startpos``.
|
|
1113
|
+
All top-level nodes are annotated with ``endpos``.
|
|
1114
|
+
|
|
1115
|
+
:rtype:
|
|
1116
|
+
``ast.Module``
|
|
1117
|
+
"""
|
|
1118
|
+
result = self.ast_node
|
|
1119
|
+
# ! result is mutated and returned
|
|
1120
|
+
return _annotate_ast_nodes(result)
|
|
1121
|
+
|
|
1122
|
+
@cached_attribute
|
|
1123
|
+
def expression_ast_node(self) -> Optional[ast.Expression]:
|
|
1124
|
+
"""
|
|
1125
|
+
Return an ``ast.Expression`` if ``self.ast_node`` can be converted into
|
|
1126
|
+
one. I.e., return parse(self.text, mode="eval"), if possible.
|
|
1127
|
+
|
|
1128
|
+
Otherwise, return ``None``.
|
|
1129
|
+
|
|
1130
|
+
:rtype:
|
|
1131
|
+
``ast.Expression``
|
|
1132
|
+
"""
|
|
1133
|
+
node = self.ast_node
|
|
1134
|
+
if len(node.body) == 1 and isinstance(node.body[0], ast.Expr):
|
|
1135
|
+
return ast.Expression(node.body[0].value)
|
|
1136
|
+
else:
|
|
1137
|
+
return None
|
|
1138
|
+
|
|
1139
|
+
def parse(self, mode=None) -> Union[ast.Expression, ast.Module]:
|
|
1140
|
+
"""
|
|
1141
|
+
Parse the source text into an AST.
|
|
1142
|
+
|
|
1143
|
+
:param mode:
|
|
1144
|
+
Compilation mode: "exec", "single", or "eval". "exec", "single",
|
|
1145
|
+
and "eval" work as the built-in ``compile`` function do. If ``None``,
|
|
1146
|
+
then default to "eval" if the input is a string with a single
|
|
1147
|
+
expression, else "exec".
|
|
1148
|
+
:rtype:
|
|
1149
|
+
``ast.AST``
|
|
1150
|
+
"""
|
|
1151
|
+
if mode == "exec":
|
|
1152
|
+
assert isinstance(self.ast_node, ast.Module)
|
|
1153
|
+
return self.ast_node
|
|
1154
|
+
elif mode == "eval":
|
|
1155
|
+
if self.expression_ast_node:
|
|
1156
|
+
assert isinstance(self.ast_node, ast.Expression)
|
|
1157
|
+
return self.expression_ast_node
|
|
1158
|
+
else:
|
|
1159
|
+
raise SyntaxError
|
|
1160
|
+
elif mode == None:
|
|
1161
|
+
if self.expression_ast_node:
|
|
1162
|
+
return self.expression_ast_node
|
|
1163
|
+
else:
|
|
1164
|
+
assert isinstance(self.ast_node, ast.Module)
|
|
1165
|
+
return self.ast_node
|
|
1166
|
+
elif mode == "exec":
|
|
1167
|
+
raise NotImplementedError
|
|
1168
|
+
else:
|
|
1169
|
+
raise ValueError("parse(): invalid mode=%r" % (mode,))
|
|
1170
|
+
|
|
1171
|
+
def compile(self, mode=None):
|
|
1172
|
+
"""
|
|
1173
|
+
Parse into AST and compile AST into code.
|
|
1174
|
+
|
|
1175
|
+
:rtype:
|
|
1176
|
+
``CodeType``
|
|
1177
|
+
"""
|
|
1178
|
+
ast_node = self.parse(mode=mode)
|
|
1179
|
+
mode = infer_compile_mode(ast_node)
|
|
1180
|
+
filename = str(self.filename or "<unknown>")
|
|
1181
|
+
return compile(ast_node, filename, mode)
|
|
1182
|
+
|
|
1183
|
+
@cached_attribute
|
|
1184
|
+
def statements(self) -> Tuple[PythonStatement, ...]:
|
|
1185
|
+
r"""
|
|
1186
|
+
Partition of this ``PythonBlock`` into individual ``PythonStatement`` s.
|
|
1187
|
+
Each one contains at most 1 top-level ast node. A ``PythonStatement``
|
|
1188
|
+
can contain no ast node to represent comments.
|
|
1189
|
+
|
|
1190
|
+
>>> code = "# multiline\n# comment\n'''multiline\nstring'''\nblah\n"
|
|
1191
|
+
>>> print(PythonBlock(code).statements) # doctest:+NORMALIZE_WHITESPACE
|
|
1192
|
+
(PythonStatement('# multiline\n# comment\n'),
|
|
1193
|
+
PythonStatement("'''multiline\nstring'''\n", startpos=(3,1)),
|
|
1194
|
+
PythonStatement('blah\n', startpos=(5,1)))
|
|
1195
|
+
|
|
1196
|
+
:rtype:
|
|
1197
|
+
``tuple`` of `PythonStatement` s
|
|
1198
|
+
"""
|
|
1199
|
+
node = self.annotated_ast_node
|
|
1200
|
+
nodes_subtexts = list(_split_code_lines(node.body, self.text)) # type: ignore
|
|
1201
|
+
cls = type(self)
|
|
1202
|
+
statement_blocks: List[PythonBlock] = [
|
|
1203
|
+
cls.__construct_from_annotated_ast(subnodes, subtext, self.flags)
|
|
1204
|
+
for subnodes, subtext in nodes_subtexts]
|
|
1205
|
+
|
|
1206
|
+
no_newline_blocks = []
|
|
1207
|
+
for block in statement_blocks:
|
|
1208
|
+
# The ast parsing make "comments" start at the ends of the previous node,
|
|
1209
|
+
# so might including starting with blank lines. We never want blocks to
|
|
1210
|
+
# start with new liens or that messes up the formatting code that insert/count new lines.
|
|
1211
|
+
while block.text.joined.startswith("\n") and block.text.joined != "\n":
|
|
1212
|
+
first, *other = block.text.lines
|
|
1213
|
+
assert not first.endswith('\n')
|
|
1214
|
+
no_newline_blocks.append(
|
|
1215
|
+
PythonBlock(
|
|
1216
|
+
first+'\n',
|
|
1217
|
+
filename=block.filename,
|
|
1218
|
+
startpos=block.startpos,
|
|
1219
|
+
flags=block.flags,
|
|
1220
|
+
)
|
|
1221
|
+
)
|
|
1222
|
+
# assert block.startpos == (0,0), (block.startpos, block.text.joined)
|
|
1223
|
+
# just use lines 1: here and decrease startpos ?
|
|
1224
|
+
block = PythonBlock(
|
|
1225
|
+
"\n".join(other),
|
|
1226
|
+
filename=block.filename,
|
|
1227
|
+
startpos=block.startpos,
|
|
1228
|
+
flags=block.flags,
|
|
1229
|
+
)
|
|
1230
|
+
no_newline_blocks.append(block)
|
|
1231
|
+
|
|
1232
|
+
# Convert to statements.
|
|
1233
|
+
statements = []
|
|
1234
|
+
for b in no_newline_blocks:
|
|
1235
|
+
assert isinstance(b, PythonBlock)
|
|
1236
|
+
statement = PythonStatement._construct_from_block(b)
|
|
1237
|
+
statements.append(statement)
|
|
1238
|
+
return tuple(statements)
|
|
1239
|
+
|
|
1240
|
+
@cached_attribute
|
|
1241
|
+
def source_flags(self):
|
|
1242
|
+
"""
|
|
1243
|
+
If the AST contains __future__ imports, then the compiler_flags
|
|
1244
|
+
associated with them. Otherwise, 0.
|
|
1245
|
+
|
|
1246
|
+
The difference between ``source_flags`` and ``flags`` is that ``flags``
|
|
1247
|
+
may be set by the caller (e.g. based on an earlier __future__ import)
|
|
1248
|
+
and include automatically guessed flags, whereas ``source_flags`` is
|
|
1249
|
+
only nonzero if this code itself contains __future__ imports.
|
|
1250
|
+
|
|
1251
|
+
:rtype:
|
|
1252
|
+
`CompilerFlags`
|
|
1253
|
+
"""
|
|
1254
|
+
return self.ast_node.source_flags
|
|
1255
|
+
|
|
1256
|
+
@cached_attribute
|
|
1257
|
+
def flags(self):
|
|
1258
|
+
"""
|
|
1259
|
+
The compiler flags for this code block, including both the input flags
|
|
1260
|
+
(possibly automatically guessed), and the flags from "__future__"
|
|
1261
|
+
imports in the source code text.
|
|
1262
|
+
|
|
1263
|
+
:rtype:
|
|
1264
|
+
`CompilerFlags`
|
|
1265
|
+
"""
|
|
1266
|
+
return self.ast_node.flags
|
|
1267
|
+
|
|
1268
|
+
def groupby(self, predicate):
|
|
1269
|
+
"""
|
|
1270
|
+
Partition this block of code into smaller blocks of code which
|
|
1271
|
+
consecutively have the same ``predicate``.
|
|
1272
|
+
|
|
1273
|
+
:param predicate:
|
|
1274
|
+
Function that takes a `PythonStatement` and returns a value.
|
|
1275
|
+
:return:
|
|
1276
|
+
Generator that yields (group, `PythonBlock` s).
|
|
1277
|
+
"""
|
|
1278
|
+
cls = type(self)
|
|
1279
|
+
for pred, stmts in groupby(self.statements, predicate):
|
|
1280
|
+
blocks = [s.block for s in stmts]
|
|
1281
|
+
yield pred, cls.concatenate(blocks)
|
|
1282
|
+
|
|
1283
|
+
def string_literals(self):
|
|
1284
|
+
r"""
|
|
1285
|
+
Yield all string literals anywhere in this block.
|
|
1286
|
+
|
|
1287
|
+
The string literals have ``startpos`` attributes attached.
|
|
1288
|
+
|
|
1289
|
+
>>> block = PythonBlock("'a' + ('b' + \n'c')")
|
|
1290
|
+
>>> [(f.s, f.startpos) for f in block.string_literals()]
|
|
1291
|
+
[('a', FilePos(1,1)), ('b', FilePos(1,8)), ('c', FilePos(2,1))]
|
|
1292
|
+
|
|
1293
|
+
:return:
|
|
1294
|
+
Iterable of ``ast.Str`` or ``ast.Bytes`` nodes
|
|
1295
|
+
"""
|
|
1296
|
+
for node in _walk_ast_nodes_in_order(self.annotated_ast_node):
|
|
1297
|
+
if _is_ast_str_or_byte(node):
|
|
1298
|
+
assert hasattr(node, 'startpos')
|
|
1299
|
+
yield node
|
|
1300
|
+
|
|
1301
|
+
def _get_docstring_nodes(self):
|
|
1302
|
+
"""
|
|
1303
|
+
Yield docstring AST nodes.
|
|
1304
|
+
|
|
1305
|
+
We consider the following to be docstrings::
|
|
1306
|
+
|
|
1307
|
+
- First literal string of function definitions, class definitions,
|
|
1308
|
+
and modules (the python standard)
|
|
1309
|
+
- Literal strings after assignments, per Epydoc
|
|
1310
|
+
|
|
1311
|
+
:rtype:
|
|
1312
|
+
Generator of ``ast.Str`` nodes
|
|
1313
|
+
"""
|
|
1314
|
+
# This is similar to ``ast.get_docstring``, but:
|
|
1315
|
+
# - This function is recursive
|
|
1316
|
+
# - This function yields the node object, rather than the string
|
|
1317
|
+
# - This function yields multiple docstrings (even per ast node)
|
|
1318
|
+
# - This function doesn't raise TypeError on other AST types
|
|
1319
|
+
# - This function doesn't cleandoc
|
|
1320
|
+
# A previous implementation did
|
|
1321
|
+
# [n for n in self.string_literals()
|
|
1322
|
+
# if _ast_node_is_in_docstring_position(n)]
|
|
1323
|
+
# However, the method we now use is more straightforward, and doesn't
|
|
1324
|
+
# require first annotating each node with context information.
|
|
1325
|
+
docstring_containers = (ast.FunctionDef, ast.ClassDef, ast.Module, AsyncFunctionDef)
|
|
1326
|
+
for node in _walk_ast_nodes_in_order(self.annotated_ast_node):
|
|
1327
|
+
if not isinstance(node, docstring_containers):
|
|
1328
|
+
continue
|
|
1329
|
+
if not node.body:
|
|
1330
|
+
continue
|
|
1331
|
+
# If the first body item is a literal string, then yield the node.
|
|
1332
|
+
if (isinstance(node.body[0], ast.Expr) and
|
|
1333
|
+
_is_ast_str(node.body[0].value)):
|
|
1334
|
+
yield node.body[0].value
|
|
1335
|
+
for i in range(1, len(node.body)-1):
|
|
1336
|
+
# If a body item is an assignment and the next one is a
|
|
1337
|
+
# literal string, then yield the node for the literal string.
|
|
1338
|
+
n1, n2 = node.body[i], node.body[i+1]
|
|
1339
|
+
if (isinstance(n1, ast.Assign) and
|
|
1340
|
+
isinstance(n2, ast.Expr) and
|
|
1341
|
+
_is_ast_str(n2.value)):
|
|
1342
|
+
yield n2.value
|
|
1343
|
+
|
|
1344
|
+
def get_doctests(self):
|
|
1345
|
+
r"""
|
|
1346
|
+
Return doctests in this code.
|
|
1347
|
+
|
|
1348
|
+
>>> PythonBlock("x\n'''\n >>> foo(bar\n ... + baz)\n'''\n").get_doctests()
|
|
1349
|
+
[PythonBlock('foo(bar\n + baz)\n', startpos=(3,2))]
|
|
1350
|
+
|
|
1351
|
+
:rtype:
|
|
1352
|
+
``list`` of `PythonStatement` s
|
|
1353
|
+
"""
|
|
1354
|
+
parser = IgnoreOptionsDocTestParser()
|
|
1355
|
+
doctest_blocks = []
|
|
1356
|
+
filename = self.filename
|
|
1357
|
+
flags = self.flags
|
|
1358
|
+
for ast_node in self._get_docstring_nodes():
|
|
1359
|
+
try:
|
|
1360
|
+
if sys.version_info >= (3, 10):
|
|
1361
|
+
examples = parser.get_examples(ast_node.value)
|
|
1362
|
+
else:
|
|
1363
|
+
examples = parser.get_examples(ast_node.s)
|
|
1364
|
+
except Exception:
|
|
1365
|
+
if sys.version_info >= (3, 10):
|
|
1366
|
+
blob = ast_node.s
|
|
1367
|
+
else:
|
|
1368
|
+
blob = ast_node.value
|
|
1369
|
+
if len(blob) > 60:
|
|
1370
|
+
blob = blob[:60] + '...'
|
|
1371
|
+
# TODO: let caller decide how to handle
|
|
1372
|
+
logger.warning("Can't parse docstring; ignoring: %r", blob)
|
|
1373
|
+
continue
|
|
1374
|
+
for example in examples:
|
|
1375
|
+
lineno = ast_node.startpos.lineno + example.lineno
|
|
1376
|
+
colno = ast_node.startpos.colno + example.indent # dubious
|
|
1377
|
+
text = FileText(example.source, filename=filename,
|
|
1378
|
+
startpos=(lineno,colno))
|
|
1379
|
+
try:
|
|
1380
|
+
block = PythonBlock(text, flags=flags)
|
|
1381
|
+
block.ast_node # make sure we can parse
|
|
1382
|
+
except Exception:
|
|
1383
|
+
blob = text.joined
|
|
1384
|
+
if len(blob) > 60:
|
|
1385
|
+
blob = blob[:60] + '...'
|
|
1386
|
+
logger.warning("Can't parse doctest; ignoring: %r", blob)
|
|
1387
|
+
continue
|
|
1388
|
+
doctest_blocks.append(block)
|
|
1389
|
+
return doctest_blocks
|
|
1390
|
+
|
|
1391
|
+
def __repr__(self):
|
|
1392
|
+
r = "%s(%r" % (type(self).__name__, self.text.joined)
|
|
1393
|
+
if self.filename:
|
|
1394
|
+
r += ", filename=%r" % (str(self.filename),)
|
|
1395
|
+
if self.startpos != FilePos():
|
|
1396
|
+
r += ", startpos=%s" % (self.startpos,)
|
|
1397
|
+
if self.flags != self.source_flags:
|
|
1398
|
+
r += ", flags=%s" % (self.flags,)
|
|
1399
|
+
r += ")"
|
|
1400
|
+
return r
|
|
1401
|
+
|
|
1402
|
+
def __str__(self):
|
|
1403
|
+
return str(self.text)
|
|
1404
|
+
|
|
1405
|
+
def __text__(self):
|
|
1406
|
+
return self.text
|
|
1407
|
+
|
|
1408
|
+
def __eq__(self, other):
|
|
1409
|
+
if self is other:
|
|
1410
|
+
return True
|
|
1411
|
+
if not isinstance(other, PythonBlock):
|
|
1412
|
+
return NotImplemented
|
|
1413
|
+
return self.text == other.text and self.flags == other.flags
|
|
1414
|
+
|
|
1415
|
+
def __ne__(self, other):
|
|
1416
|
+
return not (self == other)
|
|
1417
|
+
|
|
1418
|
+
# The rest are defined by total_ordering
|
|
1419
|
+
def __lt__(self, other):
|
|
1420
|
+
if not isinstance(other, PythonBlock):
|
|
1421
|
+
return NotImplemented
|
|
1422
|
+
return (self.text, self.flags) < (other.text, other.flags)
|
|
1423
|
+
|
|
1424
|
+
def __cmp__(self, other):
|
|
1425
|
+
if self is other:
|
|
1426
|
+
return 0
|
|
1427
|
+
if not isinstance(other, PythonBlock):
|
|
1428
|
+
return NotImplemented
|
|
1429
|
+
return cmp(self.text, other.text) or cmp(self.flags, other.flags)
|
|
1430
|
+
|
|
1431
|
+
def __hash__(self):
|
|
1432
|
+
h = hash((self.text, self.flags))
|
|
1433
|
+
self.__hash__ = lambda: h
|
|
1434
|
+
return h
|
|
1435
|
+
|
|
1436
|
+
class IgnoreOptionsDocTestParser(DocTestParser):
|
|
1437
|
+
def _find_options(self, source, name, lineno):
|
|
1438
|
+
# Ignore doctest options. We don't use them, and we don't want to
|
|
1439
|
+
# error on unknown options, which is what the default DocTestParser
|
|
1440
|
+
# does.
|
|
1441
|
+
return {}
|