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.
- pine2pyne/__init__.py +84 -0
- pine2pyne/__main__.py +12 -0
- pine2pyne/ast_nodes.py +405 -0
- pine2pyne/ast_walk.py +93 -0
- pine2pyne/block_value_lowering.py +694 -0
- pine2pyne/bool_builtins.py +42 -0
- pine2pyne/bool_value_typing.py +580 -0
- pine2pyne/cli.py +247 -0
- pine2pyne/codegen.py +886 -0
- pine2pyne/compat/__init__.py +19 -0
- pine2pyne/compat/__main__.py +6 -0
- pine2pyne/compat/cli.py +114 -0
- pine2pyne/compat/comparison.py +105 -0
- pine2pyne/compat/corpus.py +73 -0
- pine2pyne/compat/report.py +95 -0
- pine2pyne/compat/runner.py +63 -0
- pine2pyne/condition_typing.py +260 -0
- pine2pyne/conditional_history_calls.py +96 -0
- pine2pyne/core/__init__.py +26 -0
- pine2pyne/core/contracts.py +125 -0
- pine2pyne/di.py +173 -0
- pine2pyne/emit/__init__.py +5 -0
- pine2pyne/emit/legacy.py +14 -0
- pine2pyne/emit/pynecomp/__init__.py +20 -0
- pine2pyne/emit/pynecomp/banner.py +30 -0
- pine2pyne/emit/pynecomp/blank_lines.py +69 -0
- pine2pyne/emit/pynecomp/emitter.py +61 -0
- pine2pyne/emit/pynecomp/imports.py +65 -0
- pine2pyne/emit/pynecomp/passes/__init__.py +84 -0
- pine2pyne/emit/pynecomp/passes/annotations.py +422 -0
- pine2pyne/emit/pynecomp/passes/base.py +69 -0
- pine2pyne/emit/pynecomp/passes/builtins.py +169 -0
- pine2pyne/emit/pynecomp/passes/hoisting.py +205 -0
- pine2pyne/emit/pynecomp/passes/imports.py +112 -0
- pine2pyne/emit/pynecomp/passes/legacy_colors.py +101 -0
- pine2pyne/emit/pynecomp/passes/library.py +205 -0
- pine2pyne/emit/pynecomp/passes/rewrites.py +215 -0
- pine2pyne/emit/pynecomp/printer.py +288 -0
- pine2pyne/emit/pynecomp/unparser.py +186 -0
- pine2pyne/emitted_names.py +152 -0
- pine2pyne/errors.py +69 -0
- pine2pyne/for_loop_end_bound.py +172 -0
- pine2pyne/grouped_history_lowering.py +310 -0
- pine2pyne/import_resolver.py +469 -0
- pine2pyne/input_hoisting.py +195 -0
- pine2pyne/legacy_function_names.py +74 -0
- pine2pyne/legacy_sessions.py +68 -0
- pine2pyne/lexer.py +630 -0
- pine2pyne/library_collision.py +434 -0
- pine2pyne/namespace_members.py +247 -0
- pine2pyne/node_ids.py +235 -0
- pine2pyne/once_lowering.py +75 -0
- pine2pyne/overload_resolution.py +475 -0
- pine2pyne/parse_repair.py +398 -0
- pine2pyne/parser.py +1873 -0
- pine2pyne/pine_builtins.py +394 -0
- pine2pyne/pine_semantics.py +604 -0
- pine2pyne/pipeline/__init__.py +18 -0
- pine2pyne/pipeline/service.py +70 -0
- pine2pyne/pipeline/stages.py +73 -0
- pine2pyne/scope_renamer.py +965 -0
- pine2pyne/strategy_when_lowering.py +101 -0
- pine2pyne/strict_boolean.py +101 -0
- pine2pyne/symbol_table.py +166 -0
- pine2pyne/tokens.py +143 -0
- pine2pyne/transformer.py +2510 -0
- pine2pyne/type_inference.py +422 -0
- pine2pyne/udt_field_defaults.py +149 -0
- pine2pyne/value_blocks.py +357 -0
- pine2pyne-0.2.4.dist-info/METADATA +99 -0
- pine2pyne-0.2.4.dist-info/RECORD +75 -0
- pine2pyne-0.2.4.dist-info/WHEEL +5 -0
- pine2pyne-0.2.4.dist-info/entry_points.txt +3 -0
- pine2pyne-0.2.4.dist-info/licenses/LICENSE +201 -0
- pine2pyne-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Type annotations the way PyneComp writes them.
|
|
2
|
+
|
|
3
|
+
* A name is a series (``Series``, ``PersistentSeries``) only when its scope
|
|
4
|
+
reads it with history (``x[1]``); a ``var`` otherwise is ``Persistent``.
|
|
5
|
+
* A declaration (the first assignment of a name in a scope; later ones are
|
|
6
|
+
Pine ``:=``) is annotated when its type is known without inference: the Pine
|
|
7
|
+
source types it (``float x = ...``) or its value says it — a literal, a hex
|
|
8
|
+
colour, ``array.new_float()``, ``array.from_items('a', 'b')``, ``line.all``.
|
|
9
|
+
* A series or persistent annotation keeps its element type (``Series[float]``)
|
|
10
|
+
only then; otherwise it is bare (``Series``).
|
|
11
|
+
|
|
12
|
+
Measured on ``assets/pyne``: 2626 ``str``, 1254 ``int``, 1048 ``float``, 1146
|
|
13
|
+
``bool``, 941 ``Color`` declarations; ``Series`` 2108 bare against 859 typed.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ast
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
from .base import PassContext, function_scopes, own_nodes
|
|
21
|
+
|
|
22
|
+
_LITERAL_TYPES = {bool: 'bool', int: 'int', float: 'float', str: 'str'}
|
|
23
|
+
_ARRAY_ELEMENTS = {
|
|
24
|
+
'new_float': 'float', 'new_int': 'int', 'new_bool': 'bool', 'new_string': 'str', 'new_color': 'Color',
|
|
25
|
+
'new_line': 'Line', 'new_label': 'Label', 'new_box': 'Box', 'new_table': 'Table',
|
|
26
|
+
'new_linefill': 'LineFill', 'new_polyline': 'Polyline',
|
|
27
|
+
}
|
|
28
|
+
_ALL_COLLECTIONS = {'line': 'Line', 'label': 'Label', 'box': 'Box', 'table': 'Table',
|
|
29
|
+
'linefill': 'LineFill', 'polyline': 'Polyline'}
|
|
30
|
+
_SERIES_KINDS = {'Series': None, 'PersistentSeries': 'Persistent', 'IBPersistentSeries': 'IBPersistent'}
|
|
31
|
+
_PERSISTENT_KINDS = ('Series', 'PersistentSeries', 'IBPersistentSeries', 'Persistent', 'IBPersistent')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _dotted(node: ast.AST) -> str:
|
|
35
|
+
if isinstance(node, ast.Attribute):
|
|
36
|
+
return f'{_dotted(node.value)}.{node.attr}'
|
|
37
|
+
return node.id if isinstance(node, ast.Name) else ''
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def literal_type(value: ast.expr | None) -> str | None:
|
|
41
|
+
if isinstance(value, ast.Constant):
|
|
42
|
+
return _LITERAL_TYPES.get(type(value.value))
|
|
43
|
+
if (isinstance(value, ast.UnaryOp) and isinstance(value.op, ast.USub)
|
|
44
|
+
and isinstance(value.operand, ast.Constant) and type(value.operand.value) is int):
|
|
45
|
+
return 'int'
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _typed_na_argument(call: ast.Call) -> str | None:
|
|
50
|
+
"""T of the `NA(T)` value of `array.new(n, NA(T))` / `matrix.new(..., initial_value=NA(T))`."""
|
|
51
|
+
position = 2 if _dotted(call.func) == 'matrix.new' else 1 # `matrix.new(r, c, NA(T))`
|
|
52
|
+
values = list(call.args[position:position + 1]) + [keyword.value for keyword in call.keywords
|
|
53
|
+
if keyword.arg == 'initial_value']
|
|
54
|
+
for value in values:
|
|
55
|
+
if (isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == 'NA'
|
|
56
|
+
and len(value.args) == 1 and isinstance(value.args[0], ast.Name)):
|
|
57
|
+
return value.args[0].id
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def inferable_type(value: ast.expr | None, classes: frozenset = frozenset(),
|
|
62
|
+
methods: frozenset = frozenset()) -> str | None:
|
|
63
|
+
"""The type PyneComp reads off a value without inference, or None. ``classes``:
|
|
64
|
+
the module's UDT classes, whose constructor call names its type; ``methods``: the user
|
|
65
|
+
methods, whose call on a constructor (`T.new().m()`) PyneComp types as `T` (probe mc5)."""
|
|
66
|
+
found = literal_type(value)
|
|
67
|
+
if found:
|
|
68
|
+
return found
|
|
69
|
+
if isinstance(value, ast.Attribute) and value.attr == 'all' and isinstance(value.value, ast.Name):
|
|
70
|
+
element = _ALL_COLLECTIONS.get(value.value.id)
|
|
71
|
+
return f'list[{element}]' if element else None
|
|
72
|
+
if not isinstance(value, ast.Call):
|
|
73
|
+
return None
|
|
74
|
+
function = _dotted(value.func)
|
|
75
|
+
if function in classes:
|
|
76
|
+
return function
|
|
77
|
+
if function.endswith('.new') and function.count('.') == 2 and function.split('.')[1][:1].isupper():
|
|
78
|
+
return function[:-len('.new')] # a library's type, `dr.AuxData.new(...)` (biased_price_target)
|
|
79
|
+
receiver = None
|
|
80
|
+
if function == 'method_call' and len(value.args) >= 2:
|
|
81
|
+
receiver = value.args[1]
|
|
82
|
+
elif function in methods and value.args:
|
|
83
|
+
receiver = value.args[0]
|
|
84
|
+
if isinstance(receiver, ast.Call):
|
|
85
|
+
return inferable_type(receiver, classes, methods) if _dotted(receiver.func) in classes | methods \
|
|
86
|
+
or _dotted(receiver.func) == 'method_call' else None
|
|
87
|
+
if function == 'color.new' and len(value.args) == 1 and not value.keywords \
|
|
88
|
+
and isinstance(value.args[0], ast.Constant) and isinstance(value.args[0].value, str):
|
|
89
|
+
return 'Color' # a hex colour literal
|
|
90
|
+
if function == 'matrix.new' and len(value.args) >= 3 and literal_type(value.args[2]):
|
|
91
|
+
return f'Matrix[{literal_type(value.args[2])}]' # the element type of the initial value
|
|
92
|
+
typed_na = _typed_na_argument(value)
|
|
93
|
+
if function in ('matrix.new', 'array.new') and typed_na:
|
|
94
|
+
return f"{'Matrix' if function == 'matrix.new' else 'list'}[{typed_na}]" # `NA(T)`: of T
|
|
95
|
+
if function.startswith('array.'):
|
|
96
|
+
element = _ARRAY_ELEMENTS.get(function[len('array.'):])
|
|
97
|
+
if element:
|
|
98
|
+
return f'list[{element}]'
|
|
99
|
+
if function == 'array.from_items' and value.args:
|
|
100
|
+
types = {literal_type(argument) for argument in value.args}
|
|
101
|
+
if len(types) == 1 and None not in types:
|
|
102
|
+
return f'list[{types.pop()}]'
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class DeclaredTypes:
|
|
107
|
+
"""The Python types of the Pine source's typed declarations (``float x = ...``),
|
|
108
|
+
looked up by name and, when the statement's Pine line is known, by that line."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, script) -> None:
|
|
111
|
+
from ....ast_nodes import Assignment, VarDecl, VaripDecl
|
|
112
|
+
from ....ast_walk import walk
|
|
113
|
+
from ....transformer import convert_pine_generic_to_python
|
|
114
|
+
self.by_line: dict = {}
|
|
115
|
+
self.by_name: dict = {}
|
|
116
|
+
self.declared_lines: dict = {}
|
|
117
|
+
for node in walk([getattr(script, 'declarations', []), getattr(script, 'body', [])]):
|
|
118
|
+
if isinstance(node, (Assignment, VarDecl, VaripDecl)):
|
|
119
|
+
name = node.target if isinstance(node, Assignment) else node.name
|
|
120
|
+
if not isinstance(name, str):
|
|
121
|
+
continue
|
|
122
|
+
line = getattr(node, 'line', 0)
|
|
123
|
+
self.declared_lines.setdefault(name, set()).add(line)
|
|
124
|
+
type_hint = getattr(node, 'type_hint', None) or self._generic_constructor_type(node.value)
|
|
125
|
+
if type_hint:
|
|
126
|
+
python_type = convert_pine_generic_to_python(type_hint)
|
|
127
|
+
self.by_line[(name, line)] = python_type
|
|
128
|
+
self.by_name.setdefault(name, python_type)
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _generic_constructor_type(value) -> str | None:
|
|
132
|
+
"""`map.new<string, bool>()` names its type, `map<string, bool>` (probe nb4_02); `T.copy(x)` names `T`
|
|
133
|
+
(Trendoscope/Zigzag `lPivot: Pivot = udt_copy(...)`)."""
|
|
134
|
+
from ....ast_nodes import FunctionCall
|
|
135
|
+
if isinstance(value, FunctionCall) and isinstance(value.func, str) and value.func.startswith('map.new<'):
|
|
136
|
+
return 'map' + value.func[len('map.new'):]
|
|
137
|
+
if isinstance(value, FunctionCall) and isinstance(value.func, str) and value.func.endswith('.copy') \
|
|
138
|
+
and value.func.count('.') == 1 and value.func[:1].isupper() and len(value.args) == 1:
|
|
139
|
+
return value.func[:-len('.copy')]
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
#: the suffixes renaming gives a Pine name: `x__000000c0__`, `x__ren__`, `x__global__`
|
|
143
|
+
_RENAMED = re.compile(r'__[0-9a-f]{8}__$|__ren__$|__global__$')
|
|
144
|
+
|
|
145
|
+
def get(self, name: str, line: int = 0):
|
|
146
|
+
name = self._RENAMED.sub('', name)
|
|
147
|
+
if line and line in self.declared_lines.get(name, ()):
|
|
148
|
+
return self.by_line.get((name, line))
|
|
149
|
+
return self.by_name.get(name)
|
|
150
|
+
|
|
151
|
+
def __contains__(self, name: str) -> bool:
|
|
152
|
+
return self._RENAMED.sub('', name) in self.by_name
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def pine_declared_types(script) -> DeclaredTypes:
|
|
156
|
+
return DeclaredTypes(script)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SeriesAnnotationPass:
|
|
160
|
+
"""A name is a series only where its scope reads it with history: otherwise
|
|
161
|
+
``PersistentSeries[T]`` is ``Persistent[T]`` and a ``Series`` annotation goes."""
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _history_reads(scope: ast.AST) -> set[str]:
|
|
165
|
+
"""Names `scope` reads with history, its nested functions included where they do
|
|
166
|
+
not bind the name themselves (a parameter `min` is not main's `min`)."""
|
|
167
|
+
reads = {node.value.id for node in own_nodes(scope)
|
|
168
|
+
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name)}
|
|
169
|
+
for node in own_nodes(scope):
|
|
170
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
171
|
+
arguments = node.args
|
|
172
|
+
bound = {arg.arg for arg in arguments.posonlyargs + arguments.args + arguments.kwonlyargs}
|
|
173
|
+
bound |= {child.id for child in own_nodes(node)
|
|
174
|
+
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store)}
|
|
175
|
+
reads |= SeriesAnnotationPass._history_reads(node) - bound
|
|
176
|
+
return reads
|
|
177
|
+
|
|
178
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
179
|
+
for scope in function_scopes(tree):
|
|
180
|
+
read_with_history = self._history_reads(scope)
|
|
181
|
+
blocks = [node for node in own_nodes(scope) if hasattr(node, 'body')
|
|
182
|
+
and not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))]
|
|
183
|
+
for owner in [scope] + blocks:
|
|
184
|
+
for field in ('body', 'orelse', 'finalbody'):
|
|
185
|
+
block = getattr(owner, field, None)
|
|
186
|
+
if isinstance(block, list):
|
|
187
|
+
block[:] = [self._demoted(statement, read_with_history) for statement in block]
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _demoted(statement: ast.stmt, read_with_history: set) -> ast.stmt:
|
|
191
|
+
if not (isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name)
|
|
192
|
+
and statement.target.id not in read_with_history):
|
|
193
|
+
return statement
|
|
194
|
+
annotation = statement.annotation
|
|
195
|
+
head = annotation.value if isinstance(annotation, ast.Subscript) else annotation
|
|
196
|
+
if not isinstance(head, ast.Name) or head.id not in _SERIES_KINDS:
|
|
197
|
+
return statement
|
|
198
|
+
replacement = _SERIES_KINDS[head.id]
|
|
199
|
+
if replacement is None: # a plain series: no annotation
|
|
200
|
+
if statement.value is None:
|
|
201
|
+
return statement
|
|
202
|
+
return ast.copy_location(ast.Assign(targets=[statement.target], value=statement.value), statement)
|
|
203
|
+
if isinstance(annotation, ast.Subscript):
|
|
204
|
+
annotation.value = ast.Name(id=replacement, ctx=ast.Load())
|
|
205
|
+
else:
|
|
206
|
+
statement.annotation = ast.Name(id=replacement, ctx=ast.Load())
|
|
207
|
+
return statement
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _unquoted(annotation) -> str:
|
|
211
|
+
"""An annotation's text, a string annotation (`'list[Pivot]'`) unquoted."""
|
|
212
|
+
if isinstance(annotation, ast.Constant) and isinstance(annotation.value, str):
|
|
213
|
+
return annotation.value
|
|
214
|
+
return ast.unparse(annotation)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _element_of_kind(annotation: str | None) -> str | None:
|
|
218
|
+
"""`Persistent[Pivot]`, `Series[Pivot]` -> `Pivot`."""
|
|
219
|
+
if annotation is None:
|
|
220
|
+
return None
|
|
221
|
+
match = re.fullmatch(r'(?:Persistent|PersistentSeries|Series|IBPersistent|IBPersistentSeries)\[(.*)\]', annotation)
|
|
222
|
+
return match.group(1) if match else annotation
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class DeclarationAnnotationPass:
|
|
226
|
+
"""Declarations carry a type PyneComp knows; series and persistent annotations
|
|
227
|
+
keep their element type only then (see the module docstring)."""
|
|
228
|
+
|
|
229
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
230
|
+
typed = pine_declared_types(context.unit.pine_script)
|
|
231
|
+
self._origins = context.origins
|
|
232
|
+
self._classes = frozenset(node.name for node in tree.body if isinstance(node, ast.ClassDef))
|
|
233
|
+
self._methods = frozenset(node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
|
|
234
|
+
and any(isinstance(d, ast.Name) and d.id == 'method' for d in node.decorator_list))
|
|
235
|
+
# each UDT's fields and their annotations: a field read is typed by its field (probe nodes1)
|
|
236
|
+
self._fields = {node.name: {field.target.id: _unquoted(field.annotation) for field in node.body
|
|
237
|
+
if isinstance(field, ast.AnnAssign) and isinstance(field.target, ast.Name)}
|
|
238
|
+
for node in tree.body if isinstance(node, ast.ClassDef)}
|
|
239
|
+
for scope in function_scopes(tree):
|
|
240
|
+
declared: set[str] = set()
|
|
241
|
+
types: dict[str, str] = {}
|
|
242
|
+
if isinstance(scope, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
243
|
+
arguments = scope.args
|
|
244
|
+
for arg in arguments.posonlyargs + arguments.args + arguments.kwonlyargs:
|
|
245
|
+
declared.add(arg.arg)
|
|
246
|
+
if arg.annotation is not None:
|
|
247
|
+
types[arg.arg] = _unquoted(arg.annotation)
|
|
248
|
+
self._annotate(scope.body, declared, typed, types)
|
|
249
|
+
|
|
250
|
+
_classes: frozenset = frozenset()
|
|
251
|
+
_methods: frozenset = frozenset()
|
|
252
|
+
_fields: dict = {}
|
|
253
|
+
_origins: dict = {}
|
|
254
|
+
|
|
255
|
+
def _known(self, name: str, value, typed, statement=None) -> str | None:
|
|
256
|
+
line = self._origins.get(getattr(statement, 'lineno', 0), 0) if statement is not None else 0
|
|
257
|
+
return typed.get(name, line) or inferable_type(value, self._classes, self._methods)
|
|
258
|
+
|
|
259
|
+
def _bare_unless_known(self, statement: ast.AnnAssign, typed: dict[str, str],
|
|
260
|
+
types: dict[str, str] | None = None) -> None:
|
|
261
|
+
annotation = statement.annotation
|
|
262
|
+
if isinstance(annotation, ast.Name) and annotation.id in _PERSISTENT_KINDS:
|
|
263
|
+
# a bare kind takes the type of a constructor's method chain (`var w = T.new().init()`), of a
|
|
264
|
+
# library type's constructor (`var s = lib.T.new()`) (chrono_utils, biased_price_target) or of the
|
|
265
|
+
# field it reads (`cl: Series[float] = c.c`, Trendoscope/oota)
|
|
266
|
+
element = self._constructed_type(statement.value) or self._field_type(statement.value, typed, types or {})
|
|
267
|
+
if element:
|
|
268
|
+
statement.annotation = ast.Subscript(value=annotation, slice=ast.parse(element, mode='eval').body,
|
|
269
|
+
ctx=ast.Load())
|
|
270
|
+
return
|
|
271
|
+
if not (isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name)
|
|
272
|
+
and annotation.value.id in _PERSISTENT_KINDS):
|
|
273
|
+
return
|
|
274
|
+
if not (self._known(statement.target.id, statement.value, typed, statement)
|
|
275
|
+
or self._field_type(statement.value, typed, types or {})):
|
|
276
|
+
statement.annotation = ast.Name(id=annotation.value.id, ctx=ast.Load())
|
|
277
|
+
elif literal_type(statement.value) and statement.target.id not in typed:
|
|
278
|
+
# a literal names the element type itself: `x: Series[float] = 0.0`
|
|
279
|
+
annotation.slice = ast.Name(id=literal_type(statement.value), ctx=ast.Load())
|
|
280
|
+
|
|
281
|
+
def _constructed_type(self, value) -> str | None:
|
|
282
|
+
if isinstance(value, ast.Call):
|
|
283
|
+
function = _dotted(value.func)
|
|
284
|
+
if function.endswith('.new') and function.count('.') == 2 and function.split('.')[1][:1].isupper():
|
|
285
|
+
return function[:-len('.new')] # `dr.DistSettings.new(...)`
|
|
286
|
+
if function in self._methods or function == 'method_call':
|
|
287
|
+
return inferable_type(value, self._classes, self._methods)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
def _field_type(self, value, typed, types: dict[str, str]) -> str | None:
|
|
291
|
+
"""The declared type of the UDT field `value` reads (`p.x`, `a.b.c`), when its receiver's type is known."""
|
|
292
|
+
if not isinstance(value, ast.Attribute):
|
|
293
|
+
return None
|
|
294
|
+
receiver = value.value
|
|
295
|
+
if isinstance(receiver, ast.Name):
|
|
296
|
+
owner = types.get(receiver.id) # only a type this scope declares (Zigzag `llastPivot` is untyped)
|
|
297
|
+
else:
|
|
298
|
+
owner = self._field_type(receiver, typed, types)
|
|
299
|
+
owner = _element_of_kind(owner)
|
|
300
|
+
return self._fields.get(owner, {}).get(value.attr) if owner else None
|
|
301
|
+
|
|
302
|
+
def _annotate(self, body: list[ast.stmt], declared: set[str], typed: dict[str, str],
|
|
303
|
+
types: dict[str, str] | None = None) -> None:
|
|
304
|
+
types = {} if types is None else types
|
|
305
|
+
for index, statement in enumerate(body):
|
|
306
|
+
if isinstance(statement, ast.Assign) and len(statement.targets) == 1 \
|
|
307
|
+
and isinstance(statement.targets[0], ast.Name):
|
|
308
|
+
name = statement.targets[0].id
|
|
309
|
+
value = statement.value
|
|
310
|
+
if isinstance(value, ast.Attribute) and isinstance(value.value, ast.Name) \
|
|
311
|
+
and re.fullmatch(r'__loop_\d+__', value.value.id):
|
|
312
|
+
declared.add(name) # a `pine_loop` variable's value is never annotated (analytics_tables)
|
|
313
|
+
continue
|
|
314
|
+
annotation = self._known(name, statement.value, typed, statement)
|
|
315
|
+
if annotation is None and name not in declared:
|
|
316
|
+
annotation = self._field_type(statement.value, typed, types)
|
|
317
|
+
if name not in declared and annotation and not name.startswith('__'):
|
|
318
|
+
body[index] = ast.copy_location(ast.AnnAssign(
|
|
319
|
+
target=statement.targets[0], annotation=ast.parse(annotation, mode='eval').body,
|
|
320
|
+
value=statement.value, simple=1), statement)
|
|
321
|
+
types[name] = annotation
|
|
322
|
+
declared.add(name)
|
|
323
|
+
elif isinstance(statement, ast.Assign): # `a, b = f()` declares its names
|
|
324
|
+
declared.update(node.id for target in statement.targets for node in ast.walk(target)
|
|
325
|
+
if isinstance(node, ast.Name))
|
|
326
|
+
elif isinstance(statement, ast.AnnAssign) and isinstance(statement.target, ast.Name):
|
|
327
|
+
self._bare_unless_known(statement, typed, types)
|
|
328
|
+
declared.add(statement.target.id)
|
|
329
|
+
types[statement.target.id] = _unquoted(statement.annotation)
|
|
330
|
+
elif isinstance(statement, (ast.For, ast.AsyncFor)):
|
|
331
|
+
declared.update(node.id for node in ast.walk(statement.target) if isinstance(node, ast.Name))
|
|
332
|
+
elif isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
333
|
+
declared.add(statement.name)
|
|
334
|
+
continue
|
|
335
|
+
for name in ('body', 'orelse', 'finalbody'):
|
|
336
|
+
nested = getattr(statement, name, None)
|
|
337
|
+
if isinstance(nested, list):
|
|
338
|
+
# a Pine block is a scope: what it declares is unknown to its siblings
|
|
339
|
+
self._annotate(nested, set(declared), typed, dict(types))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class ColorCastPass:
|
|
343
|
+
"""A colour value is not wrapped in ``Color(...)``: ``GREEN: Color = color.new('#00FF00')``."""
|
|
344
|
+
|
|
345
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
346
|
+
for node in ast.walk(tree):
|
|
347
|
+
for field, value in ast.iter_fields(node):
|
|
348
|
+
if (isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == 'Color'
|
|
349
|
+
and len(value.args) == 1 and not value.keywords and isinstance(value.args[0], ast.Call)):
|
|
350
|
+
setattr(node, field, value.args[0])
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class ClosureHistoryPass:
|
|
354
|
+
"""A function that reads the history of a variable of its enclosing function
|
|
355
|
+
(``avg[1]`` with ``avg`` a main() variable) reads it from its own series,
|
|
356
|
+
``__hist_avg__: Series = avg[0]``, bound first in its body (33 modules of the reference)."""
|
|
357
|
+
|
|
358
|
+
PREFIX, SUFFIX = '__hist_', '__'
|
|
359
|
+
|
|
360
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
361
|
+
main = next((node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == 'main'), None)
|
|
362
|
+
if main is not None:
|
|
363
|
+
# the snapshot keeps the element type the variable is declared with in main():
|
|
364
|
+
# `x: PersistentSeries[float]` or `x: float` gives `__hist_x__: Series[float]`
|
|
365
|
+
self._series_types = {}
|
|
366
|
+
declared = [(arg.arg, arg.annotation) for arg in main.args.args]
|
|
367
|
+
declared += [(node.target.id, node.annotation) for node in own_nodes(main)
|
|
368
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name)]
|
|
369
|
+
for name, annotation in declared:
|
|
370
|
+
element = self._element_type(annotation)
|
|
371
|
+
if element is not None and name not in self._series_types:
|
|
372
|
+
self._series_types[name] = ast.Subscript(value=ast.Name(id='Series', ctx=ast.Load()),
|
|
373
|
+
slice=element, ctx=ast.Load())
|
|
374
|
+
self._visit(main, set())
|
|
375
|
+
|
|
376
|
+
_series_types: dict = {}
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def _element_type(annotation):
|
|
380
|
+
if annotation is None:
|
|
381
|
+
return None
|
|
382
|
+
if isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name) \
|
|
383
|
+
and annotation.value.id in _PERSISTENT_KINDS:
|
|
384
|
+
return annotation.slice
|
|
385
|
+
if isinstance(annotation, ast.Name) and annotation.id not in _PERSISTENT_KINDS:
|
|
386
|
+
return annotation
|
|
387
|
+
return None
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _own_bindings(function: ast.FunctionDef) -> set[str]:
|
|
391
|
+
arguments = function.args
|
|
392
|
+
bound = {arg.arg for arg in arguments.posonlyargs + arguments.args + arguments.kwonlyargs}
|
|
393
|
+
for node in own_nodes(function):
|
|
394
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
395
|
+
bound.add(node.id)
|
|
396
|
+
elif isinstance(node, (ast.FunctionDef, ast.ClassDef)):
|
|
397
|
+
bound.add(node.name)
|
|
398
|
+
return bound
|
|
399
|
+
|
|
400
|
+
def _visit(self, function: ast.FunctionDef, enclosing: set[str]) -> None:
|
|
401
|
+
own = self._own_bindings(function)
|
|
402
|
+
if enclosing:
|
|
403
|
+
snapshots: list[str] = []
|
|
404
|
+
for node in ast.walk(function):
|
|
405
|
+
if (isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name)
|
|
406
|
+
and node.value.id in enclosing and node.value.id not in own
|
|
407
|
+
and node.value.id not in snapshots):
|
|
408
|
+
snapshots.append(node.value.id)
|
|
409
|
+
if snapshots:
|
|
410
|
+
for node in ast.walk(function):
|
|
411
|
+
if (isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name)
|
|
412
|
+
and node.value.id in snapshots):
|
|
413
|
+
node.value = ast.Name(id=f'{self.PREFIX}{node.value.id}{self.SUFFIX}', ctx=ast.Load())
|
|
414
|
+
first = function.body[0] if function.body else function
|
|
415
|
+
function.body[:0] = [ast.copy_location(ast.AnnAssign(
|
|
416
|
+
target=ast.Name(id=f'{self.PREFIX}{name}{self.SUFFIX}', ctx=ast.Store()),
|
|
417
|
+
annotation=self._series_types.get(name) or ast.Name(id='Series', ctx=ast.Load()),
|
|
418
|
+
value=ast.Subscript(value=ast.Name(id=name, ctx=ast.Load()), slice=ast.Constant(0),
|
|
419
|
+
ctx=ast.Load()), simple=1), first) for name in snapshots]
|
|
420
|
+
for node in own_nodes(function):
|
|
421
|
+
if isinstance(node, ast.FunctionDef):
|
|
422
|
+
self._visit(node, (enclosing | own) - self._own_bindings(node))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Rewrites of the generated Python module towards PyneComp's output.
|
|
2
|
+
|
|
3
|
+
pine2pyne and PyneComp agree on what a script computes but often spell it
|
|
4
|
+
differently (``PersistentSeries`` vs ``Persistent``, ``'#00ced1'`` vs
|
|
5
|
+
``color.new('#00CED1')``). Each difference is one :class:`ModulePass`: a
|
|
6
|
+
small rewrite of the Python AST, applied in order by the PyneComp emitter.
|
|
7
|
+
A new difference is a new pass; existing passes stay untouched.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Iterator, Protocol
|
|
14
|
+
|
|
15
|
+
from ....core.contracts import CompilationUnit
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class PassContext:
|
|
20
|
+
unit: CompilationUnit
|
|
21
|
+
#: exact text to print for a node (``id(node)``), e.g. a title's source quotes
|
|
22
|
+
spellings: dict[int, str] = field(default_factory=dict)
|
|
23
|
+
#: Python line -> the Pine line of the statement generated there
|
|
24
|
+
origins: dict[int, int] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def pine_version(self) -> int:
|
|
28
|
+
return self.unit.pine_version or 6
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ModulePass(Protocol):
|
|
32
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main_function(tree: ast.Module) -> ast.FunctionDef | None:
|
|
36
|
+
for node in tree.body:
|
|
37
|
+
if isinstance(node, ast.FunctionDef) and node.name == 'main':
|
|
38
|
+
return node
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def function_scopes(tree: ast.Module) -> Iterator[ast.AST]:
|
|
43
|
+
"""The module and every function in it."""
|
|
44
|
+
yield tree
|
|
45
|
+
for node in ast.walk(tree):
|
|
46
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
47
|
+
yield node
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def own_nodes(scope: ast.AST) -> Iterator[ast.AST]:
|
|
51
|
+
"""Nodes of ``scope`` that are not inside a nested function or class."""
|
|
52
|
+
stack = list(ast.iter_child_nodes(scope))
|
|
53
|
+
while stack:
|
|
54
|
+
node = stack.pop()
|
|
55
|
+
yield node
|
|
56
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
|
|
57
|
+
stack.extend(ast.iter_child_nodes(node))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def statements_in_order(body: list[ast.stmt]) -> Iterator[ast.stmt]:
|
|
61
|
+
"""Statements of a block and its nested blocks (not nested functions), in source order."""
|
|
62
|
+
for statement in body:
|
|
63
|
+
yield statement
|
|
64
|
+
if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
65
|
+
continue
|
|
66
|
+
for name in ('body', 'orelse', 'finalbody'):
|
|
67
|
+
nested = getattr(statement, name, None)
|
|
68
|
+
if isinstance(nested, list):
|
|
69
|
+
yield from statements_in_order(nested)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Built-in calls spelled the way PyneComp spells them."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
|
|
6
|
+
from .base import PassContext
|
|
7
|
+
|
|
8
|
+
#: ``ta`` members that are series values, not calls (``ta.tr(true)`` is the call form)
|
|
9
|
+
TA_SERIES = frozenset({'tr', 'vwap', 'obv', 'accdist', 'iii', 'nvi', 'pvi', 'pvt', 'wad', 'wvad'})
|
|
10
|
+
#: Pine v4 hline and plot styles were integers
|
|
11
|
+
_HLINE_STYLES = {0: 'style_solid', 1: 'style_dotted', 2: 'style_dashed'}
|
|
12
|
+
_PLOT_STYLES = {0: 'style_line', 1: 'style_stepline', 2: 'style_histogram', 3: 'style_cross', 4: 'style_area',
|
|
13
|
+
5: 'style_columns', 6: 'style_circles'}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _dotted(node: ast.AST) -> str:
|
|
17
|
+
if isinstance(node, ast.Attribute):
|
|
18
|
+
return f'{_dotted(node.value)}.{node.attr}'
|
|
19
|
+
return node.id if isinstance(node, ast.Name) else ''
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _attribute(base: str, member: str) -> ast.Attribute:
|
|
23
|
+
return ast.Attribute(value=ast.Name(id=base, ctx=ast.Load()), attr=member, ctx=ast.Load())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _bound_names(tree: ast.Module) -> set[str]:
|
|
27
|
+
bound = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)}
|
|
28
|
+
bound |= {node.arg for node in ast.walk(tree) if isinstance(node, ast.arg)}
|
|
29
|
+
bound |= {node.name for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.ClassDef))}
|
|
30
|
+
return bound
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Replacer(ast.NodeTransformer):
|
|
34
|
+
def __init__(self, replace):
|
|
35
|
+
self._replace = replace
|
|
36
|
+
|
|
37
|
+
def generic_visit(self, node):
|
|
38
|
+
node = super().generic_visit(node)
|
|
39
|
+
replaced = self._replace(node)
|
|
40
|
+
return node if replaced is None else ast.copy_location(replaced, node)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TaSeriesPass:
|
|
44
|
+
"""``ta.tr``, ``ta.vwap``, ``ta.obv``, ... are values: no call without arguments;
|
|
45
|
+
a Pine v4 bare ``tr``/``vwap``/... is the ``ta.`` value."""
|
|
46
|
+
|
|
47
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
48
|
+
bound = _bound_names(tree)
|
|
49
|
+
legacy = context.pine_version <= 4
|
|
50
|
+
|
|
51
|
+
def replace(node):
|
|
52
|
+
if (isinstance(node, ast.Call) and not node.args and not node.keywords
|
|
53
|
+
and isinstance(node.func, ast.Attribute) and node.func.attr in TA_SERIES
|
|
54
|
+
and _dotted(node.func.value) == 'ta'):
|
|
55
|
+
return node.func
|
|
56
|
+
if (legacy and isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
|
|
57
|
+
and node.id in TA_SERIES and node.id not in bound):
|
|
58
|
+
return _attribute('ta', node.id)
|
|
59
|
+
return None
|
|
60
|
+
_Replacer(replace).visit(tree)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class LegacyStrategyDirectionPass:
|
|
64
|
+
"""Pine v4 ``strategy.entry(id, true)`` / ``long=true``: the direction is
|
|
65
|
+
``strategy.long`` / ``strategy.short`` (``direction=`` for the keyword)."""
|
|
66
|
+
|
|
67
|
+
FUNCTIONS = frozenset({'strategy.entry', 'strategy.order'})
|
|
68
|
+
|
|
69
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
70
|
+
if context.pine_version > 4:
|
|
71
|
+
return
|
|
72
|
+
for node in ast.walk(tree):
|
|
73
|
+
if isinstance(node, ast.Call) and _dotted(node.func) in self.FUNCTIONS:
|
|
74
|
+
if len(node.args) >= 2:
|
|
75
|
+
node.args[1] = self._direction(node.args[1])
|
|
76
|
+
for keyword in node.keywords:
|
|
77
|
+
if keyword.arg == 'long':
|
|
78
|
+
keyword.arg, keyword.value = 'direction', self._direction(keyword.value)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _direction(value: ast.expr) -> ast.expr:
|
|
82
|
+
if isinstance(value, ast.Constant) and value.value in (True, False, 1, 0):
|
|
83
|
+
return _attribute('strategy', 'long' if value.value else 'short')
|
|
84
|
+
return value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LegacyLineWidthPass:
|
|
88
|
+
"""Pine v4/v5: a ``linewidth`` that is not a constant of at least 1 is ``math.max(1, width)``."""
|
|
89
|
+
|
|
90
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
91
|
+
if context.pine_version >= 6:
|
|
92
|
+
return
|
|
93
|
+
for node in ast.walk(tree):
|
|
94
|
+
at_least_one = (isinstance(getattr(node, 'value', None), ast.Constant)
|
|
95
|
+
and isinstance(node.value.value, (int, float)) and node.value.value >= 1)
|
|
96
|
+
if isinstance(node, ast.keyword) and node.arg == 'linewidth' \
|
|
97
|
+
and not at_least_one and not self._is_max(node.value):
|
|
98
|
+
node.value = ast.Call(func=_attribute('math', 'max'), args=[ast.Constant(1), node.value], keywords=[])
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _is_max(value: ast.expr) -> bool:
|
|
102
|
+
return isinstance(value, ast.Call) and _dotted(value.func) == 'math.max'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class CurrentValuePass:
|
|
106
|
+
"""``x[0]`` is ``x``."""
|
|
107
|
+
|
|
108
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
109
|
+
def replace(node):
|
|
110
|
+
if (isinstance(node, ast.Subscript) and isinstance(node.ctx, ast.Load)
|
|
111
|
+
and isinstance(node.slice, ast.Constant) and node.slice.value == 0
|
|
112
|
+
and isinstance(node.value, (ast.Name, ast.Attribute))
|
|
113
|
+
and not (isinstance(node.value, ast.Name) and node.value.id.startswith('__'))):
|
|
114
|
+
return node.value
|
|
115
|
+
return None
|
|
116
|
+
_Replacer(replace).visit(tree)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class LegacyHlineStylePass:
|
|
120
|
+
"""Pine v4 ``hline(..., linestyle=2)`` / ``plot(..., style=5)``: the style constant,
|
|
121
|
+
``hline.style_dashed`` / ``plot.style_columns``."""
|
|
122
|
+
|
|
123
|
+
_STYLES = {('hline', 'linestyle'): ('hline', _HLINE_STYLES), ('plot', 'style'): ('plot', _PLOT_STYLES)}
|
|
124
|
+
|
|
125
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
126
|
+
if context.pine_version > 4:
|
|
127
|
+
return
|
|
128
|
+
for node in ast.walk(tree):
|
|
129
|
+
if not isinstance(node, ast.Call):
|
|
130
|
+
continue
|
|
131
|
+
for keyword in node.keywords:
|
|
132
|
+
namespace, styles = self._STYLES.get((_dotted(node.func), keyword.arg), (None, {}))
|
|
133
|
+
if (namespace and isinstance(keyword.value, ast.Constant)
|
|
134
|
+
and type(keyword.value.value) is int and keyword.value.value in styles):
|
|
135
|
+
keyword.value = _attribute(namespace, styles[keyword.value.value])
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class InputOptionsPass:
|
|
139
|
+
"""``options=`` is always a tuple: ``options=x`` (one name) is ``options=(x,)``."""
|
|
140
|
+
|
|
141
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
142
|
+
for node in ast.walk(tree):
|
|
143
|
+
if isinstance(node, ast.Call) and _dotted(node.func).startswith('input'):
|
|
144
|
+
for keyword in node.keywords:
|
|
145
|
+
if keyword.arg == 'options' and not isinstance(keyword.value, (ast.Tuple, ast.List)):
|
|
146
|
+
keyword.value = ast.Tuple(elts=[keyword.value], ctx=ast.Load())
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ShadowedNamespacePass:
|
|
150
|
+
"""A library imported under a built-in namespace's name (``import TradingView/ta/12``)
|
|
151
|
+
reads through ``ta__ren__ = shadowed_namespace(ta__ren____lib, ta)``: the library's
|
|
152
|
+
members first, the built-in's after. Every ``ta.`` of the script reads ``ta__ren__``."""
|
|
153
|
+
|
|
154
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
155
|
+
shadowed = getattr(context.unit.transformed, 'shadowed_namespaces', None) or {}
|
|
156
|
+
if not shadowed:
|
|
157
|
+
return
|
|
158
|
+
for node in ast.walk(tree):
|
|
159
|
+
if (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
|
|
160
|
+
and node.value.id in shadowed):
|
|
161
|
+
node.value = ast.Name(id=f'{node.value.id}__ren__', ctx=ast.Load())
|
|
162
|
+
position = max((index + 1 for index, node in enumerate(tree.body)
|
|
163
|
+
if isinstance(node, (ast.Import, ast.ImportFrom))), default=0)
|
|
164
|
+
tree.body[position:position] = [
|
|
165
|
+
ast.Assign(targets=[ast.Name(id=f'{alias}__ren__', ctx=ast.Store())],
|
|
166
|
+
value=ast.Call(func=ast.Name(id='shadowed_namespace', ctx=ast.Load()),
|
|
167
|
+
args=[ast.Name(id=module, ctx=ast.Load()), ast.Name(id=alias, ctx=ast.Load())],
|
|
168
|
+
keywords=[]), lineno=0, col_offset=0)
|
|
169
|
+
for alias, module in shadowed.items()]
|