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,215 @@
|
|
|
1
|
+
"""The PyneComp rewrites, one class per difference.
|
|
2
|
+
|
|
3
|
+
Each docstring says what PyneComp does and how often ``assets/pyne`` shows it.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from .base import PassContext, function_scopes, main_function, own_nodes, statements_in_order
|
|
11
|
+
|
|
12
|
+
_HEX_COLOR = re.compile(r'#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _double_quoted(text: str) -> str:
|
|
16
|
+
return '"' + text.replace('\\', '\\\\').replace('"', '\\"').replace('\n', '\\n') + '"'
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DeclarationCallShapePass:
|
|
20
|
+
"""``@script.indicator(...)`` keeps the call shape of the Pine declaration:
|
|
21
|
+
the arguments the source passed positionally stay positional
|
|
22
|
+
(``indicator('T', 'S', overlay=true)`` -> ``('T', 'S', overlay=True)``), a
|
|
23
|
+
title passed by keyword stays ``title=``, and a Pine v6 title keeps its quotes."""
|
|
24
|
+
|
|
25
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
26
|
+
main = main_function(tree)
|
|
27
|
+
declaration = getattr(context.unit.pine_script, 'script_decl', None)
|
|
28
|
+
if main is None or declaration is None:
|
|
29
|
+
return
|
|
30
|
+
for decorator in main.decorator_list:
|
|
31
|
+
if (isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute)
|
|
32
|
+
and isinstance(decorator.func.value, ast.Name) and decorator.func.value.id == 'script'):
|
|
33
|
+
self._reshape(decorator, declaration, context)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _reshape(call: ast.Call, declaration, context: PassContext) -> None:
|
|
37
|
+
positional = getattr(declaration, 'positional_count', 0)
|
|
38
|
+
order = tuple(getattr(declaration, 'keyword_order', ()))
|
|
39
|
+
title = call.args[0] if call.args else None
|
|
40
|
+
if title is None: # a title that is an expression: the first keyword
|
|
41
|
+
if positional:
|
|
42
|
+
call.args = [keyword.value for keyword in call.keywords[:positional]]
|
|
43
|
+
call.keywords = call.keywords[positional:]
|
|
44
|
+
return
|
|
45
|
+
if positional == 0:
|
|
46
|
+
call.args = []
|
|
47
|
+
at = order.index('title') if 'title' in order else 0
|
|
48
|
+
call.keywords = call.keywords[:at] + [ast.keyword(arg='title', value=title)] + call.keywords[at:]
|
|
49
|
+
elif positional > 1:
|
|
50
|
+
taken = call.keywords[:positional - 1]
|
|
51
|
+
call.args = [title] + [keyword.value for keyword in taken]
|
|
52
|
+
call.keywords = call.keywords[positional - 1:]
|
|
53
|
+
if (title is not None and getattr(declaration, 'title_double_quoted', False)
|
|
54
|
+
and context.pine_version >= 6 and isinstance(title, ast.Constant)):
|
|
55
|
+
context.spellings[id(title)] = _double_quoted(title.value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class StrategyDefaultsPass:
|
|
59
|
+
"""A Pine v4/v5 ``strategy()`` is compiled with its version's defaults made
|
|
60
|
+
explicit, appended after the source's own arguments in this order (every
|
|
61
|
+
v4/v5 strategy of the reference): ``margin_long=0, margin_short=0,
|
|
62
|
+
initial_capital=100000`` (v4) or ``1000000`` (v5), ``default_qty_type=strategy.fixed,
|
|
63
|
+
default_qty_value=1, close_by_id=True``. An argument the source gives keeps its value."""
|
|
64
|
+
|
|
65
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
66
|
+
if context.pine_version >= 6:
|
|
67
|
+
return
|
|
68
|
+
main = main_function(tree)
|
|
69
|
+
for decorator in main.decorator_list if main else ():
|
|
70
|
+
if (isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Attribute)
|
|
71
|
+
and decorator.func.attr == 'strategy'):
|
|
72
|
+
self._add_defaults(decorator, context.pine_version)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _add_defaults(call: ast.Call, version: int) -> None:
|
|
76
|
+
given = {keyword.arg for keyword in call.keywords}
|
|
77
|
+
defaults = [
|
|
78
|
+
('margin_long', ast.Constant(0)),
|
|
79
|
+
('margin_short', ast.Constant(0)),
|
|
80
|
+
('initial_capital', ast.Constant(100000 if version <= 4 else 1000000)),
|
|
81
|
+
('default_qty_type', ast.Attribute(value=ast.Name(id='strategy', ctx=ast.Load()), attr='fixed',
|
|
82
|
+
ctx=ast.Load())),
|
|
83
|
+
('default_qty_value', ast.Constant(1)),
|
|
84
|
+
('close_by_id', ast.Constant(True)),
|
|
85
|
+
]
|
|
86
|
+
added = [ast.keyword(arg=name, value=value) for name, value in defaults if name not in given]
|
|
87
|
+
# PyneComp reconciles the imports before it adds these: `strategy.fixed` alone imports nothing
|
|
88
|
+
for keyword in added:
|
|
89
|
+
for node in ast.walk(keyword.value):
|
|
90
|
+
node.added_default = True
|
|
91
|
+
tail = [keyword for keyword in call.keywords if keyword.arg == 'na_bool']
|
|
92
|
+
call.keywords = [keyword for keyword in call.keywords if keyword.arg != 'na_bool'] + added + tail
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class NaBoolFlagPass:
|
|
96
|
+
"""``na_bool=True`` (Pine's three-state bool) is kept for a Pine v4 script
|
|
97
|
+
(all 295 of the reference have it). A Pine v5 script keeps it only when it
|
|
98
|
+
reads a named bool where it may be na: ``b[1]`` or ``na(b)`` (probe scripts
|
|
99
|
+
compiled by PyneComp v6.0.80; 82 of the 327 v5 scripts of the reference)."""
|
|
100
|
+
|
|
101
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
102
|
+
if context.pine_version >= 6 and getattr(context.unit.transformed, 'reads_na_of_bool', False):
|
|
103
|
+
main = main_function(tree) # Pine v6: only `na()` of a bool collection marks it (probe nb6_02)
|
|
104
|
+
for decorator in main.decorator_list if main else ():
|
|
105
|
+
if isinstance(decorator, ast.Call) and not any(k.arg == 'na_bool' for k in decorator.keywords):
|
|
106
|
+
decorator.keywords.append(ast.keyword(arg='na_bool', value=ast.Constant(True)))
|
|
107
|
+
return
|
|
108
|
+
if context.pine_version != 5 or getattr(context.unit.transformed, 'reads_na_of_bool', False):
|
|
109
|
+
return
|
|
110
|
+
main = main_function(tree)
|
|
111
|
+
for decorator in main.decorator_list if main else ():
|
|
112
|
+
if isinstance(decorator, ast.Call):
|
|
113
|
+
decorator.keywords = [keyword for keyword in decorator.keywords if keyword.arg != 'na_bool']
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MathNamespacePass:
|
|
117
|
+
"""``abs``, ``max``, ``min``, ``pow`` and ``round`` are always called through
|
|
118
|
+
``math.`` (the reference never calls the Python builtins: 956 call sites)."""
|
|
119
|
+
|
|
120
|
+
NAMES = frozenset({'abs', 'max', 'min', 'pow', 'round'})
|
|
121
|
+
|
|
122
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
123
|
+
bound = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)}
|
|
124
|
+
bound |= {node.name for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.ClassDef))}
|
|
125
|
+
for node in ast.walk(tree):
|
|
126
|
+
if (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
127
|
+
and node.func.id in self.NAMES and node.func.id not in bound):
|
|
128
|
+
node.func = ast.copy_location(ast.Attribute(
|
|
129
|
+
value=ast.Name(id='math', ctx=ast.Load()), attr=node.func.id, ctx=ast.Load()), node.func)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class FlatBoolOpPass:
|
|
133
|
+
"""``a and (b and c)`` is one ``and`` of three operands: the reference has no
|
|
134
|
+
``and`` directly inside an ``and`` (nor ``or`` in ``or``), though its sources
|
|
135
|
+
write them (119 ``and (... and ...)`` groups)."""
|
|
136
|
+
|
|
137
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
138
|
+
for node in ast.walk(tree):
|
|
139
|
+
if isinstance(node, ast.BoolOp):
|
|
140
|
+
node.values = self._flat(node)
|
|
141
|
+
|
|
142
|
+
def _flat(self, node: ast.BoolOp) -> list:
|
|
143
|
+
values = []
|
|
144
|
+
for value in node.values:
|
|
145
|
+
if isinstance(value, ast.BoolOp) and type(value.op) is type(node.op):
|
|
146
|
+
values.extend(self._flat(value))
|
|
147
|
+
else:
|
|
148
|
+
values.append(value)
|
|
149
|
+
return values
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class ArithmeticReassociationPass:
|
|
153
|
+
"""Pine v4/v5: a parenthesised right operand is re-associated to the left where
|
|
154
|
+
the operators allow it: ``a + (b - c)`` is ``a + b - c`` and ``a * (b * c)`` is
|
|
155
|
+
``a * b * c``; ``a * (b / c)`` and every right operand of ``-`` or ``/`` keep
|
|
156
|
+
their parentheses. Pine v6 keeps the source's grouping. Probe scripts compiled
|
|
157
|
+
by PyneComp v6.0.80 (``tmp/probes/par4..6``)."""
|
|
158
|
+
|
|
159
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
160
|
+
if context.pine_version >= 6:
|
|
161
|
+
return
|
|
162
|
+
_Reassociation().visit(tree)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class _Reassociation(ast.NodeTransformer):
|
|
166
|
+
def visit_BinOp(self, node: ast.BinOp) -> ast.AST:
|
|
167
|
+
self.generic_visit(node)
|
|
168
|
+
return _reassociated(node)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _reassociated(node: ast.BinOp) -> ast.BinOp:
|
|
172
|
+
right = node.right
|
|
173
|
+
joins = (isinstance(node.op, ast.Add) and isinstance(right, ast.BinOp) and isinstance(right.op, (ast.Add, ast.Sub))
|
|
174
|
+
or isinstance(node.op, ast.Mult) and isinstance(right, ast.BinOp) and isinstance(right.op, ast.Mult))
|
|
175
|
+
if not joins:
|
|
176
|
+
return node
|
|
177
|
+
left = _reassociated(ast.copy_location(ast.BinOp(left=node.left, op=node.op, right=right.left), node))
|
|
178
|
+
return _reassociated(ast.copy_location(ast.BinOp(left=left, op=right.op, right=right.right), node))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class HexColorPass:
|
|
182
|
+
"""A Pine colour literal is ``color.new('#RRGGBB')``, upper case
|
|
183
|
+
(all 5410 hex colours of the reference)."""
|
|
184
|
+
|
|
185
|
+
def apply(self, tree: ast.Module, context: PassContext) -> None:
|
|
186
|
+
tree.body[:] = [self._visit(node) for node in tree.body]
|
|
187
|
+
|
|
188
|
+
def _visit(self, node):
|
|
189
|
+
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
190
|
+
return node
|
|
191
|
+
return _HexColorRewriter().visit(node)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class _HexColorRewriter(ast.NodeTransformer):
|
|
195
|
+
def visit_Constant(self, node: ast.Constant):
|
|
196
|
+
if isinstance(node.value, str) and _HEX_COLOR.fullmatch(node.value):
|
|
197
|
+
spelled = node.value.upper()
|
|
198
|
+
if len(spelled) == 9 and spelled.endswith('FF'):
|
|
199
|
+
spelled = spelled[:7] # an opaque alpha is left out: '#00FF00FF' is '#00FF00'
|
|
200
|
+
call = ast.Call(
|
|
201
|
+
func=ast.Attribute(value=ast.Name(id='color', ctx=ast.Load()), attr='new', ctx=ast.Load()),
|
|
202
|
+
args=[ast.Constant(value=spelled)], keywords=[])
|
|
203
|
+
return ast.copy_location(call, node)
|
|
204
|
+
return node
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
__all__ = [
|
|
208
|
+
'DeclarationCallShapePass',
|
|
209
|
+
'FlatBoolOpPass',
|
|
210
|
+
'MathNamespacePass',
|
|
211
|
+
'NaBoolFlagPass',
|
|
212
|
+
'StrategyDefaultsPass',
|
|
213
|
+
'HexColorPass',
|
|
214
|
+
'statements_in_order',
|
|
215
|
+
]
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Module layout of PyneComp output, printed from a Python AST.
|
|
2
|
+
|
|
3
|
+
The printer owns layout only: what a statement *is* comes from the code
|
|
4
|
+
generator; how it is spread over lines, indented and separated comes from
|
|
5
|
+
here. Expressions go through :class:`~.unparser.ExpressionPrinter`, the
|
|
6
|
+
import block through :class:`~.imports.ImportBlockFormatter`, blank lines in
|
|
7
|
+
function bodies through a :class:`~.blank_lines.BlankLinePolicy`.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Callable, Mapping
|
|
15
|
+
|
|
16
|
+
from .banner import PyneCompBanner
|
|
17
|
+
from .blank_lines import AfterDefinitionPolicy, BlankLinePolicy, SourceLayoutPolicy
|
|
18
|
+
from .imports import ImportBlockFormatter
|
|
19
|
+
from .unparser import ExpressionPrinter, header_strings, source_quoted_strings
|
|
20
|
+
|
|
21
|
+
INDENT = ' '
|
|
22
|
+
_DEFINITIONS = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class LayoutSource:
|
|
27
|
+
"""What the printer reads: the generated code and where it came from."""
|
|
28
|
+
|
|
29
|
+
python_source: str
|
|
30
|
+
pine_source: str = ''
|
|
31
|
+
pine_version: int | None = None
|
|
32
|
+
#: Python line number -> Pine line number of the statement printed there
|
|
33
|
+
origins: Mapping[int, int] = field(default_factory=dict)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def keeps_source_layout(self) -> bool:
|
|
37
|
+
"""Pine v6 keeps its blank lines and string quotes; earlier versions do not."""
|
|
38
|
+
return (self.pine_version or 6) >= 6
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _ModuleWriter:
|
|
42
|
+
"""Prints one module; created per render, so the printer stays stateless."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, expression: Callable[[ast.AST], str], blank_lines: BlankLinePolicy,
|
|
45
|
+
written_as_elif: Callable[[ast.If], bool] = lambda node: True,
|
|
46
|
+
blank_in_source: Callable[[ast.stmt], bool] = lambda node: False) -> None:
|
|
47
|
+
self._expression = expression
|
|
48
|
+
self._blank_lines = blank_lines
|
|
49
|
+
self._written_as_elif = written_as_elif
|
|
50
|
+
self._blank_in_source = blank_in_source
|
|
51
|
+
|
|
52
|
+
# -- functions ---------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def _argument(self, arg: ast.arg, default: ast.expr | None, spaced: bool = False) -> str:
|
|
55
|
+
"""`name=default` for main()'s inputs; `name = default` for a user function or an annotation."""
|
|
56
|
+
text = arg.arg
|
|
57
|
+
if arg.annotation is not None:
|
|
58
|
+
text += f': {self._expression(arg.annotation)}'
|
|
59
|
+
if default is not None:
|
|
60
|
+
text += f' = {self._expression(default)}'
|
|
61
|
+
elif default is not None:
|
|
62
|
+
text += f' = {self._expression(default)}' if spaced else f'={self._expression(default)}'
|
|
63
|
+
return text
|
|
64
|
+
|
|
65
|
+
def _arguments(self, args: ast.arguments, spaced: bool = False) -> list[str]:
|
|
66
|
+
positional = list(args.posonlyargs) + list(args.args)
|
|
67
|
+
defaults = [None] * (len(positional) - len(args.defaults)) + list(args.defaults)
|
|
68
|
+
parts = [self._argument(arg, default, spaced) for arg, default in zip(positional, defaults)]
|
|
69
|
+
if args.posonlyargs:
|
|
70
|
+
parts.insert(len(args.posonlyargs), '/')
|
|
71
|
+
if args.vararg is not None:
|
|
72
|
+
parts.append('*' + self._argument(args.vararg, None))
|
|
73
|
+
elif args.kwonlyargs:
|
|
74
|
+
parts.append('*')
|
|
75
|
+
parts.extend(self._argument(arg, default) for arg, default in zip(args.kwonlyargs, args.kw_defaults))
|
|
76
|
+
if args.kwarg is not None:
|
|
77
|
+
parts.append('**' + self._argument(args.kwarg, None))
|
|
78
|
+
return parts
|
|
79
|
+
|
|
80
|
+
def _function(self, node: ast.FunctionDef, depth: int, one_parameter_per_line: bool) -> list[str]:
|
|
81
|
+
indent = INDENT * depth
|
|
82
|
+
lines = [f'{indent}@{self._expression(decorator)}' for decorator in node.decorator_list]
|
|
83
|
+
keyword = 'async def' if isinstance(node, ast.AsyncFunctionDef) else 'def'
|
|
84
|
+
returns = f' -> {self._expression(node.returns)}' if node.returns is not None else ''
|
|
85
|
+
parameters = self._arguments(node.args, spaced=not one_parameter_per_line)
|
|
86
|
+
if one_parameter_per_line and parameters:
|
|
87
|
+
lines.append(f'{indent}{keyword} {node.name}(')
|
|
88
|
+
lines.append(',\n'.join(f'{indent}{INDENT}{parameter}' for parameter in parameters))
|
|
89
|
+
lines.append(f'{indent}){returns}:')
|
|
90
|
+
elif getattr(node, 'protocol_call', False) and parameters == ['self']:
|
|
91
|
+
lines.append(f'{indent}{keyword} {node.name}(self, ){returns}:') # PyneComp's spelling
|
|
92
|
+
else:
|
|
93
|
+
lines.append(f'{indent}{keyword} {node.name}({", ".join(parameters)}){returns}:')
|
|
94
|
+
if _is_stub(node.body):
|
|
95
|
+
lines[-1] += ' ...'
|
|
96
|
+
return lines
|
|
97
|
+
return lines + self.block(node.body, depth + 1)
|
|
98
|
+
|
|
99
|
+
def _class(self, node: ast.ClassDef, depth: int) -> list[str]:
|
|
100
|
+
indent = INDENT * depth
|
|
101
|
+
lines = [f'{indent}@{self._expression(decorator)}' for decorator in node.decorator_list]
|
|
102
|
+
bases = [self._expression(base) for base in node.bases]
|
|
103
|
+
bases += [self._expression(keyword) for keyword in node.keywords]
|
|
104
|
+
head = f'{indent}class {node.name}({", ".join(bases)}):' if bases else f'{indent}class {node.name}:'
|
|
105
|
+
body: list[str] = []
|
|
106
|
+
for index, field in enumerate(node.body):
|
|
107
|
+
if index and self._blank_in_source(field): # a Pine v6 UDT keeps the gaps between its fields
|
|
108
|
+
body.append('')
|
|
109
|
+
body += self.statement(field, depth + 1)
|
|
110
|
+
return lines + [head] + body
|
|
111
|
+
|
|
112
|
+
# -- statements --------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
def _if(self, node: ast.If, depth: int, keyword: str = 'if') -> list[str]:
|
|
115
|
+
indent = INDENT * depth
|
|
116
|
+
lines = [f'{indent}{keyword} {self._expression(node.test)}:'] + self.block(node.body, depth + 1)
|
|
117
|
+
if node.orelse:
|
|
118
|
+
if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If) and self._written_as_elif(node.orelse[0]):
|
|
119
|
+
lines += self._if(node.orelse[0], depth, 'elif')
|
|
120
|
+
else:
|
|
121
|
+
lines += [f'{indent}else:'] + self.block(node.orelse, depth + 1)
|
|
122
|
+
return lines
|
|
123
|
+
|
|
124
|
+
def _loop(self, node: ast.For | ast.While, depth: int) -> list[str]:
|
|
125
|
+
indent = INDENT * depth
|
|
126
|
+
if isinstance(node, ast.While):
|
|
127
|
+
head = f'{indent}while {self._expression(node.test)}:'
|
|
128
|
+
else:
|
|
129
|
+
target = node.target
|
|
130
|
+
spelled = (', '.join(self._expression(element) for element in target.elts)
|
|
131
|
+
if isinstance(target, ast.Tuple) and target.elts else self._expression(target))
|
|
132
|
+
head = f'{indent}for {spelled} in {self._expression(node.iter)}:'
|
|
133
|
+
lines = [head] + self.block(node.body, depth + 1)
|
|
134
|
+
if node.orelse:
|
|
135
|
+
lines += [f'{indent}else:'] + self.block(node.orelse, depth + 1)
|
|
136
|
+
return lines
|
|
137
|
+
|
|
138
|
+
def statement(self, node: ast.stmt, depth: int, one_parameter_per_line: bool = False) -> list[str]:
|
|
139
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
140
|
+
return self._function(node, depth, one_parameter_per_line)
|
|
141
|
+
if isinstance(node, ast.ClassDef):
|
|
142
|
+
return self._class(node, depth)
|
|
143
|
+
if isinstance(node, ast.If):
|
|
144
|
+
return self._if(node, depth)
|
|
145
|
+
if isinstance(node, (ast.For, ast.While)):
|
|
146
|
+
return self._loop(node, depth)
|
|
147
|
+
indent = INDENT * depth
|
|
148
|
+
if isinstance(node, (ast.Try, ast.With, ast.Match, ast.AsyncFor, ast.AsyncWith)):
|
|
149
|
+
return [indent + line for line in ast.unparse(node).split('\n')] # PyneComp does not emit these
|
|
150
|
+
return [indent + self._expression(node)]
|
|
151
|
+
|
|
152
|
+
def block(self, body: list[ast.stmt], depth: int) -> list[str]:
|
|
153
|
+
lines: list[str] = []
|
|
154
|
+
previous: ast.stmt | None = None
|
|
155
|
+
for node in body:
|
|
156
|
+
if previous is not None:
|
|
157
|
+
lines += [''] * self._blank_lines.blank_lines_before(previous, node)
|
|
158
|
+
lines += self.statement(node, depth)
|
|
159
|
+
previous = node
|
|
160
|
+
return lines
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _is_stub(body: list[ast.stmt]) -> bool:
|
|
164
|
+
return (len(body) == 1 and isinstance(body[0], ast.Expr)
|
|
165
|
+
and isinstance(body[0].value, ast.Constant) and body[0].value.value is ...)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _is_docstring(node: ast.stmt) -> bool:
|
|
169
|
+
return isinstance(node, ast.Expr) and isinstance(node.value, ast.Constant) and isinstance(node.value.value, str)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _is_main_guard(node: ast.stmt) -> bool:
|
|
173
|
+
return (isinstance(node, ast.If) and isinstance(node.test, ast.Compare)
|
|
174
|
+
and isinstance(node.test.left, ast.Name) and node.test.left.id == '__name__')
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _binds_shadowed_namespace(node: ast.stmt) -> bool:
|
|
178
|
+
return (isinstance(node, ast.Assign) and isinstance(node.value, ast.Call)
|
|
179
|
+
and isinstance(node.value.func, ast.Name) and node.value.func.id == 'shadowed_namespace')
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _declares_an_export(node: ast.stmt | None) -> bool:
|
|
183
|
+
"""`name: _ProtocolName = Exported()`."""
|
|
184
|
+
value = getattr(node, 'value', None)
|
|
185
|
+
return isinstance(value, ast.Call) and isinstance(value.func, ast.Name) and value.func.id == 'Exported'
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _assigned_names(node: ast.stmt) -> set[str]:
|
|
189
|
+
targets = node.targets if isinstance(node, ast.Assign) else [getattr(node, 'target', None)]
|
|
190
|
+
return {target.id for target in targets if isinstance(target, ast.Name)}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _moved_lines(source: LayoutSource, tree: ast.Module | None) -> list[int]:
|
|
194
|
+
"""The Pine lines of the statements moved out of main(): the module-level assignments and the
|
|
195
|
+
enums and UDTs (found by their declaration in the Pine source)."""
|
|
196
|
+
if tree is None:
|
|
197
|
+
return []
|
|
198
|
+
lines = [source.origins.get(node.lineno, 0) for node in tree.body
|
|
199
|
+
if isinstance(node, (ast.Assign, ast.AnnAssign)) and source.origins.get(node.lineno, 0)]
|
|
200
|
+
for node in tree.body:
|
|
201
|
+
if isinstance(node, ast.ClassDef):
|
|
202
|
+
pine_name = re.sub(r'__ren__$', '', node.name)
|
|
203
|
+
match = re.search(rf'^(?:export\s+)?(?:type|enum)\s+{re.escape(pine_name)}\b', source.pine_source, re.M)
|
|
204
|
+
if match:
|
|
205
|
+
lines.append(source.pine_source.count('\n', 0, match.start()) + 1)
|
|
206
|
+
return lines
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class PyneCompModulePrinter:
|
|
210
|
+
"""Prints a generated module in PyneComp's layout."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, banner: PyneCompBanner | None = None, imports: ImportBlockFormatter | None = None) -> None:
|
|
213
|
+
self._banner = banner or PyneCompBanner()
|
|
214
|
+
self._imports = imports or ImportBlockFormatter()
|
|
215
|
+
|
|
216
|
+
def blank_line_policy(self, source: LayoutSource, tree: ast.Module | None = None) -> BlankLinePolicy:
|
|
217
|
+
if source.keeps_source_layout:
|
|
218
|
+
return SourceLayoutPolicy(source.origins, source.pine_source.split('\n'), _moved_lines(source, tree))
|
|
219
|
+
return AfterDefinitionPolicy()
|
|
220
|
+
|
|
221
|
+
_declares_types = False # set per render: the module has a UDT or an enum
|
|
222
|
+
|
|
223
|
+
def _module_gap(self, previous: ast.stmt | None, current: ast.stmt) -> int:
|
|
224
|
+
"""Blank lines between two module-level statements."""
|
|
225
|
+
if _binds_shadowed_namespace(current):
|
|
226
|
+
return 2 if previous is None or not _binds_shadowed_namespace(previous) else 0
|
|
227
|
+
if previous is not None and _binds_shadowed_namespace(previous) and not isinstance(current, _DEFINITIONS):
|
|
228
|
+
return 1
|
|
229
|
+
if previous is None: # right after the import block
|
|
230
|
+
if isinstance(current, _DEFINITIONS) or '__all__' in _assigned_names(current):
|
|
231
|
+
return 2
|
|
232
|
+
return 1
|
|
233
|
+
if isinstance(previous, _DEFINITIONS) or isinstance(current, _DEFINITIONS):
|
|
234
|
+
return 2
|
|
235
|
+
if '__all__' in _assigned_names(previous) or '__all__' in _assigned_names(current):
|
|
236
|
+
return 2
|
|
237
|
+
if _declares_an_export(previous) != _declares_an_export(current):
|
|
238
|
+
# the Exported() declarations are one group; a constant after them is one line away, two in a
|
|
239
|
+
# library that declares a UDT or an enum (probes const2, ex1..ex8; 16 of 16 libraries)
|
|
240
|
+
if _declares_an_export(previous):
|
|
241
|
+
return 2 if self._declares_types else 1
|
|
242
|
+
return 2
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
def render(self, source: LayoutSource, tree: ast.Module | None = None,
|
|
246
|
+
spellings: Mapping[int, str] | None = None) -> str:
|
|
247
|
+
"""Print ``tree`` (default: ``source.python_source`` parsed) in PyneComp's layout.
|
|
248
|
+
|
|
249
|
+
``tree`` may be a rewritten parse of ``source.python_source``: nodes that
|
|
250
|
+
kept their positions still find their source spelling and Pine line.
|
|
251
|
+
"""
|
|
252
|
+
if tree is None:
|
|
253
|
+
tree = ast.parse(source.python_source)
|
|
254
|
+
source_quoted = source_quoted_strings(tree) if source.keeps_source_layout else frozenset()
|
|
255
|
+
single_quoted = frozenset() if source.keeps_source_layout else header_strings(tree)
|
|
256
|
+
# the strings PyneComp writes as the source has them keep their non-printable characters raw
|
|
257
|
+
raw_strings = source_quoted if source.keeps_source_layout else source_quoted_strings(tree)
|
|
258
|
+
expression = ExpressionPrinter(source.python_source, source_quoted, spellings, single_quoted, raw_strings)
|
|
259
|
+
source_lines = source.python_source.split('\n')
|
|
260
|
+
|
|
261
|
+
def written_as_elif(node: ast.If) -> bool:
|
|
262
|
+
"""Pine `else if` is `elif`; an `if` nested in an `else` block stays nested."""
|
|
263
|
+
lineno = getattr(node, 'lineno', 0)
|
|
264
|
+
return not lineno or source_lines[lineno - 1].lstrip().startswith('elif ')
|
|
265
|
+
|
|
266
|
+
def blank_in_source(node: ast.stmt) -> bool:
|
|
267
|
+
"""The generated source has a blank line just above `node` (a gap between two UDT fields)."""
|
|
268
|
+
lineno = getattr(node, 'lineno', 0)
|
|
269
|
+
return lineno > 1 and not source_lines[lineno - 2].strip()
|
|
270
|
+
|
|
271
|
+
writer = _ModuleWriter(expression, self.blank_line_policy(source, tree), written_as_elif, blank_in_source)
|
|
272
|
+
|
|
273
|
+
self._declares_types = any(
|
|
274
|
+
isinstance(node, ast.ClassDef) and not any(isinstance(base, ast.Name) and base.id == 'Protocol'
|
|
275
|
+
for base in node.bases) for node in tree.body)
|
|
276
|
+
body = [node for node in tree.body if not _is_docstring(node) and not _is_main_guard(node)]
|
|
277
|
+
import_nodes = [node for node in body if isinstance(node, (ast.Import, ast.ImportFrom))]
|
|
278
|
+
others = [node for node in body if not isinstance(node, (ast.Import, ast.ImportFrom))]
|
|
279
|
+
|
|
280
|
+
lines = self._banner.header() + self._imports.format(import_nodes)
|
|
281
|
+
previous: ast.stmt | None = None
|
|
282
|
+
for node in others:
|
|
283
|
+
lines += [''] * self._module_gap(previous, node)
|
|
284
|
+
is_main = isinstance(node, ast.FunctionDef) and node.name == 'main'
|
|
285
|
+
lines += writer.statement(node, 0, one_parameter_per_line=is_main)
|
|
286
|
+
previous = node
|
|
287
|
+
lines += ['', ''] + self._banner.footer()
|
|
288
|
+
return '\n'.join(lines) + '\n'
|