opencode-pyneruntime 6.6.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.
- opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
- opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
- opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
- opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
- opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
- opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
- pynecore/__init__.py +6 -0
- pynecore/cli/__init__.py +2 -0
- pynecore/cli/app.py +238 -0
- pynecore/cli/commands/__init__.py +343 -0
- pynecore/cli/commands/benchmark.py +186 -0
- pynecore/cli/commands/compile.py +198 -0
- pynecore/cli/commands/data.py +857 -0
- pynecore/cli/commands/debug.py +63 -0
- pynecore/cli/commands/optimize.py +956 -0
- pynecore/cli/commands/plugin.py +242 -0
- pynecore/cli/commands/run.py +2006 -0
- pynecore/cli/pluggable.py +132 -0
- pynecore/cli/utils/__init__.py +0 -0
- pynecore/cli/utils/api_error_handler.py +168 -0
- pynecore/cli/utils/broker_picker.py +330 -0
- pynecore/cli/utils/error_hook.py +28 -0
- pynecore/cli/utils/keyreader.py +178 -0
- pynecore/cli/utils/provider_picker.py +19 -0
- pynecore/cli/utils/symbol_browser.py +1149 -0
- pynecore/core/__init__.py +0 -0
- pynecore/core/aggregator.py +257 -0
- pynecore/core/bar_magnifier.py +168 -0
- pynecore/core/broker/__init__.py +64 -0
- pynecore/core/broker/defaults.py +113 -0
- pynecore/core/broker/disappearance.py +927 -0
- pynecore/core/broker/emulator.py +345 -0
- pynecore/core/broker/exceptions.py +346 -0
- pynecore/core/broker/idempotency.py +401 -0
- pynecore/core/broker/intent_builder.py +334 -0
- pynecore/core/broker/journal.py +1785 -0
- pynecore/core/broker/models.py +1600 -0
- pynecore/core/broker/native_failsafe_manager.py +1436 -0
- pynecore/core/broker/one_way_emulator.py +1128 -0
- pynecore/core/broker/position.py +787 -0
- pynecore/core/broker/run_identity.py +126 -0
- pynecore/core/broker/software_entry_stop_engine.py +351 -0
- pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
- pynecore/core/broker/spot_inventory.py +1327 -0
- pynecore/core/broker/storage.py +2655 -0
- pynecore/core/broker/store_helpers.py +2161 -0
- pynecore/core/broker/sync_engine.py +16070 -0
- pynecore/core/broker/validation.py +382 -0
- pynecore/core/class_property.py +7 -0
- pynecore/core/config.py +392 -0
- pynecore/core/csv_file.py +547 -0
- pynecore/core/currency.py +262 -0
- pynecore/core/data_converter.py +1002 -0
- pynecore/core/datetime.py +296 -0
- pynecore/core/download_info.py +71 -0
- pynecore/core/download_runner.py +274 -0
- pynecore/core/htf_aggregator.py +181 -0
- pynecore/core/import_hook.py +358 -0
- pynecore/core/instance_state.py +494 -0
- pynecore/core/live_ltf_collector.py +442 -0
- pynecore/core/live_ltf_window.py +189 -0
- pynecore/core/live_runner.py +1347 -0
- pynecore/core/module_property.py +26 -0
- pynecore/core/ohlcv_file.py +1888 -0
- pynecore/core/overload.py +371 -0
- pynecore/core/pine_cast.py +113 -0
- pynecore/core/pine_export.py +95 -0
- pynecore/core/pine_method.py +244 -0
- pynecore/core/pine_range.py +86 -0
- pynecore/core/pine_udt.py +69 -0
- pynecore/core/plugin/__init__.py +394 -0
- pynecore/core/plugin/broker.py +781 -0
- pynecore/core/plugin/cli.py +96 -0
- pynecore/core/plugin/live_provider.py +208 -0
- pynecore/core/plugin/provider.py +331 -0
- pynecore/core/provider_string.py +148 -0
- pynecore/core/random.py +40 -0
- pynecore/core/resampler.py +686 -0
- pynecore/core/safe_convert.py +64 -0
- pynecore/core/script.py +1011 -0
- pynecore/core/script_runner.py +3202 -0
- pynecore/core/security.py +1749 -0
- pynecore/core/security_process.py +1253 -0
- pynecore/core/security_shm.py +456 -0
- pynecore/core/series.py +417 -0
- pynecore/core/strategy_stats.py +669 -0
- pynecore/core/symbol_map.py +134 -0
- pynecore/core/syminfo.py +505 -0
- pynecore/core/viz.py +591 -0
- pynecore/lib/__init__.py +1771 -0
- pynecore/lib/_fixnan.py +32 -0
- pynecore/lib/_math_stateful.py +202 -0
- pynecore/lib/_timeframe_change.py +101 -0
- pynecore/lib/adjustment.py +6 -0
- pynecore/lib/alert.py +39 -0
- pynecore/lib/alert.pyi +14 -0
- pynecore/lib/array.py +1051 -0
- pynecore/lib/barmerge.py +60 -0
- pynecore/lib/barstate.py +30 -0
- pynecore/lib/box.py +415 -0
- pynecore/lib/chart.py +128 -0
- pynecore/lib/color.py +152 -0
- pynecore/lib/color.pyi +50 -0
- pynecore/lib/currency.py +62 -0
- pynecore/lib/dayofweek.py +36 -0
- pynecore/lib/dayofweek.pyi +18 -0
- pynecore/lib/display.py +8 -0
- pynecore/lib/dividends.py +9 -0
- pynecore/lib/earnings.py +11 -0
- pynecore/lib/extend.py +6 -0
- pynecore/lib/font.py +5 -0
- pynecore/lib/footprint.py +79 -0
- pynecore/lib/format.py +11 -0
- pynecore/lib/hline.py +67 -0
- pynecore/lib/hline.pyi +24 -0
- pynecore/lib/label.py +409 -0
- pynecore/lib/line.py +433 -0
- pynecore/lib/linefill.py +93 -0
- pynecore/lib/location.py +11 -0
- pynecore/lib/log.py +362 -0
- pynecore/lib/map.py +150 -0
- pynecore/lib/math.py +385 -0
- pynecore/lib/matrix.py +708 -0
- pynecore/lib/order.py +8 -0
- pynecore/lib/pivotpointtype.py +8 -0
- pynecore/lib/plot.py +95 -0
- pynecore/lib/plot.pyi +33 -0
- pynecore/lib/polyline.py +91 -0
- pynecore/lib/position.py +15 -0
- pynecore/lib/request.py +281 -0
- pynecore/lib/runtime.py +5 -0
- pynecore/lib/scale.py +9 -0
- pynecore/lib/session.py +267 -0
- pynecore/lib/session.pyi +12 -0
- pynecore/lib/shape.py +18 -0
- pynecore/lib/size.py +12 -0
- pynecore/lib/splits.py +4 -0
- pynecore/lib/strategy/__init__.py +4778 -0
- pynecore/lib/strategy/closedtrades.py +347 -0
- pynecore/lib/strategy/closedtrades.pyi +53 -0
- pynecore/lib/strategy/commission.py +9 -0
- pynecore/lib/strategy/direction.py +9 -0
- pynecore/lib/strategy/oca.py +13 -0
- pynecore/lib/strategy/opentrades.py +281 -0
- pynecore/lib/strategy/opentrades.pyi +49 -0
- pynecore/lib/strategy/risk.py +109 -0
- pynecore/lib/string.py +649 -0
- pynecore/lib/syminfo.py +84 -0
- pynecore/lib/ta.py +2230 -0
- pynecore/lib/table.py +290 -0
- pynecore/lib/text.py +17 -0
- pynecore/lib/ticker.py +207 -0
- pynecore/lib/timeframe.py +293 -0
- pynecore/lib/volume_row.py +67 -0
- pynecore/lib/xloc.py +4 -0
- pynecore/lib/yloc.py +5 -0
- pynecore/providers/__init__.py +0 -0
- pynecore/providers/ccxt.py +664 -0
- pynecore/providers/replay.py +187 -0
- pynecore/pynesys/__init__.py +0 -0
- pynecore/pynesys/api.py +498 -0
- pynecore/pynesys/compiler.py +112 -0
- pynecore/standalone.py +99 -0
- pynecore/testing/__init__.py +1 -0
- pynecore/testing/broker_lab/__init__.py +41 -0
- pynecore/testing/broker_lab/__main__.py +5 -0
- pynecore/testing/broker_lab/cli.py +87 -0
- pynecore/testing/broker_lab/generate.py +47 -0
- pynecore/testing/broker_lab/model.py +84 -0
- pynecore/testing/broker_lab/reference.py +645 -0
- pynecore/testing/broker_lab/runner.py +372 -0
- pynecore/testing/broker_lab/scheduler.py +50 -0
- pynecore/testing/broker_lab/subprocess.py +73 -0
- pynecore/transformers/__init__.py +0 -0
- pynecore/transformers/builtin_shadow.py +136 -0
- pynecore/transformers/closure_arguments_transformer.py +428 -0
- pynecore/transformers/display_rewrite.py +140 -0
- pynecore/transformers/dynamic_default.py +147 -0
- pynecore/transformers/function_isolation.py +757 -0
- pynecore/transformers/import_lifter.py +61 -0
- pynecore/transformers/import_normalizer.py +328 -0
- pynecore/transformers/inline_series_hoist.py +178 -0
- pynecore/transformers/input_transformer.py +175 -0
- pynecore/transformers/lib_series.py +201 -0
- pynecore/transformers/locations.py +70 -0
- pynecore/transformers/module_properties.json +3387 -0
- pynecore/transformers/module_property.py +221 -0
- pynecore/transformers/ne_guard.py +70 -0
- pynecore/transformers/persistent.py +320 -0
- pynecore/transformers/persistent_series.py +76 -0
- pynecore/transformers/safe_convert_transformer.py +97 -0
- pynecore/transformers/safe_division_transformer.py +95 -0
- pynecore/transformers/script_requirements.py +308 -0
- pynecore/transformers/security.py +752 -0
- pynecore/transformers/security_instantiation.py +274 -0
- pynecore/transformers/series.py +275 -0
- pynecore/transformers/slot_layout.py +381 -0
- pynecore/transformers/type_checking_stripper.py +25 -0
- pynecore/transformers/unused_series_detector.py +267 -0
- pynecore/types/__init__.py +21 -0
- pynecore/types/alert.py +5 -0
- pynecore/types/barmerge.py +5 -0
- pynecore/types/base.py +39 -0
- pynecore/types/box.py +37 -0
- pynecore/types/chart.py +17 -0
- pynecore/types/color.py +107 -0
- pynecore/types/currency.py +5 -0
- pynecore/types/datetime.py +6 -0
- pynecore/types/display.py +5 -0
- pynecore/types/dividends.py +5 -0
- pynecore/types/earnings.py +5 -0
- pynecore/types/extend.py +5 -0
- pynecore/types/font.py +5 -0
- pynecore/types/footprint.py +41 -0
- pynecore/types/format.py +5 -0
- pynecore/types/hline.py +24 -0
- pynecore/types/ib_persistent.py +8 -0
- pynecore/types/ib_persistent.pyi +10 -0
- pynecore/types/label.py +35 -0
- pynecore/types/line.py +32 -0
- pynecore/types/linefill.py +13 -0
- pynecore/types/location.py +5 -0
- pynecore/types/matrix.py +999 -0
- pynecore/types/na.py +237 -0
- pynecore/types/na.pyi +83 -0
- pynecore/types/ohlcv.py +12 -0
- pynecore/types/order.py +5 -0
- pynecore/types/persistent.py +8 -0
- pynecore/types/persistent.pyi +13 -0
- pynecore/types/pine_types.py +11 -0
- pynecore/types/pine_types.pyi +15 -0
- pynecore/types/pivotpointtype.py +5 -0
- pynecore/types/plot.py +12 -0
- pynecore/types/plot_meta.py +60 -0
- pynecore/types/polyline.py +40 -0
- pynecore/types/position.py +5 -0
- pynecore/types/scale.py +5 -0
- pynecore/types/script_type.py +15 -0
- pynecore/types/series.py +23 -0
- pynecore/types/series.pyi +19 -0
- pynecore/types/session.py +35 -0
- pynecore/types/shape.py +5 -0
- pynecore/types/size.py +5 -0
- pynecore/types/source.py +33 -0
- pynecore/types/splits.py +5 -0
- pynecore/types/strategy.py +45 -0
- pynecore/types/table.py +87 -0
- pynecore/types/text.py +13 -0
- pynecore/types/type_checker.py +7 -0
- pynecore/types/type_checker.pyi +48 -0
- pynecore/types/volume_row.py +36 -0
- pynecore/types/weekdays.py +11 -0
- pynecore/types/xloc.py +5 -0
- pynecore/types/yloc.py +5 -0
- pynecore/utils/__init__.py +0 -0
- pynecore/utils/file_utils.py +50 -0
- pynecore/utils/rich/__init__.py +0 -0
- pynecore/utils/rich/date_column.py +25 -0
- pynecore/utils/sequence_view.py +92 -0
- pynecore/utils/stdlib_checker.py +17 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
import ast
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InputTransformer(ast.NodeTransformer):
|
|
6
|
+
"""
|
|
7
|
+
Transform input function calls:
|
|
8
|
+
1. Add _id parameter to input calls
|
|
9
|
+
2. Add getattr for source inputs at the start of functions
|
|
10
|
+
3. Add required imports (lib, na) if not present
|
|
11
|
+
Must be applied after SeriesTransformer.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.function_source_vars = {} # function_name -> {var_name -> source_str}
|
|
16
|
+
self.current_function = None
|
|
17
|
+
self.has_source_inputs = False
|
|
18
|
+
self.imported_names: set[str] = set() # Track what's already imported
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def _is_input_call(node: ast.Call) -> bool:
|
|
22
|
+
"""Check if node is lib.input(), lib.input.xxx(), or imported input() call"""
|
|
23
|
+
# Handle lib.input(...) pattern
|
|
24
|
+
if isinstance(node.func, ast.Attribute) and node.func.attr == 'input':
|
|
25
|
+
return isinstance(node.func.value, ast.Name) and node.func.value.id == 'lib'
|
|
26
|
+
|
|
27
|
+
# Handle lib.input.xxx(...) pattern
|
|
28
|
+
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Attribute):
|
|
29
|
+
return (isinstance(node.func.value.value, ast.Name) and
|
|
30
|
+
node.func.value.value.id == 'lib' and
|
|
31
|
+
node.func.value.attr == 'input')
|
|
32
|
+
|
|
33
|
+
# Handle imported input(...) call
|
|
34
|
+
if isinstance(node.func, ast.Name) and node.func.id == 'input':
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
# Handle imported input.xxx(...) call
|
|
38
|
+
if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
|
|
39
|
+
return node.func.value.id == 'input'
|
|
40
|
+
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
def visit_arguments(self, node: ast.arguments) -> ast.arguments:
|
|
44
|
+
"""Add _id to input calls in function arguments and collect source vars"""
|
|
45
|
+
if self.current_function is None:
|
|
46
|
+
return node
|
|
47
|
+
|
|
48
|
+
# Loop through arguments and defaults together
|
|
49
|
+
for arg, default in zip(node.args[-len(node.defaults):], node.defaults):
|
|
50
|
+
if default and isinstance(default, ast.Call):
|
|
51
|
+
if self._is_input_call(default):
|
|
52
|
+
# Add id keyword argument with argument name
|
|
53
|
+
default.keywords.append(
|
|
54
|
+
ast.keyword(
|
|
55
|
+
arg='_id',
|
|
56
|
+
value=ast.Constant(value=arg.arg)
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Check if it's a source input
|
|
61
|
+
is_source_call = False
|
|
62
|
+
is_input_call = False
|
|
63
|
+
|
|
64
|
+
if isinstance(default.func, ast.Attribute) and default.func.attr == 'source':
|
|
65
|
+
# This is lib.input.source or input.source call
|
|
66
|
+
is_source_call = True
|
|
67
|
+
elif isinstance(default.func, ast.Attribute) and default.func.attr == 'input':
|
|
68
|
+
# This is lib.input call
|
|
69
|
+
is_input_call = True
|
|
70
|
+
elif isinstance(default.func, ast.Name) and default.func.id == 'input':
|
|
71
|
+
# This is imported input() call - check if defval is a source
|
|
72
|
+
is_input_call = True
|
|
73
|
+
|
|
74
|
+
# Find the defval parameter value (either positional or keyword)
|
|
75
|
+
defval_node = None
|
|
76
|
+
if default.args:
|
|
77
|
+
defval_node = default.args[0]
|
|
78
|
+
else:
|
|
79
|
+
# Look for defval keyword argument
|
|
80
|
+
for kw in default.keywords:
|
|
81
|
+
if kw.arg == 'defval':
|
|
82
|
+
defval_node = kw.value
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
# Only proceed if it's a source call or input call with a defval
|
|
86
|
+
if (is_source_call or is_input_call) and defval_node:
|
|
87
|
+
source_name = None
|
|
88
|
+
|
|
89
|
+
if isinstance(defval_node, ast.Constant) and is_source_call:
|
|
90
|
+
# Handle string constant in lib.input.source
|
|
91
|
+
source_name = defval_node.value
|
|
92
|
+
elif isinstance(defval_node, ast.Attribute):
|
|
93
|
+
# Handle attribute reference (e.g., lib.close or close)
|
|
94
|
+
attr = defval_node
|
|
95
|
+
if isinstance(attr.value, ast.Name) and attr.value.id == 'lib':
|
|
96
|
+
# For lib.xxx pattern, store the attribute name
|
|
97
|
+
source_name = attr.attr
|
|
98
|
+
elif isinstance(defval_node, ast.Name):
|
|
99
|
+
# Handle direct name reference (e.g., close)
|
|
100
|
+
source_name = defval_node.id
|
|
101
|
+
|
|
102
|
+
if source_name:
|
|
103
|
+
if self.current_function not in self.function_source_vars:
|
|
104
|
+
self.function_source_vars[self.current_function] = {}
|
|
105
|
+
self.function_source_vars[self.current_function][arg.arg] = source_name
|
|
106
|
+
self.has_source_inputs = True
|
|
107
|
+
|
|
108
|
+
return node
|
|
109
|
+
|
|
110
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
111
|
+
"""Insert getattr calls at the start of functions for source inputs"""
|
|
112
|
+
# Save previous function name
|
|
113
|
+
previous_function = self.current_function
|
|
114
|
+
self.current_function = node.name
|
|
115
|
+
|
|
116
|
+
# Process function arguments and body
|
|
117
|
+
node = cast(ast.FunctionDef, self.generic_visit(node))
|
|
118
|
+
|
|
119
|
+
# Add getattr for each source input in this function
|
|
120
|
+
source_vars = self.function_source_vars.get(self.current_function, {})
|
|
121
|
+
for var_name, source_str in source_vars.items():
|
|
122
|
+
# Create: var_name = __pyne_getattr__(lib, var_name, lib.na)
|
|
123
|
+
# A reserved module-level alias of the builtin is used instead of a
|
|
124
|
+
# bare ``getattr``: a script variable named ``getattr`` would shadow
|
|
125
|
+
# the builtin in the function scope and break this resolution (same
|
|
126
|
+
# class of bug as a ``len`` input shadowing the loop-guard length).
|
|
127
|
+
assign = ast.Assign(
|
|
128
|
+
targets=[ast.Name(id=var_name, ctx=ast.Store())],
|
|
129
|
+
value=ast.Call(
|
|
130
|
+
func=ast.Name(id='__pyne_getattr__', ctx=ast.Load()),
|
|
131
|
+
args=[
|
|
132
|
+
ast.Name(id='lib', ctx=ast.Load()),
|
|
133
|
+
ast.Name(id=var_name, ctx=ast.Load()),
|
|
134
|
+
ast.Attribute(
|
|
135
|
+
value=ast.Name(id='lib', ctx=ast.Load()),
|
|
136
|
+
attr='na',
|
|
137
|
+
ctx=ast.Load()
|
|
138
|
+
)
|
|
139
|
+
],
|
|
140
|
+
keywords=[]
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
node.body.insert(0, assign)
|
|
144
|
+
|
|
145
|
+
# Restore previous function name
|
|
146
|
+
self.current_function = previous_function
|
|
147
|
+
return node
|
|
148
|
+
|
|
149
|
+
def visit_Module(self, node: ast.Module) -> ast.Module:
|
|
150
|
+
"""Add required imports if not present"""
|
|
151
|
+
# Process the module first to collect existing imports
|
|
152
|
+
node = cast(ast.Module, self.generic_visit(node))
|
|
153
|
+
|
|
154
|
+
if not self.has_source_inputs:
|
|
155
|
+
return node
|
|
156
|
+
|
|
157
|
+
# Source-input resolution emits ``__pyne_getattr__(lib, name, lib.na)``;
|
|
158
|
+
# bind that reserved alias to the builtin at module scope so a
|
|
159
|
+
# function-local ``getattr`` variable cannot shadow it.
|
|
160
|
+
alias_import = ast.ImportFrom(
|
|
161
|
+
module='builtins',
|
|
162
|
+
names=[ast.alias(name='getattr', asname='__pyne_getattr__')],
|
|
163
|
+
level=0)
|
|
164
|
+
insert_pos = 0
|
|
165
|
+
first = node.body[0] if node.body else None
|
|
166
|
+
if (isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant)
|
|
167
|
+
and isinstance(first.value.value, str)):
|
|
168
|
+
insert_pos = 1 # keep the module docstring first
|
|
169
|
+
while (insert_pos < len(node.body)
|
|
170
|
+
and isinstance(node.body[insert_pos], ast.ImportFrom)
|
|
171
|
+
and getattr(node.body[insert_pos], 'module', None) == '__future__'):
|
|
172
|
+
insert_pos += 1 # stay after any ``from __future__`` imports
|
|
173
|
+
node.body.insert(insert_pos, alias_import)
|
|
174
|
+
|
|
175
|
+
return node
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import cast
|
|
3
|
+
|
|
4
|
+
# Builtin price series that are always global (declared in main, not nested functions)
|
|
5
|
+
BUILTIN_PRICE_SERIES = frozenset({
|
|
6
|
+
'open', 'high', 'low', 'close', 'volume',
|
|
7
|
+
'bid', 'ask',
|
|
8
|
+
'hl2', 'hlc3', 'ohlc4', 'hlcc4'
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
# Lib attributes that are NOT Series — subscript access on these should not be transformed
|
|
12
|
+
NON_SERIES_LIB_ATTRS = frozenset({
|
|
13
|
+
'extra_fields',
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LibrarySeriesTransformer(ast.NodeTransformer):
|
|
18
|
+
"""
|
|
19
|
+
AST transformer that prepares library Series variables for the SeriesTransformer.
|
|
20
|
+
When a library variable is used with indexing, it creates a local Series variable
|
|
21
|
+
declaration that SeriesTransformer can then process.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.lib_series_vars: dict[
|
|
26
|
+
str, dict[str, tuple[str, ast.AST | None]]] = {} # module -> {attr -> (local_name, type_annotation)}
|
|
27
|
+
self.used_series: set[tuple[str, str, str]] = set() # (module, attr, function) tuples that are used as Series
|
|
28
|
+
self.declarations_to_insert: dict[str, list[ast.AnnAssign]] = {} # function_name -> declarations
|
|
29
|
+
self.current_function: str | None = None
|
|
30
|
+
self.parent_functions: list[str] = [] # Stack of parent function names for nested function tracking
|
|
31
|
+
self.module_level_declarations: list[ast.AnnAssign] = [] # Declarations for module level (main function)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def _make_series_name(attr: str) -> str:
|
|
35
|
+
"""Generate unique name for the local Series variable"""
|
|
36
|
+
# Use unicode middle dot to avoid name collisions between module hierarchies
|
|
37
|
+
# Example: mylib.bar vs mylib_bar would both become __lib·mylib·bar vs __lib·mylib_bar
|
|
38
|
+
attr = attr.replace('.', '·')
|
|
39
|
+
return f'__lib·{attr}'
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _create_attribute_chain(chain: list[str]) -> ast.expr:
|
|
43
|
+
"""Create an attribute chain from a list of names"""
|
|
44
|
+
result: ast.expr = cast(ast.expr, ast.Name(id=chain[0], ctx=ast.Load()))
|
|
45
|
+
for name in chain[1:]:
|
|
46
|
+
result = cast(ast.expr, ast.Attribute(
|
|
47
|
+
value=result,
|
|
48
|
+
attr=name,
|
|
49
|
+
ctx=ast.Load()
|
|
50
|
+
))
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
def visit_Module(self, node: ast.Module) -> ast.Module:
|
|
54
|
+
"""Process module and insert Series declarations"""
|
|
55
|
+
# Process the module first (collect all declarations)
|
|
56
|
+
node = cast(ast.Module, self.generic_visit(node))
|
|
57
|
+
|
|
58
|
+
# Insert function-level declarations into their target functions
|
|
59
|
+
self._insert_function_declarations(node)
|
|
60
|
+
|
|
61
|
+
# Insert module-level declarations at the beginning of the module
|
|
62
|
+
if self.module_level_declarations:
|
|
63
|
+
# Find the position after imports and before the main function
|
|
64
|
+
insert_pos = 0
|
|
65
|
+
for i, stmt in enumerate(node.body):
|
|
66
|
+
# Skip imports, from imports, and __all__ assignments
|
|
67
|
+
if isinstance(stmt, (ast.Import, ast.ImportFrom)):
|
|
68
|
+
insert_pos = i + 1
|
|
69
|
+
elif (isinstance(stmt, ast.Assign) and
|
|
70
|
+
len(stmt.targets) == 1 and
|
|
71
|
+
isinstance(stmt.targets[0], ast.Name) and
|
|
72
|
+
cast(ast.Name, stmt.targets[0]).id == '__all__'):
|
|
73
|
+
insert_pos = i + 1
|
|
74
|
+
else:
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
# Insert the declarations
|
|
78
|
+
node.body[insert_pos:insert_pos] = self.module_level_declarations
|
|
79
|
+
|
|
80
|
+
return node
|
|
81
|
+
|
|
82
|
+
def _insert_function_declarations(self, node: ast.Module) -> None:
|
|
83
|
+
"""Insert collected declarations into their target functions."""
|
|
84
|
+
for stmt in ast.walk(node):
|
|
85
|
+
if isinstance(stmt, ast.FunctionDef) and stmt.name in self.declarations_to_insert:
|
|
86
|
+
decls = self.declarations_to_insert[stmt.name]
|
|
87
|
+
stmt.body = decls + stmt.body
|
|
88
|
+
|
|
89
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
|
|
90
|
+
"""Track current function with parent tracking for nested functions"""
|
|
91
|
+
old_function = self.current_function
|
|
92
|
+
|
|
93
|
+
# Track parent functions for nested function detection
|
|
94
|
+
if self.current_function:
|
|
95
|
+
self.parent_functions.append(self.current_function)
|
|
96
|
+
self.current_function = node.name
|
|
97
|
+
|
|
98
|
+
# Process function body (declarations are inserted later in visit_Module)
|
|
99
|
+
new_node = cast(ast.FunctionDef, self.generic_visit(node))
|
|
100
|
+
|
|
101
|
+
self.current_function = old_function
|
|
102
|
+
if self.parent_functions:
|
|
103
|
+
self.parent_functions.pop()
|
|
104
|
+
|
|
105
|
+
return new_node
|
|
106
|
+
|
|
107
|
+
def process_series_usage(self, module: str, attr_chain: list[str], type_annotation: ast.AST | None = None) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Process a Series usage from a library, creating declaration if needed.
|
|
110
|
+
Returns the local variable name to use.
|
|
111
|
+
|
|
112
|
+
For builtin price series (close, open, high, low, volume, hl2, hlc3, ohlc4, hlcc4),
|
|
113
|
+
declarations are always placed in the main function, not nested functions.
|
|
114
|
+
This matches TradingView behavior where these series are global.
|
|
115
|
+
"""
|
|
116
|
+
if module not in self.lib_series_vars:
|
|
117
|
+
self.lib_series_vars[module] = {}
|
|
118
|
+
|
|
119
|
+
# Determine if this is a builtin price series (single attribute like 'close', 'open', etc.)
|
|
120
|
+
is_builtin_price = len(attr_chain) == 1 and attr_chain[0] in BUILTIN_PRICE_SERIES
|
|
121
|
+
|
|
122
|
+
# For builtin price series in nested functions, use the top-level parent (main)
|
|
123
|
+
if is_builtin_price and self.parent_functions:
|
|
124
|
+
target_function = self.parent_functions[0] # First parent is 'main'
|
|
125
|
+
function_key = target_function
|
|
126
|
+
else:
|
|
127
|
+
target_function = self.current_function
|
|
128
|
+
function_key = self.current_function or "__module__"
|
|
129
|
+
|
|
130
|
+
# Create unicode middle dot version for the variable name to avoid collisions
|
|
131
|
+
attr_key = '·'.join(attr_chain)
|
|
132
|
+
if attr_key not in self.lib_series_vars[module]:
|
|
133
|
+
local_name = self._make_series_name(attr_key)
|
|
134
|
+
self.lib_series_vars[module][attr_key] = (local_name, type_annotation)
|
|
135
|
+
|
|
136
|
+
local_name = self.lib_series_vars[module][attr_key][0]
|
|
137
|
+
|
|
138
|
+
# If this Series hasn't been used in target function or module level yet
|
|
139
|
+
if (module, attr_key, function_key) not in self.used_series:
|
|
140
|
+
self.used_series.add((module, attr_key, function_key))
|
|
141
|
+
|
|
142
|
+
# Create Series declaration with proper attribute chain. Builtin
|
|
143
|
+
# price series are float — a Series[float] annotation makes the
|
|
144
|
+
# SeriesTransformer carry the element type into the slot layout,
|
|
145
|
+
# so their buffers return the native nan for out-of-range reads.
|
|
146
|
+
if type_annotation is None:
|
|
147
|
+
if is_builtin_price:
|
|
148
|
+
annotation: ast.expr = ast.Subscript(
|
|
149
|
+
value=ast.Name(id='Series', ctx=ast.Load()),
|
|
150
|
+
slice=ast.Name(id='float', ctx=ast.Load()),
|
|
151
|
+
ctx=ast.Load())
|
|
152
|
+
else:
|
|
153
|
+
annotation = ast.Name(id='Series', ctx=ast.Load())
|
|
154
|
+
else:
|
|
155
|
+
annotation = cast(ast.expr, type_annotation)
|
|
156
|
+
decl = ast.AnnAssign(
|
|
157
|
+
target=ast.Name(id=local_name, ctx=ast.Store()),
|
|
158
|
+
annotation=annotation,
|
|
159
|
+
value=self._create_attribute_chain([module] + attr_chain),
|
|
160
|
+
simple=1
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Store declaration to be inserted
|
|
164
|
+
if target_function:
|
|
165
|
+
# Inside a function
|
|
166
|
+
if target_function not in self.declarations_to_insert:
|
|
167
|
+
self.declarations_to_insert[target_function] = []
|
|
168
|
+
self.declarations_to_insert[target_function].append(decl)
|
|
169
|
+
else:
|
|
170
|
+
# At module level (main function)
|
|
171
|
+
self.module_level_declarations.append(decl)
|
|
172
|
+
|
|
173
|
+
return local_name
|
|
174
|
+
|
|
175
|
+
def visit_Subscript(self, node: ast.Subscript) -> ast.AST:
|
|
176
|
+
"""Convert library Series access when used with indexing"""
|
|
177
|
+
|
|
178
|
+
# Get the full attribute chain
|
|
179
|
+
def get_attribute_chain(_node):
|
|
180
|
+
if isinstance(_node, ast.Name):
|
|
181
|
+
return [_node.id]
|
|
182
|
+
elif isinstance(_node, ast.Attribute):
|
|
183
|
+
return get_attribute_chain(_node.value) + [_node.attr]
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
if isinstance(node.value, ast.Attribute):
|
|
187
|
+
attr_chain = get_attribute_chain(node.value)
|
|
188
|
+
if attr_chain and attr_chain[0] == 'lib':
|
|
189
|
+
# Skip non-Series lib attributes (e.g., extra_fields is a dict, not a Series)
|
|
190
|
+
if len(attr_chain) >= 2 and attr_chain[1] in NON_SERIES_LIB_ATTRS:
|
|
191
|
+
return self.generic_visit(node)
|
|
192
|
+
|
|
193
|
+
# Use the complete chain after 'lib'. The declaration of a
|
|
194
|
+
# builtin price series is anchored in the outermost function
|
|
195
|
+
# (see process_series_usage); a nested reference resolves to
|
|
196
|
+
# that scope's series slot through the SeriesTransformer's
|
|
197
|
+
# scope-chain lookup, so the local name is always enough here.
|
|
198
|
+
local_name = self.process_series_usage('lib', attr_chain[1:])
|
|
199
|
+
node.value = cast(ast.expr, ast.Name(id=local_name, ctx=ast.Load()))
|
|
200
|
+
|
|
201
|
+
return self.generic_visit(node)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Debugger-safe source-location filling for transformer-built AST nodes.
|
|
2
|
+
|
|
3
|
+
``ast.fix_missing_locations`` copies the PARENT's full span onto every
|
|
4
|
+
location-less node. For a transformer-inserted statement in a function body
|
|
5
|
+
the nearest located parent is the ``FunctionDef`` itself, so the statement
|
|
6
|
+
inherits the function's whole range (``def`` line .. last body line) — and
|
|
7
|
+
CPython derives the bytecode position of attribute/subscript operations from
|
|
8
|
+
a span's END, mapping parts of the emitted prologue onto the function's LAST
|
|
9
|
+
source line. A breakpoint on that line then fires on every function entry,
|
|
10
|
+
mid-prologue, with the body locals still unassigned.
|
|
11
|
+
|
|
12
|
+
:func:`fix_locations` fills the same holes with single POINT anchors instead:
|
|
13
|
+
|
|
14
|
+
- a located node is never touched (a missing end position is completed from
|
|
15
|
+
the node's own start, never from the parent's);
|
|
16
|
+
- a location-less statement anchors to the earliest source location surviving
|
|
17
|
+
inside it (hoisted expressions keep their original lines), falling back to
|
|
18
|
+
the innermost located ancestor's start — function entry for the prologue;
|
|
19
|
+
- every other synthetic node anchors to its enclosing statement's point.
|
|
20
|
+
|
|
21
|
+
Points, not spans: a synthetic node stamped with a multi-line span would put
|
|
22
|
+
its attribute-op line events on the span's end line, re-creating the bug.
|
|
23
|
+
"""
|
|
24
|
+
import ast
|
|
25
|
+
|
|
26
|
+
__all__ = ('fix_locations',)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _located(node: ast.AST) -> bool:
|
|
30
|
+
return getattr(node, 'lineno', None) is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _stamp_point(node: ast.AST, line: int, col: int) -> None:
|
|
34
|
+
node.lineno = node.end_lineno = line # type: ignore[attr-defined]
|
|
35
|
+
node.col_offset = node.end_col_offset = col # type: ignore[attr-defined]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fix_locations(tree: ast.AST, line: int = 1, col: int = 0) -> ast.AST:
|
|
39
|
+
"""Fill missing locations in ``tree`` without leaking parent spans.
|
|
40
|
+
|
|
41
|
+
Drop-in replacement for :func:`ast.fix_missing_locations` on transformed
|
|
42
|
+
Pyne modules (see module docstring for why the stock helper is unsafe).
|
|
43
|
+
|
|
44
|
+
:param tree: Tree to fix in place.
|
|
45
|
+
:param line: Anchor line for top-level location-less nodes.
|
|
46
|
+
:param col: Anchor column for top-level location-less nodes.
|
|
47
|
+
:return: The same tree.
|
|
48
|
+
"""
|
|
49
|
+
_fix(tree, line, col)
|
|
50
|
+
return tree
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _fix(node: ast.AST, line: int, col: int) -> None:
|
|
54
|
+
if 'lineno' in node._attributes:
|
|
55
|
+
if _located(node):
|
|
56
|
+
line, col = node.lineno, node.col_offset # type: ignore[attr-defined]
|
|
57
|
+
if getattr(node, 'end_lineno', None) is None:
|
|
58
|
+
node.end_lineno = line # type: ignore[attr-defined]
|
|
59
|
+
if getattr(node, 'end_col_offset', None) is None:
|
|
60
|
+
node.end_col_offset = col # type: ignore[attr-defined]
|
|
61
|
+
else:
|
|
62
|
+
if isinstance(node, ast.stmt):
|
|
63
|
+
# Hoisted payloads keep their source lines — anchor the new
|
|
64
|
+
# statement next to them rather than at the function entry
|
|
65
|
+
inner = [getattr(n, 'lineno') for n in ast.walk(node) if _located(n)]
|
|
66
|
+
if inner:
|
|
67
|
+
line, col = min(inner), 0
|
|
68
|
+
_stamp_point(node, line, col)
|
|
69
|
+
for child in ast.iter_child_nodes(node):
|
|
70
|
+
_fix(child, line, col)
|