pyflyby 1.10.1__cp311-cp311-manylinux_2_24_x86_64.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.

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