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
pine2pyne/cli.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for pine2pyne transpiler.
|
|
3
|
+
"""
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable, List, Optional
|
|
9
|
+
from . import transpile, __version__, _is_comment_only
|
|
10
|
+
from .core.contracts import OutputProfile
|
|
11
|
+
from .errors import Pine2PyneError, SemanticError, SemanticWarning
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
|
|
15
|
+
"""Parse command-line arguments."""
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog='pine2pyne',
|
|
18
|
+
description='Transpile Pine Script v6 to PyneCore Python',
|
|
19
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
20
|
+
epilog='''
|
|
21
|
+
Examples:
|
|
22
|
+
# Output to stdout
|
|
23
|
+
python -m pine2pyne strategy.pine
|
|
24
|
+
|
|
25
|
+
# Output to file
|
|
26
|
+
python -m pine2pyne strategy.pine -o strategy.py
|
|
27
|
+
|
|
28
|
+
# Validate syntax only
|
|
29
|
+
python -m pine2pyne strategy.pine --validate
|
|
30
|
+
|
|
31
|
+
# Batch convert multiple files
|
|
32
|
+
python -m pine2pyne strategy/*.pine -o output/
|
|
33
|
+
'''
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
'input',
|
|
38
|
+
nargs='+',
|
|
39
|
+
type=str,
|
|
40
|
+
help='Input Pine Script file(s) (.pine)'
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
'-o', '--output',
|
|
45
|
+
type=str,
|
|
46
|
+
help='Output file or directory. If directory, outputs will have .py extension'
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
'--validate',
|
|
51
|
+
action='store_true',
|
|
52
|
+
help='Validate syntax only (parse without generating code)'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
'--profile',
|
|
57
|
+
choices=[profile.value for profile in OutputProfile],
|
|
58
|
+
default=OutputProfile.PYNECOMP.value,
|
|
59
|
+
help='Output layout: the closed PyneComp compiler\'s (pynecomp, default) or the historical one (legacy)'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
'-v', '--verbose',
|
|
64
|
+
action='store_true',
|
|
65
|
+
help='Enable verbose output'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
'--version',
|
|
70
|
+
action='version',
|
|
71
|
+
version=f'%(prog)s {__version__}'
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return parser.parse_args(args)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def reporting_semantic_warnings(input_path: Path, action: Callable[[], object]):
|
|
78
|
+
"""Run `action`, printing each SemanticWarning it raises as
|
|
79
|
+
`⚠ <file>:<line>:<column>: <message> [<code>]` (card #28). The warning
|
|
80
|
+
filters stay the user's: a fresh catch_warnings resets Python's
|
|
81
|
+
once-per-location record, so every file reports its own warnings, and
|
|
82
|
+
`-W ignore::pine2pyne.errors.SemanticWarning` silences them. A filter that
|
|
83
|
+
makes one an error turns it into a refusal with its code. Other warnings
|
|
84
|
+
are re-issued unchanged once recording has stopped."""
|
|
85
|
+
try:
|
|
86
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
87
|
+
return action()
|
|
88
|
+
except SemanticWarning as escalated:
|
|
89
|
+
raise SemanticError(escalated.code, f'{escalated.text} (a warning, made an error by the warning filters)',
|
|
90
|
+
escalated.line, escalated.column) from escalated
|
|
91
|
+
finally:
|
|
92
|
+
for record in caught:
|
|
93
|
+
if isinstance(record.message, SemanticWarning):
|
|
94
|
+
print(f'⚠ {input_path}:{record.message}', file=sys.stderr)
|
|
95
|
+
else:
|
|
96
|
+
warnings.warn_explicit(record.message, record.category, record.filename, record.lineno)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def transpile_file(input_path: Path, output_path: Optional[Path], validate_only: bool, verbose: bool,
|
|
100
|
+
profile: Optional[str] = None) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Transpile a single Pine Script file in output ``profile`` (default: legacy).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if successful, False otherwise
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# Read input file
|
|
109
|
+
if verbose:
|
|
110
|
+
print(f'Reading {input_path}...')
|
|
111
|
+
|
|
112
|
+
source = input_path.read_text(encoding='utf-8')
|
|
113
|
+
|
|
114
|
+
if validate_only:
|
|
115
|
+
# Only lex and parse to validate syntax, with transpile()'s repairs (card #55),
|
|
116
|
+
# and refuse what transpile() refuses (card #28)
|
|
117
|
+
from .di import get_transpiler
|
|
118
|
+
|
|
119
|
+
if verbose:
|
|
120
|
+
print('Tokenizing...')
|
|
121
|
+
print('Parsing...')
|
|
122
|
+
reporting_semantic_warnings(input_path, lambda: get_transpiler(profile).validate(source))
|
|
123
|
+
|
|
124
|
+
print(f'✓ {input_path} - Valid syntax')
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
# Full transpilation
|
|
128
|
+
if verbose:
|
|
129
|
+
print('Transpiling...')
|
|
130
|
+
|
|
131
|
+
if profile is None or OutputProfile.parse(profile) is OutputProfile.PYNECOMP:
|
|
132
|
+
output_code = reporting_semantic_warnings(input_path, lambda: transpile(source))
|
|
133
|
+
else:
|
|
134
|
+
output_code = reporting_semantic_warnings(input_path, lambda: transpile(source, profile=profile))
|
|
135
|
+
|
|
136
|
+
# Empty output is only legitimate for a comment-only / empty source.
|
|
137
|
+
# If the source carries real Pine code but the transpiler produced
|
|
138
|
+
# nothing, an unsupported construct was silently dropped — fail loudly
|
|
139
|
+
# instead of reporting a bogus success (exit 0 with no file written).
|
|
140
|
+
if not output_code:
|
|
141
|
+
if _is_comment_only(source):
|
|
142
|
+
if verbose:
|
|
143
|
+
print(f'Skipping {input_path} (comment-only file)')
|
|
144
|
+
return True
|
|
145
|
+
print(f'✗ {input_path}: transpiler produced no output for non-comment '
|
|
146
|
+
f'source (unsupported construct silently dropped)', file=sys.stderr)
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
# Write output
|
|
150
|
+
if output_path:
|
|
151
|
+
if verbose:
|
|
152
|
+
print(f'Writing to {output_path}...')
|
|
153
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
154
|
+
output_path.write_text(output_code, encoding='utf-8')
|
|
155
|
+
print(f'✓ {input_path} → {output_path}')
|
|
156
|
+
else:
|
|
157
|
+
# Output to stdout
|
|
158
|
+
print(output_code)
|
|
159
|
+
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
except Pine2PyneError as e:
|
|
163
|
+
print(f'✗ {input_path}: {e}', file=sys.stderr)
|
|
164
|
+
# A repaired source that still fails: the first error above, then where it stopped (card #55)
|
|
165
|
+
cause = e.__cause__
|
|
166
|
+
while isinstance(cause, Pine2PyneError):
|
|
167
|
+
print(f' after repairing it as valid Pine, the source stops at: {cause}', file=sys.stderr)
|
|
168
|
+
cause = cause.__cause__
|
|
169
|
+
return False
|
|
170
|
+
except FileNotFoundError:
|
|
171
|
+
print(f'✗ {input_path}: File not found', file=sys.stderr)
|
|
172
|
+
return False
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f'✗ {input_path}: Unexpected error: {e}', file=sys.stderr)
|
|
175
|
+
if verbose:
|
|
176
|
+
import traceback
|
|
177
|
+
traceback.print_exc()
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
182
|
+
"""Main entry point for CLI."""
|
|
183
|
+
args = parse_args(argv)
|
|
184
|
+
|
|
185
|
+
# Resolve input files
|
|
186
|
+
input_paths: List[Path] = []
|
|
187
|
+
for input_pattern in args.input:
|
|
188
|
+
path = Path(input_pattern)
|
|
189
|
+
if path.is_file():
|
|
190
|
+
input_paths.append(path)
|
|
191
|
+
elif '*' in input_pattern or '?' in input_pattern:
|
|
192
|
+
# Glob pattern
|
|
193
|
+
from glob import glob
|
|
194
|
+
for match in glob(input_pattern):
|
|
195
|
+
input_paths.append(Path(match))
|
|
196
|
+
else:
|
|
197
|
+
print(f'Error: {input_pattern} is not a file or valid pattern', file=sys.stderr)
|
|
198
|
+
return 1
|
|
199
|
+
|
|
200
|
+
if not input_paths:
|
|
201
|
+
print('Error: No input files found', file=sys.stderr)
|
|
202
|
+
return 1
|
|
203
|
+
|
|
204
|
+
# Determine output strategy
|
|
205
|
+
output_is_dir = False
|
|
206
|
+
output_base = None
|
|
207
|
+
|
|
208
|
+
if args.output:
|
|
209
|
+
output_path = Path(args.output)
|
|
210
|
+
if output_path.is_dir() or (not output_path.exists() and len(input_paths) > 1):
|
|
211
|
+
output_is_dir = True
|
|
212
|
+
output_base = output_path
|
|
213
|
+
elif len(input_paths) > 1:
|
|
214
|
+
print('Error: Multiple input files require output to be a directory', file=sys.stderr)
|
|
215
|
+
return 1
|
|
216
|
+
else:
|
|
217
|
+
output_base = output_path
|
|
218
|
+
|
|
219
|
+
# Process files
|
|
220
|
+
success_count = 0
|
|
221
|
+
fail_count = 0
|
|
222
|
+
|
|
223
|
+
for input_path in input_paths:
|
|
224
|
+
# Determine output path
|
|
225
|
+
if args.output is None:
|
|
226
|
+
output_path = None # stdout
|
|
227
|
+
elif output_is_dir:
|
|
228
|
+
output_name = input_path.stem + '.py'
|
|
229
|
+
output_path = output_base / output_name
|
|
230
|
+
else:
|
|
231
|
+
output_path = output_base
|
|
232
|
+
|
|
233
|
+
# Transpile
|
|
234
|
+
if transpile_file(input_path, output_path, args.validate, args.verbose, profile=args.profile):
|
|
235
|
+
success_count += 1
|
|
236
|
+
else:
|
|
237
|
+
fail_count += 1
|
|
238
|
+
|
|
239
|
+
# Print summary for multiple files
|
|
240
|
+
if len(input_paths) > 1:
|
|
241
|
+
print(f'\nSummary: {success_count} succeeded, {fail_count} failed')
|
|
242
|
+
|
|
243
|
+
return 0 if fail_count == 0 else 1
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == '__main__':
|
|
247
|
+
sys.exit(main())
|