pine2pyne 0.2.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.
Files changed (75) hide show
  1. pine2pyne/__init__.py +84 -0
  2. pine2pyne/__main__.py +12 -0
  3. pine2pyne/ast_nodes.py +405 -0
  4. pine2pyne/ast_walk.py +93 -0
  5. pine2pyne/block_value_lowering.py +694 -0
  6. pine2pyne/bool_builtins.py +42 -0
  7. pine2pyne/bool_value_typing.py +580 -0
  8. pine2pyne/cli.py +247 -0
  9. pine2pyne/codegen.py +886 -0
  10. pine2pyne/compat/__init__.py +19 -0
  11. pine2pyne/compat/__main__.py +6 -0
  12. pine2pyne/compat/cli.py +114 -0
  13. pine2pyne/compat/comparison.py +105 -0
  14. pine2pyne/compat/corpus.py +73 -0
  15. pine2pyne/compat/report.py +95 -0
  16. pine2pyne/compat/runner.py +63 -0
  17. pine2pyne/condition_typing.py +260 -0
  18. pine2pyne/conditional_history_calls.py +96 -0
  19. pine2pyne/core/__init__.py +26 -0
  20. pine2pyne/core/contracts.py +125 -0
  21. pine2pyne/di.py +173 -0
  22. pine2pyne/emit/__init__.py +5 -0
  23. pine2pyne/emit/legacy.py +14 -0
  24. pine2pyne/emit/pynecomp/__init__.py +20 -0
  25. pine2pyne/emit/pynecomp/banner.py +30 -0
  26. pine2pyne/emit/pynecomp/blank_lines.py +69 -0
  27. pine2pyne/emit/pynecomp/emitter.py +61 -0
  28. pine2pyne/emit/pynecomp/imports.py +65 -0
  29. pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
  30. pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
  31. pine2pyne/emit/pynecomp/passes/base.py +69 -0
  32. pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
  33. pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
  34. pine2pyne/emit/pynecomp/passes/imports.py +112 -0
  35. pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
  36. pine2pyne/emit/pynecomp/passes/library.py +205 -0
  37. pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
  38. pine2pyne/emit/pynecomp/printer.py +288 -0
  39. pine2pyne/emit/pynecomp/unparser.py +186 -0
  40. pine2pyne/emitted_names.py +152 -0
  41. pine2pyne/errors.py +69 -0
  42. pine2pyne/for_loop_end_bound.py +172 -0
  43. pine2pyne/grouped_history_lowering.py +310 -0
  44. pine2pyne/import_resolver.py +469 -0
  45. pine2pyne/input_hoisting.py +195 -0
  46. pine2pyne/legacy_function_names.py +74 -0
  47. pine2pyne/legacy_sessions.py +68 -0
  48. pine2pyne/lexer.py +630 -0
  49. pine2pyne/library_collision.py +434 -0
  50. pine2pyne/namespace_members.py +247 -0
  51. pine2pyne/node_ids.py +235 -0
  52. pine2pyne/once_lowering.py +75 -0
  53. pine2pyne/overload_resolution.py +475 -0
  54. pine2pyne/parse_repair.py +398 -0
  55. pine2pyne/parser.py +1873 -0
  56. pine2pyne/pine_builtins.py +394 -0
  57. pine2pyne/pine_semantics.py +604 -0
  58. pine2pyne/pipeline/__init__.py +18 -0
  59. pine2pyne/pipeline/service.py +70 -0
  60. pine2pyne/pipeline/stages.py +73 -0
  61. pine2pyne/scope_renamer.py +965 -0
  62. pine2pyne/strategy_when_lowering.py +101 -0
  63. pine2pyne/strict_boolean.py +101 -0
  64. pine2pyne/symbol_table.py +166 -0
  65. pine2pyne/tokens.py +143 -0
  66. pine2pyne/transformer.py +2510 -0
  67. pine2pyne/type_inference.py +422 -0
  68. pine2pyne/udt_field_defaults.py +149 -0
  69. pine2pyne/value_blocks.py +357 -0
  70. pine2pyne-0.2.4.dist-info/METADATA +99 -0
  71. pine2pyne-0.2.4.dist-info/RECORD +75 -0
  72. pine2pyne-0.2.4.dist-info/WHEEL +5 -0
  73. pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
  74. pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
  75. pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
pine2pyne/__init__.py ADDED
@@ -0,0 +1,84 @@
1
+ """
2
+ pine2pyne: Pine Script v6 to PyneCore Python transpiler.
3
+
4
+ A tool to convert TradingView Pine Script indicators and strategies
5
+ to PyneCore Python code for local backtesting and analysis.
6
+ """
7
+
8
+ __version__ = '0.2.4'
9
+ __author__ = 'Pine2Pyne Transpiler'
10
+
11
+ from .lexer import Lexer, tokenize
12
+ from .parser import Parser, parse
13
+ from .parse_repair import parse_source
14
+ from .legacy_function_names import qualify_legacy_function_names
15
+ from .pine_semantics import enforce as enforce_pine_semantics
16
+ from .transformer import Transformer, PyneTransformedScript
17
+ from .codegen import CodeGenerator, generate_code
18
+ from .errors import (
19
+ Pine2PyneError,
20
+ LexerError,
21
+ ParserError,
22
+ TransformerError,
23
+ CodeGenError,
24
+ SemanticError,
25
+ SemanticWarning,
26
+ UnsupportedFeatureError,
27
+ )
28
+
29
+
30
+ from .core.contracts import OutputProfile, Transpiler
31
+ from .pipeline.stages import CommentOnlySourceFilter
32
+
33
+
34
+ def _is_comment_only(source: str) -> bool:
35
+ """Check if source contains only comments and whitespace."""
36
+ return CommentOnlySourceFilter().is_blank(source)
37
+
38
+
39
+ def transpile(source: str, profile: 'OutputProfile | str | None' = None) -> str:
40
+ """
41
+ Transpile Pine Script source code to PyneCore Python.
42
+
43
+ The pipeline (parse -> normalize -> semantic guard -> transform -> emit) is
44
+ assembled by the dishka container in :mod:`pine2pyne.di`; ``profile`` picks
45
+ the output layout:
46
+
47
+ * ``'pynecomp'`` (default) - byte-compatible with the closed PyneComp v6.0.79 compiler;
48
+ * ``'legacy'`` - the historical pine2pyne layout, kept as a reference.
49
+
50
+ Args:
51
+ source: Pine Script v6 source code
52
+ profile: output profile (an :class:`OutputProfile` or its name)
53
+
54
+ Returns:
55
+ PyneCore-compatible Python source code (empty string if source is comment-only)
56
+
57
+ Raises:
58
+ Pine2PyneError: If any stage of transpilation fails
59
+ """
60
+ from .di import get_transpiler
61
+ return get_transpiler(profile).transpile(source)
62
+
63
+
64
+ __all__ = [
65
+ 'transpile',
66
+ 'OutputProfile',
67
+ 'Transpiler',
68
+ 'Lexer',
69
+ 'Parser',
70
+ 'Transformer',
71
+ 'CodeGenerator',
72
+ 'tokenize',
73
+ 'parse',
74
+ 'generate_code',
75
+ 'Pine2PyneError',
76
+ 'LexerError',
77
+ 'ParserError',
78
+ 'TransformerError',
79
+ 'CodeGenError',
80
+ 'SemanticError',
81
+ 'SemanticWarning',
82
+ 'UnsupportedFeatureError',
83
+ 'PyneTransformedScript',
84
+ ]
pine2pyne/__main__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ Entry point for running pine2pyne as a module.
3
+
4
+ Usage:
5
+ python -m pine2pyne input.pine
6
+ python -m pine2pyne input.pine -o output.py
7
+ """
8
+ import sys
9
+ from .cli import main
10
+
11
+ if __name__ == '__main__':
12
+ sys.exit(main())
pine2pyne/ast_nodes.py ADDED
@@ -0,0 +1,405 @@
1
+ """
2
+ AST (Abstract Syntax Tree) node definitions for Pine Script v6.
3
+
4
+ All node types used by the parser to represent Pine Script syntax.
5
+ """
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, List, Optional, Union
8
+
9
+
10
+ # Base class for all AST nodes
11
+ @dataclass
12
+ class ASTNode:
13
+ """Base class for all AST nodes."""
14
+ line: int = 0
15
+ column: int = 0
16
+ # the Pine line a statement built by a lowering is laid out at, where `line` stays unset
17
+ source_line: int = field(default=0, compare=False, repr=False)
18
+
19
+
20
+ # ============================================================================
21
+ # Top-level nodes
22
+ # ============================================================================
23
+
24
+ @dataclass
25
+ class Script(ASTNode):
26
+ """Root node representing the entire Pine Script."""
27
+ version: Optional['VersionAnnotation'] = None
28
+ script_decl: Optional[Union['IndicatorDecl', 'StrategyDecl', 'LibraryDecl']] = None
29
+ imports: List['ImportDecl'] = field(default_factory=list)
30
+ declarations: List[Union['VarDecl', 'VaripDecl', 'TypeDecl', 'EnumDecl', 'FuncDecl', 'InputDecl']] = field(default_factory=list)
31
+ body: List['Statement'] = field(default_factory=list)
32
+
33
+
34
+ @dataclass
35
+ class VersionAnnotation(ASTNode):
36
+ """//@version=6 annotation."""
37
+ version: str = "" # "6" or "5"
38
+
39
+
40
+ @dataclass
41
+ class IndicatorDecl(ASTNode):
42
+ """indicator("Title", ...) declaration."""
43
+ title: Optional[str] = "" # None: the title is an expression, kept in kwargs['title']
44
+ kwargs: dict = field(default_factory=dict)
45
+ # How the source wrote the call: how many arguments were positional, and the
46
+ # title's quotes. Codegen ignores it; a layout printer may reproduce it
47
+ positional_count: int = 0
48
+ title_double_quoted: bool = False
49
+ keyword_order: tuple = () # the keyword arguments' names as the source orders them
50
+
51
+
52
+ @dataclass
53
+ class StrategyDecl(ASTNode):
54
+ """strategy("Title", ...) declaration."""
55
+ title: Optional[str] = "" # None: the title is an expression, kept in kwargs['title']
56
+ kwargs: dict = field(default_factory=dict)
57
+ # How the source wrote the call: how many arguments were positional, and the
58
+ # title's quotes. Codegen ignores it; a layout printer may reproduce it
59
+ positional_count: int = 0
60
+ title_double_quoted: bool = False
61
+ keyword_order: tuple = () # the keyword arguments' names as the source orders them
62
+
63
+
64
+ @dataclass
65
+ class LibraryDecl(ASTNode):
66
+ """library("Title", ...) declaration."""
67
+ title: Optional[str] = "" # None: the title is an expression, kept in kwargs['title']
68
+ kwargs: dict = field(default_factory=dict)
69
+ # How the source wrote the call: how many arguments were positional, and the
70
+ # title's quotes. Codegen ignores it; a layout printer may reproduce it
71
+ positional_count: int = 0
72
+ title_double_quoted: bool = False
73
+ keyword_order: tuple = () # the keyword arguments' names as the source orders them
74
+
75
+
76
+ @dataclass
77
+ class ImportDecl(ASTNode):
78
+ """import user/library/version as alias"""
79
+ user: str = ""
80
+ library: str = ""
81
+ version: Optional[str] = None
82
+ alias: Optional[str] = None
83
+
84
+
85
+ # ============================================================================
86
+ # Declarations
87
+ # ============================================================================
88
+
89
+ @dataclass
90
+ class VarDecl(ASTNode):
91
+ """var [type] name = value"""
92
+ name: str = ""
93
+ value: 'Expression' = None
94
+ type_hint: Optional[str] = None
95
+ is_var: bool = True # True for 'var', False for 'varip'
96
+
97
+
98
+ @dataclass
99
+ class VaripDecl(ASTNode):
100
+ """varip [type] name = value"""
101
+ name: str = ""
102
+ value: 'Expression' = None
103
+ type_hint: Optional[str] = None
104
+
105
+
106
+ @dataclass
107
+ class TypeDecl(ASTNode):
108
+ """type MyType ... (User-defined type)"""
109
+ name: str = ""
110
+ fields: List[tuple] = field(default_factory=list) # [(field_name, type_hint, default_value), ...] or [(field_name, type_hint), ...]
111
+ field_lines: List[int] = field(default_factory=list) # each field's Pine line
112
+
113
+
114
+ @dataclass
115
+ class EnumDecl(ASTNode):
116
+ """enum MyEnum ... (Enum definition)"""
117
+ name: str = ""
118
+ members: List = field(default_factory=list) # List[str] or List[tuple[str, Expression]]
119
+
120
+
121
+ @dataclass
122
+ class FuncDecl(ASTNode):
123
+ """Function declaration: name(params) => body"""
124
+ name: str = ""
125
+ params: List['Parameter'] = field(default_factory=list)
126
+ body: Union['Expression', List['Statement']] = None
127
+ return_type: Optional[str] = None
128
+ is_method: bool = False
129
+ is_export: bool = False
130
+ is_overload: bool = False # One of several same-name declarations (card #39)
131
+ indexed_params: set = field(default_factory=set) # Params that use [n] history access
132
+ value_is_bool: bool = False # Pine v6: its value is of type bool (card #56)
133
+
134
+
135
+ @dataclass
136
+ class Parameter(ASTNode):
137
+ """Function parameter: [type] name [= default]"""
138
+ name: str = ""
139
+ type_hint: Optional[str] = None
140
+ default: Optional['Expression'] = None
141
+ is_series: bool = False # declared `series T`: PyneComp annotates it Series[T]
142
+
143
+
144
+ @dataclass
145
+ class InputDecl(ASTNode):
146
+ """Input declaration: name = input.*(...)"""
147
+ name: str = ""
148
+ func: str = "" # e.g., "input.int"
149
+ args: List['Expression'] = field(default_factory=list)
150
+ kwargs: dict = field(default_factory=dict)
151
+
152
+
153
+ # ============================================================================
154
+ # Expressions
155
+ # ============================================================================
156
+
157
+ @dataclass
158
+ class Expression(ASTNode):
159
+ """Base class for all expressions."""
160
+ pass
161
+
162
+
163
+ @dataclass
164
+ class BinaryOp(Expression):
165
+ """Binary operation: left op right"""
166
+ left: Expression = None
167
+ op: str = ""
168
+ right: Expression = None
169
+ compound: bool = False # written `x op= y`: printed that way, `x := x op y` is not
170
+
171
+
172
+ @dataclass
173
+ class UnaryOp(Expression):
174
+ """Unary operation: op operand"""
175
+ op: str = ""
176
+ operand: Expression = None
177
+
178
+
179
+ @dataclass
180
+ class TernaryOp(Expression):
181
+ """Ternary operation: condition ? true_expr : false_expr"""
182
+ condition: Expression = None
183
+ true_expr: Expression = None
184
+ false_expr: Expression = None
185
+
186
+
187
+ @dataclass
188
+ class FunctionCall(Expression):
189
+ """Function call: func(args, kwargs)"""
190
+ func: Union[str, 'MemberAccess'] = "" # Can be "ta.sma" or MemberAccess node
191
+ args: List[Expression] = field(default_factory=list)
192
+ kwargs: dict = field(default_factory=dict)
193
+
194
+
195
+ @dataclass
196
+ class MethodCall(Expression):
197
+ """Method call: object.method(args)"""
198
+ object: Expression = None
199
+ method: str = ""
200
+ args: List[Expression] = field(default_factory=list)
201
+ kwargs: dict = field(default_factory=dict)
202
+
203
+
204
+ @dataclass
205
+ class MemberAccess(Expression):
206
+ """Member access: object.member"""
207
+ object: Union[str, Expression] = "" # Can be identifier string or expression
208
+ member: str = ""
209
+
210
+
211
+ @dataclass
212
+ class IndexAccess(Expression):
213
+ """Index access: object[index]"""
214
+ object: Expression = None
215
+ index: Expression = None
216
+
217
+
218
+ @dataclass
219
+ class ArrayLiteral(Expression):
220
+ """Array literal: [elem1, elem2, ...]"""
221
+ elements: List[Expression] = field(default_factory=list)
222
+
223
+
224
+ @dataclass
225
+ class TupleDestructure(Expression):
226
+ """Tuple destructuring: [a, b, c] = expr"""
227
+ names: List[str] = field(default_factory=list)
228
+ value: Expression = None
229
+
230
+
231
+ @dataclass
232
+ class Identifier(Expression):
233
+ """Identifier (variable name)"""
234
+ name: str = ""
235
+
236
+
237
+ @dataclass
238
+ class NamedExpression(Expression):
239
+ """Transformer output only: binds `name` to `value` and is that value
240
+ (Python `(name := value)`). A switch subject is bound this way in the
241
+ first case test, so it is evaluated once, where Pine evaluates it (card #43)."""
242
+ name: str = ""
243
+ value: Expression = None
244
+
245
+
246
+ @dataclass
247
+ class Literal(Expression):
248
+ """Literal value (int, float, string, bool, color)"""
249
+ value: Any = None
250
+ literal_type: str = "" # 'int', 'float', 'string', 'bool', 'color'
251
+ is_double_quoted: bool = False # Track if original source used double quotes
252
+
253
+
254
+ @dataclass
255
+ class NaLiteral(Expression):
256
+ """The 'na' literal value"""
257
+ pass
258
+
259
+
260
+ @dataclass
261
+ class IfExpression(Expression):
262
+ """If expression (returns a value): if cond\n val1\nelse\n val2"""
263
+ condition: Expression = None
264
+ true_expr: Union[Expression, List['Statement']] = None
265
+ false_expr: Optional[Union[Expression, List['Statement']]] = None
266
+ value_is_bool: bool = False # Pine v6: its value is of type bool (card #56)
267
+ chained: bool = False # written `else if` after the if before it (not an `if` inside an `else` block)
268
+
269
+
270
+ @dataclass
271
+ class SwitchExpression(Expression):
272
+ """Switch expression (returns a value)"""
273
+ expr: Optional[Expression] = None
274
+ cases: List[tuple[Expression, Union[Expression, List['Statement']]]] = field(default_factory=list)
275
+ default: Optional[Union[Expression, List['Statement']]] = None
276
+ value_is_bool: bool = False # Pine v6: its value is of type bool (card #56)
277
+
278
+
279
+ # ============================================================================
280
+ # Statements
281
+ # ============================================================================
282
+
283
+ @dataclass
284
+ class Statement(ASTNode):
285
+ """Base class for all statements."""
286
+ pass
287
+
288
+
289
+ @dataclass
290
+ class Assignment(Statement):
291
+ """Assignment statement: target = value"""
292
+ target: Union[str, 'TupleDestructure'] = ""
293
+ value: Expression = None
294
+ type_hint: Optional[str] = None
295
+ is_const: bool = False # declared `const T name = ...`: PyneComp moves a top-level one to the module
296
+
297
+
298
+ @dataclass
299
+ class Reassignment(Statement):
300
+ """Reassignment statement: target := value"""
301
+ target: str = ""
302
+ value: Expression = None
303
+
304
+
305
+ @dataclass
306
+ class IfStatement(Statement):
307
+ """If statement: if cond\n body\nelse if ...\nelse ..."""
308
+ condition: Expression = None
309
+ body: List[Statement] = field(default_factory=list)
310
+ elseifs: List[tuple[Expression, List[Statement]]] = field(default_factory=list)
311
+ else_body: Optional[List[Statement]] = None
312
+
313
+
314
+ @dataclass
315
+ class ForLoop(Statement):
316
+ """For loop: for var = from to to_val [by step]"""
317
+ var: str = ""
318
+ from_val: Expression = None
319
+ to_val: Expression = None
320
+ step: Optional[Expression] = None
321
+ body: List[Statement] = field(default_factory=list)
322
+ value_is_bool: bool = False # Pine v6: its value is of type bool (card #56)
323
+ # Pine v6, an end bound that can change between passes: the name of the runtime
324
+ # counter (`pine_loop`) that re-reads it; empty for a fixed `pine_range` (card #82)
325
+ counter_name: str = ""
326
+ counter_helper: str = "" # the name `pine_loop` is imported under (an alias when the script uses the name)
327
+ range_helper: str = "" # the name `pine_range` is imported under for a fixed loop; empty is `pine_range` (card #94)
328
+ # the value of an assignment (`x = for ...`): PyneComp writes no default step for it, even in Pine v4/v5
329
+ is_assigned_value: bool = False
330
+
331
+
332
+ @dataclass
333
+ class ForInLoop(Statement):
334
+ """For-in loop: for [vars] in iterable"""
335
+ vars: List[str] = field(default_factory=list)
336
+ iterable: Expression = None
337
+ body: List[Statement] = field(default_factory=list)
338
+ value_is_bool: bool = False # Pine v6: its value is of type bool (card #56)
339
+
340
+
341
+ @dataclass
342
+ class WhileLoop(Statement):
343
+ """While loop: while condition\n body"""
344
+ condition: Expression = None
345
+ body: List[Statement] = field(default_factory=list)
346
+ value_is_bool: bool = False # Pine v6: its value is of type bool (card #56)
347
+
348
+
349
+ @dataclass
350
+ class SwitchStatement(Statement):
351
+ """Switch statement (imperative form)"""
352
+ expr: Optional[Expression] = None
353
+ cases: List[tuple[Expression, List[Statement]]] = field(default_factory=list)
354
+ default: Optional[List[Statement]] = None
355
+
356
+
357
+ @dataclass
358
+ class OnceStatement(Statement):
359
+ """Pine v6 once structure: once [cond]\n body. The parser lowers it to
360
+ a `var` flag and an `if` (once_lowering.py, card #59); no later stage sees it."""
361
+ condition: Optional[Expression] = None
362
+ body: List[Statement] = field(default_factory=list)
363
+
364
+
365
+ @dataclass
366
+ class BreakStatement(Statement):
367
+ """Break statement"""
368
+ pass
369
+
370
+
371
+ @dataclass
372
+ class ContinueStatement(Statement):
373
+ """Continue statement"""
374
+ pass
375
+
376
+
377
+ @dataclass
378
+ class ExpressionStatement(Statement):
379
+ """Expression as statement (e.g., function call on its own line)"""
380
+ expr: Expression = None
381
+
382
+
383
+ @dataclass
384
+ class ReturnStatement(Statement):
385
+ """Explicit return statement (for codegen)"""
386
+ expr: Optional[Expression] = None
387
+
388
+
389
+ @dataclass
390
+ class RawCode(Statement):
391
+ """Raw Python code to be emitted as-is by codegen."""
392
+ code: str = ""
393
+
394
+
395
+ # ============================================================================
396
+ # Type annotations
397
+ # ============================================================================
398
+
399
+ # Forward references for type checking
400
+ Statement = Union[
401
+ Assignment, Reassignment, IfStatement, ForLoop, ForInLoop,
402
+ WhileLoop, SwitchStatement, BreakStatement, ContinueStatement,
403
+ ExpressionStatement, ReturnStatement, VarDecl, VaripDecl,
404
+ FuncDecl, InputDecl
405
+ ]
pine2pyne/ast_walk.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ AST walk: the child traversal shared by the Pine-AST pre-passes and scans.
3
+
4
+ A Pine AST node is a dataclass; its children sit in its fields, directly or
5
+ inside lists, tuples (the arms of an else-if, the cases of a switch) and dicts
6
+ (the keyword arguments of a call). A walk that forgets one of these containers
7
+ silently misses what is inside it: a history index in a keyword argument or in
8
+ an else-if arm was not seen (cards #32, #44). Every generic walk goes through
9
+ `walk` (to find nodes) or `map_children` (to rebuild them), so the containers
10
+ are decided in one place.
11
+
12
+ Every transpile walks every file, so `walk` is iterative (a deep expression
13
+ does not use up the recursion limit) and caches the child fields per node type.
14
+ """
15
+ import dataclasses
16
+ from typing import Callable, Dict, Iterator, Tuple, Union
17
+
18
+ # The source position is not a child
19
+ POSITION_FIELDS = frozenset({'line', 'column'})
20
+ # Field values that are never a node nor hold one
21
+ LEAF_TYPES = frozenset({str, int, float, bool, type(None)})
22
+ CONTAINER_TYPES = frozenset({list, tuple})
23
+
24
+ # node type -> its child field names, last first; False for a type that is not a node
25
+ _CHILD_FIELDS_LAST_FIRST: Dict[type, Union[Tuple[str, ...], bool]] = {}
26
+ # node type -> its child field names in declaration order
27
+ _CHILD_FIELDS_IN_ORDER: Dict[type, Tuple[str, ...]] = {}
28
+
29
+
30
+ def child_field_names(node_type: type) -> Tuple[str, ...]:
31
+ """The fields of a node type that can hold children, in declaration order."""
32
+ fields = _CHILD_FIELDS_IN_ORDER.get(node_type)
33
+ if fields is None:
34
+ fields = _CHILD_FIELDS_IN_ORDER[node_type] = tuple(reversed(_child_fields_last_first(node_type) or ()))
35
+ return fields
36
+
37
+
38
+ def _child_fields_last_first(value_type: type):
39
+ fields = _CHILD_FIELDS_LAST_FIRST.get(value_type)
40
+ if fields is None:
41
+ is_node_type = hasattr(value_type, '__dataclass_fields__')
42
+ fields = tuple(reversed([field.name for field in dataclasses.fields(value_type)
43
+ if field.name not in POSITION_FIELDS])) if is_node_type else False
44
+ _CHILD_FIELDS_LAST_FIRST[value_type] = fields
45
+ return fields
46
+
47
+
48
+ def is_node(value) -> bool:
49
+ """True for an AST node instance (a dataclass instance, not a class)."""
50
+ return _child_fields_last_first(type(value)) is not False and not isinstance(value, type)
51
+
52
+
53
+ def walk(tree) -> Iterator:
54
+ """Every node in `tree` (a node, or lists, tuples and dicts of them),
55
+ each parent before its children, in source order."""
56
+ pending = [tree]
57
+ while pending:
58
+ value = pending.pop()
59
+ value_type = type(value)
60
+ if value_type in CONTAINER_TYPES:
61
+ pending.extend(item for item in reversed(value) if type(item) not in LEAF_TYPES)
62
+ elif value_type is dict:
63
+ pending.extend(item for item in reversed(list(value.values())) if type(item) not in LEAF_TYPES)
64
+ elif value_type not in LEAF_TYPES:
65
+ fields = _child_fields_last_first(value_type)
66
+ if fields is False:
67
+ continue
68
+ yield value
69
+ for name in fields:
70
+ child = getattr(value, name)
71
+ if type(child) not in LEAF_TYPES:
72
+ pending.append(child)
73
+
74
+
75
+ def contains(tree, predicate: Callable) -> bool:
76
+ """True when a node in `tree` satisfies `predicate`."""
77
+ for node in walk(tree):
78
+ if predicate(node):
79
+ return True
80
+ return False
81
+
82
+
83
+ def map_children(value, rebuild: Callable):
84
+ """`value` with `rebuild` applied to each node it holds directly: a list,
85
+ tuple or dict is copied around the rebuilt items, anything else that is
86
+ not a node (a name, a number) is kept."""
87
+ if isinstance(value, list):
88
+ return [map_children(item, rebuild) for item in value]
89
+ if isinstance(value, tuple):
90
+ return tuple(map_children(item, rebuild) for item in value)
91
+ if isinstance(value, dict):
92
+ return {key: map_children(item, rebuild) for key, item in value.items()}
93
+ return rebuild(value) if is_node(value) else value