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.

Files changed (54) hide show
  1. pyflyby/__init__.py +56 -0
  2. pyflyby/__main__.py +9 -0
  3. pyflyby/_autoimp.py +2114 -0
  4. pyflyby/_cmdline.py +531 -0
  5. pyflyby/_comms.py +221 -0
  6. pyflyby/_dbg.py +1339 -0
  7. pyflyby/_docxref.py +379 -0
  8. pyflyby/_file.py +738 -0
  9. pyflyby/_flags.py +230 -0
  10. pyflyby/_format.py +182 -0
  11. pyflyby/_idents.py +233 -0
  12. pyflyby/_import_sorting.py +165 -0
  13. pyflyby/_importclns.py +642 -0
  14. pyflyby/_importdb.py +588 -0
  15. pyflyby/_imports2s.py +639 -0
  16. pyflyby/_importstmt.py +662 -0
  17. pyflyby/_interactive.py +2605 -0
  18. pyflyby/_livepatch.py +793 -0
  19. pyflyby/_log.py +199 -0
  20. pyflyby/_modules.py +515 -0
  21. pyflyby/_parse.py +1441 -0
  22. pyflyby/_py.py +2078 -0
  23. pyflyby/_util.py +459 -0
  24. pyflyby/_version.py +7 -0
  25. pyflyby/autoimport.py +20 -0
  26. pyflyby/importdb.py +19 -0
  27. pyflyby-1.9.4.data/data/etc/pyflyby/canonical.py +10 -0
  28. pyflyby-1.9.4.data/data/etc/pyflyby/common.py +27 -0
  29. pyflyby-1.9.4.data/data/etc/pyflyby/forget.py +10 -0
  30. pyflyby-1.9.4.data/data/etc/pyflyby/mandatory.py +10 -0
  31. pyflyby-1.9.4.data/data/etc/pyflyby/numpy.py +156 -0
  32. pyflyby-1.9.4.data/data/etc/pyflyby/std.py +335 -0
  33. pyflyby-1.9.4.data/data/libexec/pyflyby/colordiff +34 -0
  34. pyflyby-1.9.4.data/data/libexec/pyflyby/diff-colorize +148 -0
  35. pyflyby-1.9.4.data/data/share/doc/pyflyby/LICENSE.txt +23 -0
  36. pyflyby-1.9.4.data/data/share/doc/pyflyby/TODO.txt +115 -0
  37. pyflyby-1.9.4.data/data/share/doc/pyflyby/testing.txt +13 -0
  38. pyflyby-1.9.4.data/data/share/emacs/site-lisp/pyflyby.el +108 -0
  39. pyflyby-1.9.4.data/scripts/collect-exports +76 -0
  40. pyflyby-1.9.4.data/scripts/collect-imports +58 -0
  41. pyflyby-1.9.4.data/scripts/find-import +38 -0
  42. pyflyby-1.9.4.data/scripts/list-bad-xrefs +34 -0
  43. pyflyby-1.9.4.data/scripts/prune-broken-imports +34 -0
  44. pyflyby-1.9.4.data/scripts/pyflyby-diff +34 -0
  45. pyflyby-1.9.4.data/scripts/reformat-imports +27 -0
  46. pyflyby-1.9.4.data/scripts/replace-star-imports +37 -0
  47. pyflyby-1.9.4.data/scripts/tidy-imports +191 -0
  48. pyflyby-1.9.4.data/scripts/transform-imports +47 -0
  49. pyflyby-1.9.4.dist-info/LICENSE.txt +23 -0
  50. pyflyby-1.9.4.dist-info/METADATA +507 -0
  51. pyflyby-1.9.4.dist-info/RECORD +54 -0
  52. pyflyby-1.9.4.dist-info/WHEEL +5 -0
  53. pyflyby-1.9.4.dist-info/entry_points.txt +3 -0
  54. 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 {}