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
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())