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,205 @@
|
|
|
1
|
+
"""Temporaries PyneComp hoists in front of a statement (Pine v4/v5).
|
|
2
|
+
|
|
3
|
+
Pine v4 and v5 evaluate both operands of ``and``/``or``; Python evaluates the
|
|
4
|
+
second only when needed. PyneComp keeps a stateful call (``ta.*``,
|
|
5
|
+
``request.*``, a user function) in the second or a later operand running on
|
|
6
|
+
every bar by computing it first, into ``<name>_<n>``; a time function given a
|
|
7
|
+
time (``month(time)``) counts as stateful, and a stateful call in the arguments
|
|
8
|
+
of one is computed before it::
|
|
9
|
+
|
|
10
|
+
crossover_1 = ta.crossover(close, fastSma)
|
|
11
|
+
long = fastAboveSlow and crossover_1
|
|
12
|
+
|
|
13
|
+
The number counts per name within one block. Pine v4 ``iff(c, a, b)`` is
|
|
14
|
+
``a if c else b``; an ``iff`` nested in its arguments is hoisted the same way
|
|
15
|
+
(``iff_1 = ...``), innermost first, after the calls.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ast
|
|
20
|
+
from typing import Callable
|
|
21
|
+
|
|
22
|
+
from .base import PassContext
|
|
23
|
+
|
|
24
|
+
_BLOCK_FIELDS = ('body', 'orelse', 'finalbody')
|
|
25
|
+
#: the time functions PyneComp hoists like a stateful call when they are given a time
|
|
26
|
+
TIME_FUNCTIONS = frozenset({'time', 'time_close', 'year', 'month', 'weekofyear', 'dayofmonth', 'dayofweek',
|
|
27
|
+
'hour', 'minute', 'second'})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _call_name(call: ast.Call) -> str | None:
|
|
31
|
+
func = call.func
|
|
32
|
+
if isinstance(func, ast.Name):
|
|
33
|
+
return func.id
|
|
34
|
+
if isinstance(func, ast.Attribute):
|
|
35
|
+
return func.attr
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _dotted(node: ast.expr) -> str:
|
|
40
|
+
if isinstance(node, ast.Attribute):
|
|
41
|
+
return f'{_dotted(node.value)}.{node.attr}'
|
|
42
|
+
return node.id if isinstance(node, ast.Name) else ''
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _BlockNames:
|
|
46
|
+
"""``<name>_<n>`` numbering within one block."""
|
|
47
|
+
|
|
48
|
+
def __init__(self) -> None:
|
|
49
|
+
self._counts: dict[str, int] = {}
|
|
50
|
+
|
|
51
|
+
def next(self, stem: str) -> str:
|
|
52
|
+
self._counts[stem] = self._counts.get(stem, 0) + 1
|
|
53
|
+
return f'{stem}_{self._counts[stem]}'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _statement_expressions(statement: ast.stmt) -> list[tuple[ast.AST, str]]:
|
|
57
|
+
"""(owner, field) of the expressions a statement evaluates before its body runs;
|
|
58
|
+
an `elif` condition belongs to its `if`."""
|
|
59
|
+
if isinstance(statement, ast.If):
|
|
60
|
+
found = [(statement, 'test')]
|
|
61
|
+
orelse = statement.orelse
|
|
62
|
+
while len(orelse) == 1 and isinstance(orelse[0], ast.If):
|
|
63
|
+
found.append((orelse[0], 'test'))
|
|
64
|
+
orelse = orelse[0].orelse
|
|
65
|
+
return found
|
|
66
|
+
if isinstance(statement, (ast.Assign, ast.AnnAssign, ast.AugAssign, ast.Expr, ast.Return)):
|
|
67
|
+
return [(statement, 'value')] if getattr(statement, 'value', None) is not None else []
|
|
68
|
+
if isinstance(statement, ast.For):
|
|
69
|
+
return [(statement, 'iter')]
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _nested_blocks(statement: ast.stmt):
|
|
74
|
+
"""The blocks a statement holds, elif chains included."""
|
|
75
|
+
for field in _BLOCK_FIELDS:
|
|
76
|
+
block = getattr(statement, field, None)
|
|
77
|
+
if isinstance(block, list) and block:
|
|
78
|
+
if isinstance(statement, ast.If) and field == 'orelse' and len(block) == 1 and isinstance(block[0], ast.If):
|
|
79
|
+
yield from _nested_blocks(block[0])
|
|
80
|
+
yield block[0].body # the elif's own body
|
|
81
|
+
continue
|
|
82
|
+
yield block
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def rewrite_blocks(tree: ast.Module, rewrite: Callable[[ast.stmt, _BlockNames], list[ast.stmt]]) -> None:
|
|
86
|
+
"""Apply ``rewrite`` (statement -> statements to insert before it) to every block."""
|
|
87
|
+
def visit(block: list[ast.stmt]) -> None:
|
|
88
|
+
names = _BlockNames()
|
|
89
|
+
index = 0
|
|
90
|
+
while index < len(block):
|
|
91
|
+
statement = block[index]
|
|
92
|
+
inserted = rewrite(statement, names)
|
|
93
|
+
for offset, new in enumerate(inserted):
|
|
94
|
+
block.insert(index + offset, ast.copy_location(new, statement))
|
|
95
|
+
index += len(inserted)
|
|
96
|
+
if not isinstance(statement, (ast.FunctionDef, ast.ClassDef)): # functions: visited on their own
|
|
97
|
+
for nested in list(_nested_blocks(statement)):
|
|
98
|
+
visit(nested)
|
|
99
|
+
index += 1
|
|
100
|
+
for node in ast.walk(tree):
|
|
101
|
+
if isinstance(node, ast.FunctionDef):
|
|
102
|
+
visit(node.body)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StrictBooleanHoistingPass:
|
|
106
|
+
"""Pine v4/v5: stateful calls in a later operand of ``and``/``or`` are computed first."""
|
|
107
|
+
|
|
108
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
109
|
+
if context.pine_version >= 6:
|
|
110
|
+
return
|
|
111
|
+
user_functions = {node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)}
|
|
112
|
+
|
|
113
|
+
def stateful(call: ast.Call) -> bool:
|
|
114
|
+
dotted = _dotted(call.func)
|
|
115
|
+
if dotted in TIME_FUNCTIONS:
|
|
116
|
+
return bool(call.args or call.keywords) # `month(time)`, `hour(time, tz)` (corpus)
|
|
117
|
+
return (dotted.startswith(('ta.', 'request.')) or dotted in user_functions) and dotted != 'main'
|
|
118
|
+
|
|
119
|
+
def rewrite(statement: ast.stmt, names: _BlockNames) -> list[ast.stmt]:
|
|
120
|
+
hoisted: list[ast.stmt] = []
|
|
121
|
+
|
|
122
|
+
def hoist_in(node: ast.AST) -> ast.AST:
|
|
123
|
+
"""``node`` with its outermost stateful calls replaced by temporaries."""
|
|
124
|
+
if isinstance(node, ast.Call) and stateful(node):
|
|
125
|
+
# a stateful call in its arguments first: `ema_1 = ta.ema(close, 5)`, `change_1 = ta.change(ema_1)`
|
|
126
|
+
for index, argument in enumerate(node.args):
|
|
127
|
+
node.args[index] = visit_operand_free(argument, hoist=True)
|
|
128
|
+
for keyword in node.keywords:
|
|
129
|
+
keyword.value = visit_operand_free(keyword.value, hoist=True)
|
|
130
|
+
name = names.next(_call_name(node))
|
|
131
|
+
hoisted.append(ast.Assign(targets=[ast.Name(id=name, ctx=ast.Store())], value=node))
|
|
132
|
+
return ast.Name(id=name, ctx=ast.Load())
|
|
133
|
+
return visit_operand_free(node, hoist=True)
|
|
134
|
+
|
|
135
|
+
def visit_operand_free(node: ast.AST, hoist: bool = False) -> ast.AST:
|
|
136
|
+
"""Walk ``node``; BoolOps inside it hoist from their later operands."""
|
|
137
|
+
if hoist and isinstance(node, ast.Call) and _dotted(node.func) == 'inline_series' \
|
|
138
|
+
and len(node.args) == 2 and isinstance(node.args[0], ast.Call) and stateful(node.args[0]):
|
|
139
|
+
# `inline_series(ta.lowest(l, n), n)`: the call is hoisted as a series and read with history,
|
|
140
|
+
# `lowest_1: Series = ta.lowest(l, n)`, `lowest_1[n]` (MightyZinger/Chikou)
|
|
141
|
+
temporary = hoist_in(node.args[0])
|
|
142
|
+
hoisted[-1] = ast.AnnAssign(target=ast.Name(id=temporary.id, ctx=ast.Store()),
|
|
143
|
+
annotation=ast.Name(id='Series', ctx=ast.Load()),
|
|
144
|
+
value=hoisted[-1].value, simple=1)
|
|
145
|
+
return ast.Subscript(value=temporary, slice=visit_operand_free(node.args[1]), ctx=ast.Load())
|
|
146
|
+
if isinstance(node, ast.BoolOp):
|
|
147
|
+
node.values[0] = visit_operand_free(node.values[0])
|
|
148
|
+
node.values[1:] = [hoist_in(value) for value in node.values[1:]]
|
|
149
|
+
return node
|
|
150
|
+
if hoist and isinstance(node, ast.Call) and stateful(node):
|
|
151
|
+
return hoist_in(node)
|
|
152
|
+
for field, value in ast.iter_fields(node):
|
|
153
|
+
if isinstance(value, ast.AST) and not isinstance(value, (ast.expr_context, ast.operator)):
|
|
154
|
+
setattr(node, field, visit_operand_free(value, hoist))
|
|
155
|
+
elif isinstance(value, list):
|
|
156
|
+
setattr(node, field, [visit_operand_free(item, hoist) if isinstance(item, ast.AST) else item
|
|
157
|
+
for item in value])
|
|
158
|
+
return node
|
|
159
|
+
|
|
160
|
+
for owner, field in _statement_expressions(statement):
|
|
161
|
+
setattr(owner, field, visit_operand_free(getattr(owner, field)))
|
|
162
|
+
return hoisted
|
|
163
|
+
|
|
164
|
+
rewrite_blocks(tree, rewrite)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class LegacyIffPass:
|
|
168
|
+
"""Pine v4 ``iff(c, a, b)`` is ``a if c else b``; a nested ``iff`` is hoisted to ``iff_<n>``."""
|
|
169
|
+
|
|
170
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
171
|
+
if context.pine_version >= 5:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
def is_iff(node: ast.AST) -> bool:
|
|
175
|
+
return (isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == 'iff'
|
|
176
|
+
and len(node.args) == 3 and not node.keywords)
|
|
177
|
+
|
|
178
|
+
def rewrite(statement: ast.stmt, names: _BlockNames) -> list[ast.stmt]:
|
|
179
|
+
hoisted: list[ast.stmt] = []
|
|
180
|
+
|
|
181
|
+
def ternary(call: ast.Call, nested: bool) -> ast.expr:
|
|
182
|
+
condition, then, otherwise = (convert(argument, nested=True) for argument in call.args)
|
|
183
|
+
expression = ast.IfExp(test=condition, body=then, orelse=otherwise)
|
|
184
|
+
if not nested:
|
|
185
|
+
return expression
|
|
186
|
+
name = names.next('iff')
|
|
187
|
+
hoisted.append(ast.Assign(targets=[ast.Name(id=name, ctx=ast.Store())], value=expression))
|
|
188
|
+
return ast.Name(id=name, ctx=ast.Load())
|
|
189
|
+
|
|
190
|
+
def convert(node: ast.AST, nested: bool = False) -> ast.AST:
|
|
191
|
+
if is_iff(node):
|
|
192
|
+
return ternary(node, nested)
|
|
193
|
+
for field, value in ast.iter_fields(node):
|
|
194
|
+
if isinstance(value, ast.AST):
|
|
195
|
+
setattr(node, field, convert(value, nested))
|
|
196
|
+
elif isinstance(value, list):
|
|
197
|
+
setattr(node, field, [convert(item, nested) if isinstance(item, ast.AST) else item
|
|
198
|
+
for item in value])
|
|
199
|
+
return node
|
|
200
|
+
|
|
201
|
+
for owner, field in _statement_expressions(statement):
|
|
202
|
+
setattr(owner, field, convert(getattr(owner, field)))
|
|
203
|
+
return hoisted
|
|
204
|
+
|
|
205
|
+
rewrite_blocks(tree, rewrite)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Imports that match what the rewritten module actually uses."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import builtins
|
|
6
|
+
|
|
7
|
+
from .base import PassContext
|
|
8
|
+
|
|
9
|
+
#: Where PyneComp imports a name from, for the names that do not live in
|
|
10
|
+
#: ``pynecore.lib`` (mined from the import blocks of ``assets/pyne``)
|
|
11
|
+
KNOWN_ORIGINS: dict[str, str] = {
|
|
12
|
+
'pine_range': 'pynecore', 'pine_loop': 'pynecore',
|
|
13
|
+
'overload': 'pynecore.core.overload',
|
|
14
|
+
'cast_bool': 'pynecore.core.pine_cast', 'cast_int': 'pynecore.core.pine_cast',
|
|
15
|
+
'cast_float': 'pynecore.core.pine_cast', 'cast_string': 'pynecore.core.pine_cast',
|
|
16
|
+
'Exported': 'pynecore.core.pine_export', 'export': 'pynecore.core.pine_export',
|
|
17
|
+
'shadowed_namespace': 'pynecore.core.pine_import',
|
|
18
|
+
'method_call': 'pynecore.core.pine_method', 'method': 'pynecore.core.pine_method',
|
|
19
|
+
'udt': 'pynecore.core.pine_udt', 'udt_copy': 'pynecore.core.pine_udt',
|
|
20
|
+
'inline_series': 'pynecore.core.series',
|
|
21
|
+
'Color': 'pynecore.types', 'Line': 'pynecore.types', 'NA': 'pynecore.types',
|
|
22
|
+
'Persistent': 'pynecore.types', 'PersistentSeries': 'pynecore.types', 'Series': 'pynecore.types',
|
|
23
|
+
'Box': 'pynecore.types', 'Label': 'pynecore.types', 'Table': 'pynecore.types',
|
|
24
|
+
'IBPersistent': 'pynecore.types', 'IBPersistentSeries': 'pynecore.types', 'LineFill': 'pynecore.types',
|
|
25
|
+
'Matrix': 'pynecore.types', 'Polyline': 'pynecore.types', 'Footprint': 'pynecore.types',
|
|
26
|
+
'ChartPoint': 'pynecore.types.chart',
|
|
27
|
+
'field': 'dataclasses', 'StrEnum': 'enum',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#: Names PyneComp imports from ``pynecore.lib`` (the same mining)
|
|
31
|
+
LIB_NAMES = frozenset((
|
|
32
|
+
'adjustment alert alertcondition array ask bar_index barcolor barmerge barstate bgcolor bid box chart '
|
|
33
|
+
'close color currency dayofmonth dayofweek display dividends earnings extend fill fixnan font footprint '
|
|
34
|
+
'format high hl2 hlc3 hlcc4 hline hour input label last_bar_index last_bar_time line linefill location '
|
|
35
|
+
'log low map math matrix max_bars_back minute month na nz ohlc4 open order plot plotarrow plotbar '
|
|
36
|
+
'plotcandle plotchar plotshape polyline position request runtime scale script second session shape '
|
|
37
|
+
'size splits strategy string syminfo ta table text ticker time time_close time_tradingday timeframe '
|
|
38
|
+
'timenow timestamp volume weekofyear xloc year yloc').split())
|
|
39
|
+
|
|
40
|
+
_MANAGED_PREFIXES = ('pynecore', 'dataclasses', 'enum')
|
|
41
|
+
#: Names imported under an alias: name -> (module, imported name)
|
|
42
|
+
KNOWN_ALIASES = {'_dataclass_field': ('dataclasses', 'field'), 'Enum': ('enum', 'StrEnum')}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _bound_names(tree: ast.Module) -> set[str]:
|
|
46
|
+
"""Names the module binds itself (anywhere), which it must not import."""
|
|
47
|
+
bound: set[str] = set()
|
|
48
|
+
for node in ast.walk(tree):
|
|
49
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
50
|
+
bound.add(node.name)
|
|
51
|
+
elif isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
52
|
+
bound.add(node.id)
|
|
53
|
+
elif isinstance(node, ast.arg):
|
|
54
|
+
bound.add(node.arg)
|
|
55
|
+
return bound
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ImportReconciliationPass:
|
|
59
|
+
"""Drops imported names nothing uses, adds the ones a rewrite introduced,
|
|
60
|
+
and imports each from the module PyneComp takes it from (``NA`` from
|
|
61
|
+
``pynecore.types``, not ``pynecore.lib``). Run it last."""
|
|
62
|
+
|
|
63
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
64
|
+
imports = [node for node in tree.body if isinstance(node, ast.ImportFrom)]
|
|
65
|
+
managed = [node for node in imports if (node.module or '').startswith(_MANAGED_PREFIXES)]
|
|
66
|
+
used = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load)
|
|
67
|
+
and not getattr(node, 'added_default', False)}
|
|
68
|
+
used |= {decorator.id for node in ast.walk(tree)
|
|
69
|
+
for decorator in getattr(node, 'decorator_list', []) if isinstance(decorator, ast.Name)}
|
|
70
|
+
local = _bound_names(tree) - {alias.asname or alias.name for node in imports for alias in node.names}
|
|
71
|
+
|
|
72
|
+
wanted: dict[str, list[str]] = {}
|
|
73
|
+
aliases: dict[str, list[ast.alias]] = {}
|
|
74
|
+
for node in managed:
|
|
75
|
+
for alias in node.names:
|
|
76
|
+
visible = alias.asname or alias.name
|
|
77
|
+
if visible not in used:
|
|
78
|
+
continue
|
|
79
|
+
if alias.asname:
|
|
80
|
+
aliases.setdefault(node.module, []).append(alias)
|
|
81
|
+
continue
|
|
82
|
+
module = KNOWN_ORIGINS.get(alias.name, node.module)
|
|
83
|
+
wanted.setdefault(module, [])
|
|
84
|
+
if alias.name not in wanted[module]:
|
|
85
|
+
wanted[module].append(alias.name)
|
|
86
|
+
already = {name for names in wanted.values() for name in names}
|
|
87
|
+
already |= {alias.asname for names in aliases.values() for alias in names}
|
|
88
|
+
for name in sorted(used - already - local - set(dir(builtins))):
|
|
89
|
+
if name in KNOWN_ALIASES:
|
|
90
|
+
module, imported = KNOWN_ALIASES[name]
|
|
91
|
+
aliases.setdefault(module, []).append(ast.alias(name=imported, asname=name))
|
|
92
|
+
continue
|
|
93
|
+
module = KNOWN_ORIGINS.get(name) or ('pynecore.lib' if name in LIB_NAMES else None)
|
|
94
|
+
if module:
|
|
95
|
+
wanted.setdefault(module, []).append(name)
|
|
96
|
+
|
|
97
|
+
rebuilt = [ast.ImportFrom(module=module, names=[ast.alias(name=name) for name in names]
|
|
98
|
+
+ aliases.pop(module, []), level=0)
|
|
99
|
+
for module, names in wanted.items() if names]
|
|
100
|
+
rebuilt += [ast.ImportFrom(module=module, names=names, level=0) for module, names in aliases.items()]
|
|
101
|
+
first = next((index for index, node in enumerate(tree.body) if node in managed), None)
|
|
102
|
+
tree.body[:] = [node for node in tree.body if node not in managed]
|
|
103
|
+
if rebuilt:
|
|
104
|
+
position = first if first is not None else _after_docstring(tree)
|
|
105
|
+
tree.body[position:position] = rebuilt
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _after_docstring(tree: ast.Module) -> int:
|
|
109
|
+
body = tree.body
|
|
110
|
+
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant):
|
|
111
|
+
return 1
|
|
112
|
+
return 0
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Pine v4 transparency, merged into the colour the way PyneComp merges it."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .base import PassContext, function_scopes, own_nodes
|
|
8
|
+
|
|
9
|
+
_HEX_COLOR = re.compile(r'#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})')
|
|
10
|
+
_COLOR_KEYWORDS = {
|
|
11
|
+
'plot': ('color',), 'plotshape': ('color', 'textcolor'), 'plotchar': ('color',),
|
|
12
|
+
'plotarrow': ('colorup', 'colordown'), 'fill': ('color',),
|
|
13
|
+
}
|
|
14
|
+
#: v4 positional parameters: (index of color, index of transp)
|
|
15
|
+
_POSITIONAL = {'plot': (2, None), 'plotshape': (4, 5), 'plotchar': (4, 5), 'fill': (2, 3)}
|
|
16
|
+
#: v4's default ``transp`` of the functions that had the argument
|
|
17
|
+
_DEFAULT_TRANSPARENCY = {'plot': 0, 'plotshape': 0, 'plotchar': 0, 'plotarrow': 0, 'fill': 90}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
#: Pine's named colour constants
|
|
21
|
+
COLOUR_CONSTANTS = frozenset({
|
|
22
|
+
'aqua', 'black', 'blue', 'fuchsia', 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange',
|
|
23
|
+
'purple', 'red', 'silver', 'teal', 'white', 'yellow'})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_constant_colour(node: ast.expr) -> bool:
|
|
27
|
+
"""A hex literal or a named constant (``color.red``)."""
|
|
28
|
+
if isinstance(node, ast.Constant):
|
|
29
|
+
return isinstance(node.value, str) and bool(_HEX_COLOR.fullmatch(node.value))
|
|
30
|
+
return (isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name)
|
|
31
|
+
and node.value.id == 'color' and node.attr in COLOUR_CONSTANTS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_colour_constructor(call: ast.Call) -> bool:
|
|
35
|
+
"""`color.new(c, transp=85)`: there transp is the colour's own argument."""
|
|
36
|
+
return (isinstance(call.func, ast.Attribute) and isinstance(call.func.value, ast.Name)
|
|
37
|
+
and call.func.value.id == 'color' and call.func.attr in ('new', 'rgb'))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _wrap(colour: ast.expr, transparency: ast.expr) -> ast.Call:
|
|
41
|
+
return ast.Call(func=ast.Attribute(value=ast.Name(id='color', ctx=ast.Load()), attr='new', ctx=ast.Load()),
|
|
42
|
+
args=[colour, transparency], keywords=[])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LegacyTransparencyPass:
|
|
46
|
+
"""Pine v4 ``plot(..., color=c, transp=t)`` is ``color=color.new(c, t)``; with no
|
|
47
|
+
``transp`` a constant colour takes v4's default (``0``, ``90`` for ``fill``).
|
|
48
|
+
A name bound once to a constant colour counts as one; a series colour (a
|
|
49
|
+
conditional, a call) keeps its value, and ``transp`` is dropped
|
|
50
|
+
(about 1100 v4 call sites of the reference follow this)."""
|
|
51
|
+
|
|
52
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
53
|
+
if context.pine_version >= 5: # transp is gone: dropped, not merged
|
|
54
|
+
for node in ast.walk(tree):
|
|
55
|
+
if isinstance(node, ast.Call) and not _is_colour_constructor(node):
|
|
56
|
+
node.keywords = [keyword for keyword in node.keywords if keyword.arg != 'transp']
|
|
57
|
+
return
|
|
58
|
+
for scope in function_scopes(tree):
|
|
59
|
+
constants = self._constant_colour_names(scope)
|
|
60
|
+
for node in own_nodes(scope):
|
|
61
|
+
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) \
|
|
62
|
+
and node.func.id in _COLOR_KEYWORDS:
|
|
63
|
+
self._merge(node, constants)
|
|
64
|
+
for node in ast.walk(tree): # transp of any other call (bgcolor, ...) has no equivalent
|
|
65
|
+
if isinstance(node, ast.Call) and not _is_colour_constructor(node):
|
|
66
|
+
node.keywords = [keyword for keyword in node.keywords if keyword.arg != 'transp']
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _constant_colour_names(scope: ast.AST) -> set[str]:
|
|
70
|
+
bindings: dict[str, list[ast.expr]] = {}
|
|
71
|
+
for node in own_nodes(scope):
|
|
72
|
+
if isinstance(node, (ast.Assign, ast.AnnAssign)) and node.value is not None:
|
|
73
|
+
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
|
|
74
|
+
for target in targets:
|
|
75
|
+
if isinstance(target, ast.Name):
|
|
76
|
+
bindings.setdefault(target.id, []).append(node.value)
|
|
77
|
+
return {name for name, values in bindings.items()
|
|
78
|
+
if len(values) == 1 and _is_constant_colour(values[0])}
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def _merge(call: ast.Call, constants: set[str]) -> None:
|
|
82
|
+
function = call.func.id
|
|
83
|
+
transparency = next((keyword.value for keyword in call.keywords if keyword.arg == 'transp'), None)
|
|
84
|
+
call.keywords = [keyword for keyword in call.keywords if keyword.arg != 'transp']
|
|
85
|
+
colour_index, transp_index = _POSITIONAL.get(function, (None, None))
|
|
86
|
+
if transp_index is not None and len(call.args) > transp_index:
|
|
87
|
+
transparency = call.args.pop(transp_index)
|
|
88
|
+
|
|
89
|
+
def merged(value: ast.expr) -> ast.expr:
|
|
90
|
+
constant = _is_constant_colour(value) or (isinstance(value, ast.Name) and value.id in constants)
|
|
91
|
+
if constant:
|
|
92
|
+
return _wrap(value, transparency or ast.Constant(_DEFAULT_TRANSPARENCY[function]))
|
|
93
|
+
if transparency is not None and function == 'fill' and not isinstance(value, ast.Constant):
|
|
94
|
+
return _wrap(value, transparency)
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
if colour_index is not None and len(call.args) > colour_index:
|
|
98
|
+
call.args[colour_index] = merged(call.args[colour_index])
|
|
99
|
+
for keyword in call.keywords:
|
|
100
|
+
if keyword.arg in _COLOR_KEYWORDS[function]:
|
|
101
|
+
keyword.value = merged(keyword.value)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""A library's module laid out the way PyneComp lays it out.
|
|
2
|
+
|
|
3
|
+
Measured on the 85 libraries of ``assets/pyne`` and probe scripts compiled by
|
|
4
|
+
PyneComp v6.0.80 (``assets/probes/lib*``)::
|
|
5
|
+
|
|
6
|
+
<imports>
|
|
7
|
+
from typing import Protocol, Any
|
|
8
|
+
|
|
9
|
+
__all__ = [...] every exported name, emitted spelling, sorted
|
|
10
|
+
|
|
11
|
+
<enums and UDT classes, in source order>
|
|
12
|
+
|
|
13
|
+
class _ProtocolInit(Protocol): one per exported function or method, source order
|
|
14
|
+
def __call__(self, <its parameters>) -> Any: ...
|
|
15
|
+
|
|
16
|
+
init: _ProtocolInit = Exported() one per exported function or method
|
|
17
|
+
|
|
18
|
+
<module constants>
|
|
19
|
+
|
|
20
|
+
@script.library(...)
|
|
21
|
+
def main(): ... @export, @overload, @method in that order
|
|
22
|
+
|
|
23
|
+
A protocol is named ``_Protocol`` + the function's name with each ``_``-part
|
|
24
|
+
capitalised at its first letter (``get_sec_in_day`` -> ``GetSecInDay``, ``renderOnly`` ->
|
|
25
|
+
``Renderonly``, ``trailing_3step`` -> ``Trailing3Step``); a
|
|
26
|
+
name that already has one gets the protocol's 0-based index across the module
|
|
27
|
+
as a suffix (``_ProtocolInit38``). Its parameters are the function's, but a
|
|
28
|
+
default of ``na`` (one Python needs after a defaulted parameter) is ``...`` and
|
|
29
|
+
a ``Series[T]`` the function's body earned by reading the parameter's history
|
|
30
|
+
is ``T`` (a parameter declared ``series T`` keeps ``Series[T]``).
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import ast
|
|
35
|
+
import copy
|
|
36
|
+
import re
|
|
37
|
+
|
|
38
|
+
from .base import PassContext, main_function
|
|
39
|
+
|
|
40
|
+
_DECORATOR_ORDER = {'export': 0, 'overload': 1, 'method': 2}
|
|
41
|
+
_EXPORTED_TYPE = re.compile(r'^export\s+(?:type|enum)\s+(\w+)', re.M)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _source_position(node: ast.ClassDef, context: PassContext) -> int:
|
|
45
|
+
"""Where the Pine source declares the `type`/`enum` a class is generated from."""
|
|
46
|
+
match = re.search(rf'^(?:export\s+)?(?:type|enum)\s+{re.escape(_pine_name(node.name))}\b',
|
|
47
|
+
context.unit.pine_source or '', re.M)
|
|
48
|
+
return match.start() if match else 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _capitalised(part: str) -> str:
|
|
52
|
+
"""The first letter upper case, the rest lower case: `3step` -> `3Step`, `is2Tap` -> `Is2tap`."""
|
|
53
|
+
match = re.search(r'[A-Za-z]', part)
|
|
54
|
+
if not match:
|
|
55
|
+
return part
|
|
56
|
+
start = match.start()
|
|
57
|
+
return part[:start] + part[start].upper() + part[start + 1:].lower()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def protocol_stem(name: str) -> str:
|
|
61
|
+
"""`get_sec_in_day` -> `GetSecInDay`, `trailing_3step` -> `Trailing3Step`; a renamed function keeps its
|
|
62
|
+
Pine name (`fill__ren__` -> `Fill`)."""
|
|
63
|
+
name = re.sub(r'__ren__$|__global__$', '', name)
|
|
64
|
+
return ''.join(_capitalised(part) for part in name.split('_'))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _pine_name(name: str) -> str:
|
|
68
|
+
"""The Pine name of a renamed parameter (`format__ren__`, `adr__00000017__` -> `format`, `adr`)."""
|
|
69
|
+
return re.sub(r'__ren__$|__global__$|__[0-9a-f]{8}__$', '', name)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_exported(node: ast.AST) -> bool:
|
|
73
|
+
return isinstance(node, ast.FunctionDef) and any(
|
|
74
|
+
isinstance(decorator, ast.Name) and decorator.id == 'export' for decorator in node.decorator_list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_library(tree: ast.Module) -> bool:
|
|
78
|
+
main = main_function(tree)
|
|
79
|
+
return main is not None and any(
|
|
80
|
+
isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute)
|
|
81
|
+
and decorator.func.attr == 'library' for decorator in main.decorator_list)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _exported_call(node: ast.stmt) -> bool:
|
|
85
|
+
value = getattr(node, 'value', None)
|
|
86
|
+
return isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == 'Exported'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _assigns_all(node: ast.stmt) -> bool:
|
|
90
|
+
return isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == '__all__'
|
|
91
|
+
for target in node.targets)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class LibraryPreamblePass:
|
|
95
|
+
"""``__all__``, the ``_Protocol*`` classes and the ``Exported()`` declarations of a library."""
|
|
96
|
+
|
|
97
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
98
|
+
main = main_function(tree)
|
|
99
|
+
if main is None:
|
|
100
|
+
return
|
|
101
|
+
for node in ast.walk(main): # PyneComp's decorator order, any script
|
|
102
|
+
if isinstance(node, ast.FunctionDef) and len(node.decorator_list) > 1:
|
|
103
|
+
node.decorator_list.sort(key=lambda decorator: _DECORATOR_ORDER.get(
|
|
104
|
+
getattr(decorator, 'id', ''), -1))
|
|
105
|
+
if not _is_library(tree):
|
|
106
|
+
self._classes_before_constants(tree, context)
|
|
107
|
+
return
|
|
108
|
+
exported = [node for node in main.body if _is_exported(node)]
|
|
109
|
+
declared = self._declared_parameters(context, len(exported))
|
|
110
|
+
protocols, declarations, seen = [], [], set()
|
|
111
|
+
for index, function in enumerate(exported):
|
|
112
|
+
name = f'_Protocol{protocol_stem(function.name)}'
|
|
113
|
+
if name in seen:
|
|
114
|
+
name = f'{name}{index}'
|
|
115
|
+
seen.add(f'_Protocol{protocol_stem(function.name)}')
|
|
116
|
+
protocols.append(self._protocol(name, function, *declared[index]))
|
|
117
|
+
declarations.append(ast.AnnAssign(target=ast.Name(id=function.name, ctx=ast.Store()),
|
|
118
|
+
annotation=ast.Name(id=name, ctx=ast.Load()),
|
|
119
|
+
value=ast.Call(func=ast.Name(id='Exported', ctx=ast.Load()),
|
|
120
|
+
args=[], keywords=[]), simple=1))
|
|
121
|
+
exported_types = set(_EXPORTED_TYPE.findall(context.unit.pine_source or ''))
|
|
122
|
+
classes = [node for node in tree.body if isinstance(node, ast.ClassDef)]
|
|
123
|
+
public = {function.name for function in exported} | {node.name for node in classes
|
|
124
|
+
if _pine_name(node.name) in exported_types}
|
|
125
|
+
names = ast.Assign(targets=[ast.Name(id='__all__', ctx=ast.Store())],
|
|
126
|
+
value=ast.List(elts=[ast.Constant(name) for name in sorted(public)], ctx=ast.Load()))
|
|
127
|
+
|
|
128
|
+
imports = [node for node in tree.body if isinstance(node, (ast.Import, ast.ImportFrom))]
|
|
129
|
+
docstring = tree.body[:1] if tree.body and isinstance(tree.body[0], ast.Expr) else []
|
|
130
|
+
rest = [node for node in tree.body if node not in imports and node not in docstring
|
|
131
|
+
and not _exported_call(node) and not _assigns_all(node)]
|
|
132
|
+
# a library alias shadowing a builtin namespace (`ta__ren__ = shadowed_namespace(...)`) comes right after
|
|
133
|
+
# __all__ (TradingView/TechnicalRating)
|
|
134
|
+
shadowing = [node for node in rest if isinstance(node, ast.Assign) and isinstance(node.value, ast.Call)
|
|
135
|
+
and isinstance(node.value.func, ast.Name) and node.value.func.id == 'shadowed_namespace']
|
|
136
|
+
constants = [node for node in rest if isinstance(node, (ast.Assign, ast.AnnAssign)) and node not in shadowing]
|
|
137
|
+
tail = [node for node in rest if node not in classes and node not in constants and node not in shadowing]
|
|
138
|
+
classes.sort(key=lambda node: _source_position(node, context))
|
|
139
|
+
if protocols:
|
|
140
|
+
typing = ast.ImportFrom(module='typing', names=[ast.alias(name='Protocol'), ast.alias(name='Any')],
|
|
141
|
+
level=0)
|
|
142
|
+
imports.append(typing)
|
|
143
|
+
for node in [names, *protocols, *declarations, *imports]:
|
|
144
|
+
for child in ast.walk(node): # new nodes have no source position: line 0 maps to no Pine line
|
|
145
|
+
for attribute in ('lineno', 'col_offset', 'end_lineno', 'end_col_offset'):
|
|
146
|
+
if not hasattr(child, attribute) and attribute in child._attributes:
|
|
147
|
+
setattr(child, attribute, 0)
|
|
148
|
+
tree.body[:] = docstring + imports + ([names] if public else []) + shadowing + classes + protocols \
|
|
149
|
+
+ declarations + constants + tail
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def _classes_before_constants(tree: ast.Module, context: PassContext) -> None:
|
|
153
|
+
"""A script's enums and UDTs, in source order, come before its module constants
|
|
154
|
+
(probe enum1, PyneComp v6.0.80)."""
|
|
155
|
+
classes = [node for node in tree.body if isinstance(node, ast.ClassDef)]
|
|
156
|
+
if not classes:
|
|
157
|
+
return
|
|
158
|
+
main = main_function(tree)
|
|
159
|
+
before_main = tree.body[:tree.body.index(main)] if main in tree.body else list(tree.body)
|
|
160
|
+
movable = [node for node in before_main if isinstance(node, (ast.Assign, ast.AnnAssign))
|
|
161
|
+
and not (isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name)
|
|
162
|
+
and node.value.func.id == 'shadowed_namespace')]
|
|
163
|
+
moved = set(map(id, classes + movable))
|
|
164
|
+
position = min(index for index, node in enumerate(tree.body) if id(node) in moved)
|
|
165
|
+
classes.sort(key=lambda node: _source_position(node, context))
|
|
166
|
+
rest = [node for node in tree.body if id(node) not in moved]
|
|
167
|
+
rest[position:position] = classes + movable
|
|
168
|
+
tree.body[:] = rest
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def _declared_parameters(context: PassContext, count: int) -> list[tuple[set[str], set[int]]]:
|
|
172
|
+
"""Per exported function, in order: its parameters declared `series T`, and the positions
|
|
173
|
+
of the parameters the source gives no default (Python's `na` there is PyneComp's `...`)."""
|
|
174
|
+
empty = [(set(), set()) for _ in range(count)]
|
|
175
|
+
script = context.unit.pine_script
|
|
176
|
+
if script is None:
|
|
177
|
+
return empty
|
|
178
|
+
from ....ast_nodes import FuncDecl
|
|
179
|
+
functions = sorted((node for node in list(script.declarations) + list(script.body)
|
|
180
|
+
if isinstance(node, FuncDecl) and node.is_export), key=lambda node: node.line)
|
|
181
|
+
found = [({parameter.name for parameter in function.params if parameter.is_series},
|
|
182
|
+
{position for position, parameter in enumerate(function.params) if parameter.default is None})
|
|
183
|
+
for function in functions]
|
|
184
|
+
return (found + empty)[:count]
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def _protocol(name: str, function: ast.FunctionDef, declared_series: set[str],
|
|
188
|
+
undefaulted: set[int]) -> ast.ClassDef:
|
|
189
|
+
arguments = copy.deepcopy(function.args)
|
|
190
|
+
arguments.defaults = list(function.args.defaults) # the same nodes: a default keeps its spelling
|
|
191
|
+
for argument in arguments.args:
|
|
192
|
+
annotation = argument.annotation
|
|
193
|
+
if (isinstance(annotation, ast.Subscript) and isinstance(annotation.value, ast.Name)
|
|
194
|
+
and annotation.value.id == 'Series' and _pine_name(argument.arg) not in declared_series):
|
|
195
|
+
argument.annotation = annotation.slice # the Series of a history read, not a declared one
|
|
196
|
+
first_default = len(arguments.args) - len(arguments.defaults)
|
|
197
|
+
arguments.defaults = [ast.Constant(...) if first_default + offset in undefaulted
|
|
198
|
+
and isinstance(default, ast.Name) and default.id == 'na' else default
|
|
199
|
+
for offset, default in enumerate(arguments.defaults)]
|
|
200
|
+
arguments.args.insert(0, ast.arg(arg='self'))
|
|
201
|
+
call = ast.FunctionDef(name='__call__', args=arguments, body=[ast.Expr(value=ast.Constant(...))],
|
|
202
|
+
decorator_list=[], returns=ast.Name(id='Any', ctx=ast.Load()), type_params=[])
|
|
203
|
+
call.protocol_call = True # printed `def __call__(self, <parameters>) -> Any: ...`, `self, ` even alone
|
|
204
|
+
return ast.ClassDef(name=name, bases=[ast.Name(id='Protocol', ctx=ast.Load())], keywords=[],
|
|
205
|
+
body=[call], decorator_list=[], type_params=[])
|