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