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
@@ -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=[])