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,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Higher-timeframe bar aggregator for live ``request.security()`` developing bars.
|
|
3
|
+
|
|
4
|
+
The chart process aggregates its own (lower-TF) OHLCV stream into a developing
|
|
5
|
+
HTF bar for each ``lookahead_on`` security context. The aggregated developing
|
|
6
|
+
bar is then transported to the security child via the SyncBlock so the child
|
|
7
|
+
can run its ``main()`` function under ``barstate.isconfirmed=False`` using the
|
|
8
|
+
chart-derived OHLCV — mirroring the TradingView live ``lookahead_on`` semantics.
|
|
9
|
+
|
|
10
|
+
Aggregation rules per HTF period (chart bars whose ``get_bar_time(chart_ts)``
|
|
11
|
+
maps to the same HTF open):
|
|
12
|
+
|
|
13
|
+
open = first chart bar's open
|
|
14
|
+
high = max chart high seen so far
|
|
15
|
+
low = min chart low seen so far
|
|
16
|
+
close = latest chart close
|
|
17
|
+
volume = sum of chart volumes (one contribution per chart bar)
|
|
18
|
+
|
|
19
|
+
Live providers (e.g. CCXT ``watch_ohlcv``) emit repeated intra-bar updates
|
|
20
|
+
carrying the *cumulative* candle volume. ``update()`` is therefore keyed on
|
|
21
|
+
``chart_time_ms``: when the same chart bar fires again, the prior
|
|
22
|
+
contribution of that bar is subtracted from the HTF developing volume and
|
|
23
|
+
replaced with the latest cumulative reading — preventing double counting.
|
|
24
|
+
|
|
25
|
+
A period closes following TradingView's ``lookahead_off`` merge rule: the
|
|
26
|
+
confirmed chart bar whose close instant reaches the HTF period end completes
|
|
27
|
+
the bar, so the closed HTF bar is delivered together with the period's LAST
|
|
28
|
+
chart bar — not with the next period's first one. When no such completing bar
|
|
29
|
+
arrives (data gap, or the chart bar span is unknown — D/W/M charts), the close
|
|
30
|
+
falls back to the first chart bar that crosses into a new HTF period.
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from typing import TYPE_CHECKING
|
|
36
|
+
|
|
37
|
+
from .resampler import Resampler
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from zoneinfo import ZoneInfo
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DevelopingBar:
|
|
45
|
+
"""In-progress aggregated OHLCV for one HTF period.
|
|
46
|
+
|
|
47
|
+
``last_chart_time_ms`` / ``last_chart_volume`` track the most recently
|
|
48
|
+
folded chart bar so repeated intra-bar updates from a live provider
|
|
49
|
+
(which carry the running cumulative candle volume) can replace their
|
|
50
|
+
own prior contribution instead of stacking it.
|
|
51
|
+
"""
|
|
52
|
+
period_start: int # HTF period open time in ms
|
|
53
|
+
open: float
|
|
54
|
+
high: float
|
|
55
|
+
low: float
|
|
56
|
+
close: float
|
|
57
|
+
volume: float
|
|
58
|
+
last_chart_time_ms: int = 0
|
|
59
|
+
last_chart_volume: float = 0.0
|
|
60
|
+
|
|
61
|
+
def update(self, chart_time_ms: int, chart_high: float, chart_low: float,
|
|
62
|
+
chart_close: float, chart_volume: float) -> None:
|
|
63
|
+
if chart_high > self.high:
|
|
64
|
+
self.high = chart_high
|
|
65
|
+
if chart_low < self.low:
|
|
66
|
+
self.low = chart_low
|
|
67
|
+
self.close = chart_close
|
|
68
|
+
if chart_time_ms == self.last_chart_time_ms:
|
|
69
|
+
# Same chart bar re-firing (intra-bar tick): live providers send
|
|
70
|
+
# the running cumulative candle volume, so the previous reading
|
|
71
|
+
# must be subtracted before adding the new one.
|
|
72
|
+
self.volume += chart_volume - self.last_chart_volume
|
|
73
|
+
else:
|
|
74
|
+
self.volume += chart_volume
|
|
75
|
+
self.last_chart_time_ms = chart_time_ms
|
|
76
|
+
self.last_chart_volume = chart_volume
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HTFAggregator:
|
|
80
|
+
"""Per-sec_id aggregator of chart OHLCV into a developing HTF bar."""
|
|
81
|
+
|
|
82
|
+
__slots__ = ('_timeframe', '_tz', '_resampler', '_state', '_session_starts',
|
|
83
|
+
'_chart_span_ms')
|
|
84
|
+
|
|
85
|
+
def __init__(self, timeframe: str, tz: 'ZoneInfo',
|
|
86
|
+
session_starts: 'list | None' = None,
|
|
87
|
+
chart_span_ms: int = 0):
|
|
88
|
+
self._timeframe = timeframe
|
|
89
|
+
self._tz = tz
|
|
90
|
+
self._resampler = Resampler.get_resampler(timeframe)
|
|
91
|
+
self._state: DevelopingBar | None = None
|
|
92
|
+
# Intraday session anchoring (TradingView aligns HTF bars to the session
|
|
93
|
+
# open). ``None`` keeps the pure clock-floor — see ``Resampler.get_bar_time``.
|
|
94
|
+
self._session_starts = session_starts
|
|
95
|
+
# Chart bar span in ms for the close-instant period completion check.
|
|
96
|
+
# 0 (D/W/M charts: no fixed arithmetic span) disables it — the period
|
|
97
|
+
# then closes on the first chart bar of the next period instead.
|
|
98
|
+
self._chart_span_ms = chart_span_ms
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def timeframe(self) -> str:
|
|
102
|
+
return self._timeframe
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def current(self) -> DevelopingBar | None:
|
|
106
|
+
"""Currently developing HTF bar, or None if no chart bar seen yet."""
|
|
107
|
+
return self._state
|
|
108
|
+
|
|
109
|
+
def update(
|
|
110
|
+
self,
|
|
111
|
+
chart_time_ms: int,
|
|
112
|
+
chart_open: float, chart_high: float, chart_low: float,
|
|
113
|
+
chart_close: float, chart_volume: float,
|
|
114
|
+
chart_confirmed: bool = True,
|
|
115
|
+
) -> tuple[bool, DevelopingBar | None, DevelopingBar | None]:
|
|
116
|
+
"""
|
|
117
|
+
Fold one chart bar into the developing HTF bar.
|
|
118
|
+
|
|
119
|
+
:param chart_time_ms: Chart bar open timestamp in ms.
|
|
120
|
+
:param chart_open: Chart bar open price.
|
|
121
|
+
:param chart_high: Chart bar high price.
|
|
122
|
+
:param chart_low: Chart bar low price.
|
|
123
|
+
:param chart_close: Chart bar close price.
|
|
124
|
+
:param chart_volume: Chart bar volume (cumulative within the bar for
|
|
125
|
+
live providers; the aggregator deduplicates same-bar updates).
|
|
126
|
+
:param chart_confirmed: Whether the folded chart bar is final
|
|
127
|
+
(``barstate.isconfirmed``). Only a confirmed chart bar may complete
|
|
128
|
+
the period — a developing intra-bar tick must not freeze a
|
|
129
|
+
non-final HTF close.
|
|
130
|
+
:return: ``(is_new_period, developing_or_none, closed_or_none)``
|
|
131
|
+
|
|
132
|
+
- ``is_new_period`` — True iff the chart bar opens a fresh HTF period.
|
|
133
|
+
- ``developing_or_none`` — the accumulated HTF bar including this
|
|
134
|
+
chart bar, or None when this chart bar just completed the period
|
|
135
|
+
(no fresh developing bar exists until the next chart bar).
|
|
136
|
+
- ``closed_or_none`` — the completed period's final accumulated bar:
|
|
137
|
+
delivered with the confirmed chart bar whose close instant reaches
|
|
138
|
+
the period end (TV ``lookahead_off`` merge rule), or — when no
|
|
139
|
+
such bar arrived — with the first chart bar of the next period.
|
|
140
|
+
"""
|
|
141
|
+
period_start = self._resampler.get_bar_time(
|
|
142
|
+
chart_time_ms, self._tz, self._session_starts)
|
|
143
|
+
|
|
144
|
+
closed: DevelopingBar | None = None
|
|
145
|
+
state = self._state
|
|
146
|
+
is_new = state is None or period_start != state.period_start
|
|
147
|
+
if is_new:
|
|
148
|
+
if state is not None:
|
|
149
|
+
# The period was left without its completing chart bar (data
|
|
150
|
+
# gap) — deliver its accumulated final form one bar late.
|
|
151
|
+
closed = state
|
|
152
|
+
state = DevelopingBar(
|
|
153
|
+
period_start=period_start,
|
|
154
|
+
open=chart_open, high=chart_high, low=chart_low,
|
|
155
|
+
close=chart_close, volume=chart_volume,
|
|
156
|
+
last_chart_time_ms=chart_time_ms,
|
|
157
|
+
last_chart_volume=chart_volume,
|
|
158
|
+
)
|
|
159
|
+
self._state = state
|
|
160
|
+
else:
|
|
161
|
+
state.update(chart_time_ms, chart_high, chart_low,
|
|
162
|
+
chart_close, chart_volume)
|
|
163
|
+
|
|
164
|
+
# TV ``lookahead_off`` merge rule: the confirmed chart bar whose close
|
|
165
|
+
# instant reaches the period end completes the HTF bar — deliver it
|
|
166
|
+
# now, one chart bar earlier than the next-period fallback above.
|
|
167
|
+
# Skipped when a gap-closed bar is already pending (one closed bar per
|
|
168
|
+
# update; the fresh period then closes via the fallback) or when the
|
|
169
|
+
# chart bar span is unknown (``chart_span_ms == 0``).
|
|
170
|
+
if (closed is None and chart_confirmed and self._chart_span_ms
|
|
171
|
+
and self._resampler.get_bar_time(
|
|
172
|
+
chart_time_ms + self._chart_span_ms, self._tz,
|
|
173
|
+
self._session_starts) != period_start):
|
|
174
|
+
self._state = None
|
|
175
|
+
return is_new, None, state
|
|
176
|
+
|
|
177
|
+
return is_new, state, closed
|
|
178
|
+
|
|
179
|
+
def reset(self) -> None:
|
|
180
|
+
"""Discard any in-flight developing bar (used on context teardown)."""
|
|
181
|
+
self._state = None
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import hashlib
|
|
5
|
+
import importlib.util
|
|
6
|
+
import importlib.machinery
|
|
7
|
+
import re
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Module-level constant the transform pipeline bakes into every transformed module
|
|
12
|
+
# (see ``PyneLoader.source_to_code``). Its presence — together with a matching
|
|
13
|
+
# pipeline hash in ``co_consts`` — certifies a loaded code object as current
|
|
14
|
+
# pipeline output, so foreign or stale bytecode can be told apart and dropped.
|
|
15
|
+
_PYNE_SENTINEL = '__pyne_transformed__'
|
|
16
|
+
|
|
17
|
+
# A module is Pyne code only when its docstring STARTS with ``@pyne``. Matching the
|
|
18
|
+
# raw source head mirrors the strict docstring check in ``source_to_code`` without
|
|
19
|
+
# paying for a full parse on every import. Leading comment lines are skipped so a
|
|
20
|
+
# PEP 723 ``# /// script`` metadata block before the docstring does not hide it.
|
|
21
|
+
_PYNE_HEAD_RE = re.compile(
|
|
22
|
+
rb'^(?:\s*#[^\r\n]*(?:\r?\n|$))*\s*[rRbBuUfF]*("""|\'\'\'|"|\')\s*@pyne(?:\s|\1|$)')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _source_starts_with_pyne(head: bytes) -> bool:
|
|
26
|
+
"""Return whether a source head is a Pyne module (docstring begins with ``@pyne``).
|
|
27
|
+
|
|
28
|
+
:param head: First bytes of the source file.
|
|
29
|
+
:return: Whether the module should carry the transform sentinel.
|
|
30
|
+
"""
|
|
31
|
+
return _PYNE_HEAD_RE.match(head) is not None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _cache_from_source(source_path: Path) -> Path:
|
|
35
|
+
"""Return the cached ``.pyc`` path CPython uses for a given ``.py`` source.
|
|
36
|
+
|
|
37
|
+
Delegates to :func:`importlib.util.cache_from_source` instead of hand-building
|
|
38
|
+
``<dir>/__pycache__/<stem>.<tag>.pyc`` so the result matches CPython exactly:
|
|
39
|
+
it honours ``sys.pycache_prefix`` / ``PYTHONPYCACHEPREFIX`` (which mirrors the
|
|
40
|
+
cache under a separate tree rather than a sibling ``__pycache__``) and the
|
|
41
|
+
active optimization level (``.opt-1`` / ``.opt-2`` under ``-O`` / ``-OO``).
|
|
42
|
+
Stale-bytecode invalidation must target the exact file CPython reads back, so
|
|
43
|
+
a mismatch here would silently leave the cache untouched and the bug unfixed.
|
|
44
|
+
|
|
45
|
+
:param source_path: Path to the ``.py`` source file.
|
|
46
|
+
:return: Path to the corresponding cached bytecode file.
|
|
47
|
+
"""
|
|
48
|
+
return Path(importlib.util.cache_from_source(str(source_path)))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_transform_pipeline_hash: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _get_transform_pipeline_hash() -> str:
|
|
55
|
+
"""Return a content digest identifying the current AST transform pipeline.
|
|
56
|
+
|
|
57
|
+
Transformed bytecode is only valid for the exact pipeline that produced it, yet
|
|
58
|
+
CPython validates a ``.pyc`` solely against its source ``.py`` mtime/size — it
|
|
59
|
+
cannot tell a transformed module from one compiled without the import hook
|
|
60
|
+
(``pip``'s post-install ``compileall``, an IDE, a packaging step) or one left
|
|
61
|
+
over by an older PyneCore. This digest is baked into every transformed module as
|
|
62
|
+
``__pyne_transformed__`` and re-checked on load; a missing or mismatched value
|
|
63
|
+
forces a retransform.
|
|
64
|
+
|
|
65
|
+
Hashing the pipeline *contents* — this module plus every file under
|
|
66
|
+
``transformers/`` (``module_properties.json`` shapes the output yet has no
|
|
67
|
+
bytecode of its own) — keeps the check deterministic and immune to file mtimes,
|
|
68
|
+
cache markers and read-only install locations.
|
|
69
|
+
|
|
70
|
+
:return: Hex digest pinning the transform pipeline.
|
|
71
|
+
"""
|
|
72
|
+
global _transform_pipeline_hash
|
|
73
|
+
if _transform_pipeline_hash is None:
|
|
74
|
+
files = [Path(__file__)] # this module pins the transformer pipeline order
|
|
75
|
+
transformers_dir = Path(__file__).parent.parent / "transformers"
|
|
76
|
+
try:
|
|
77
|
+
files.extend(transformers_dir.iterdir())
|
|
78
|
+
except OSError:
|
|
79
|
+
pass
|
|
80
|
+
digest = hashlib.sha256()
|
|
81
|
+
for f in sorted(files, key=lambda p: p.name):
|
|
82
|
+
try:
|
|
83
|
+
if f.is_file():
|
|
84
|
+
digest.update(f.name.encode('utf-8'))
|
|
85
|
+
digest.update(f.read_bytes())
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|
|
88
|
+
_transform_pipeline_hash = digest.hexdigest()[:16]
|
|
89
|
+
return _transform_pipeline_hash
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class PyneLoader(importlib.machinery.SourceFileLoader):
|
|
93
|
+
"""Loader that handles AST transformation"""
|
|
94
|
+
|
|
95
|
+
def get_code(self, fullname: str):
|
|
96
|
+
"""Retransform cached bytecode not produced by the current transform pipeline.
|
|
97
|
+
|
|
98
|
+
CPython validates a cached ``.pyc`` only against its source ``.py`` mtime and
|
|
99
|
+
size, so it cannot distinguish a transformed ``@pyne`` module from one compiled
|
|
100
|
+
without the import hook (``pip``'s post-install ``compileall``, an IDE, a
|
|
101
|
+
packaging step) or one left over by an older pipeline — all of them load as
|
|
102
|
+
"valid" and silently run the wrong bytecode. Every transformed module carries a
|
|
103
|
+
``__pyne_transformed__ = <pipeline hash>`` sentinel baked into its code object;
|
|
104
|
+
if the loaded bytecode lacks it or the hash is stale, the ``.pyc`` is dropped and
|
|
105
|
+
the source is retransformed. The check is content-based, so it holds regardless
|
|
106
|
+
of file mtimes, cache markers or a read-only install location.
|
|
107
|
+
|
|
108
|
+
:param fullname: Fully-qualified module name being loaded.
|
|
109
|
+
:return: The compiled code object (retransformed if the cache was foreign or stale).
|
|
110
|
+
"""
|
|
111
|
+
source_path = self.get_filename(fullname)
|
|
112
|
+
code = super().get_code(fullname)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
with open(source_path, 'rb') as f:
|
|
116
|
+
# Large enough to cover a PEP 723 metadata block before the docstring
|
|
117
|
+
head = f.read(4096)
|
|
118
|
+
except OSError:
|
|
119
|
+
head = b''
|
|
120
|
+
|
|
121
|
+
# Only transformed modules carry the sentinel; leave everything else untouched.
|
|
122
|
+
# ``get_code`` is typed Optional, but a real source file always yields a code
|
|
123
|
+
# object — the ``None`` guard just narrows the type for the checks below.
|
|
124
|
+
if code is None or not _source_starts_with_pyne(head):
|
|
125
|
+
return code
|
|
126
|
+
|
|
127
|
+
pipeline_hash = _get_transform_pipeline_hash()
|
|
128
|
+
if _PYNE_SENTINEL in code.co_names and pipeline_hash in code.co_consts:
|
|
129
|
+
return code
|
|
130
|
+
|
|
131
|
+
# Foreign or stale bytecode slipped past CPython's mtime/size check — drop it
|
|
132
|
+
# and let the loader recompile, refreshing the cache when the dir is writable.
|
|
133
|
+
try:
|
|
134
|
+
_cache_from_source(Path(source_path)).unlink()
|
|
135
|
+
except OSError:
|
|
136
|
+
pass # no cached bytecode, or a read-only cache dir: nothing to drop
|
|
137
|
+
code = super().get_code(fullname)
|
|
138
|
+
if code is None or (_PYNE_SENTINEL in code.co_names and pipeline_hash in code.co_consts):
|
|
139
|
+
return code
|
|
140
|
+
|
|
141
|
+
# The stale ``.pyc`` could not be removed (read-only / locked cache) and still
|
|
142
|
+
# masks the source. Compile straight from source so the correct bytecode runs
|
|
143
|
+
# regardless; caching is skipped this load — correctness wins over the cache.
|
|
144
|
+
return self.source_to_code(self.get_data(source_path), source_path)
|
|
145
|
+
|
|
146
|
+
# noinspection PyMethodOverriding
|
|
147
|
+
def source_to_code(self, data: bytes | str, path: str, *, _optimize: int = -1):
|
|
148
|
+
"""Transform source to code if needed"""
|
|
149
|
+
path: Path = Path(path)
|
|
150
|
+
|
|
151
|
+
# Fast prefilter: require @pyne as a standalone token, not just any substring.
|
|
152
|
+
# Compiled Pyne code always has it as the first non-whitespace content of the
|
|
153
|
+
# module docstring, either multi-line (`"""\n@pyne\n…"""`) or single-line
|
|
154
|
+
# (`"""@pyne"""`); the latter puts the closing quote right after the token, so a
|
|
155
|
+
# quote must terminate the match alongside whitespace / end-of-input. A loose
|
|
156
|
+
# check would AST-transform ordinary modules that merely *mention* @pyne in a
|
|
157
|
+
# docstring (e.g. standalone.py); the strict docstring check below still gates it.
|
|
158
|
+
data_str = data.decode('utf-8') if isinstance(data, bytes) else data
|
|
159
|
+
if not re.search(r'@pyne(\s|["\']|$)', data_str):
|
|
160
|
+
return compile(data, path, 'exec', optimize=_optimize)
|
|
161
|
+
|
|
162
|
+
import ast
|
|
163
|
+
|
|
164
|
+
tree = ast.parse(data_str)
|
|
165
|
+
|
|
166
|
+
# Strict check: the module docstring must START with @pyne (whitespace-stripped),
|
|
167
|
+
# followed by whitespace or end of string. Substring matches don't count — they
|
|
168
|
+
# would catch innocuous mentions inside docstrings of non-script library modules.
|
|
169
|
+
is_pyne_module = False
|
|
170
|
+
if (tree.body and isinstance(tree.body[0], ast.Expr) and
|
|
171
|
+
isinstance(cast(ast.Expr, tree.body[0]).value, ast.Constant) and
|
|
172
|
+
isinstance(cast(ast.Constant, cast(ast.Expr, tree.body[0]).value).value, str)):
|
|
173
|
+
docstring = cast(str, cast(ast.Constant, cast(ast.Expr, tree.body[0]).value).value)
|
|
174
|
+
is_pyne_module = re.match(r'\s*@pyne(\s|$)', docstring) is not None
|
|
175
|
+
|
|
176
|
+
if is_pyne_module:
|
|
177
|
+
|
|
178
|
+
# Remove test cases from the output, because they can coorupt the output
|
|
179
|
+
transformed = tree
|
|
180
|
+
# Source path for the transformers (SecurityTransformer hashes it into
|
|
181
|
+
# the per-module sec ids, so security contexts stay unique across the
|
|
182
|
+
# script and its imported library modules). Resolved so the chart
|
|
183
|
+
# process and its security children derive identical ids.
|
|
184
|
+
transformed._module_file_path = str(path.resolve()) # type: ignore[attr-defined]
|
|
185
|
+
transformed.body = [node for node in transformed.body
|
|
186
|
+
if not (isinstance(node, ast.FunctionDef)
|
|
187
|
+
and node.name.startswith('__test_') and node.name.endswith('__'))]
|
|
188
|
+
|
|
189
|
+
# Transform AST - lazy import transformers only when needed
|
|
190
|
+
from pynecore.transformers.import_lifter import ImportLifterTransformer
|
|
191
|
+
from pynecore.transformers.type_checking_stripper import TypeCheckingStripperTransformer
|
|
192
|
+
from pynecore.transformers.builtin_shadow import BuiltinShadowTransformer
|
|
193
|
+
from pynecore.transformers.import_normalizer import ImportNormalizerTransformer
|
|
194
|
+
from pynecore.transformers.dynamic_default import DynamicDefaultTransformer
|
|
195
|
+
from pynecore.transformers.inline_series_hoist import InlineSeriesHoistTransformer
|
|
196
|
+
from pynecore.transformers.security import SecurityTransformer
|
|
197
|
+
from pynecore.transformers.security_instantiation import (
|
|
198
|
+
SecurityInstantiationTransformer,
|
|
199
|
+
)
|
|
200
|
+
from pynecore.transformers.persistent_series import PersistentSeriesTransformer
|
|
201
|
+
from pynecore.transformers.lib_series import LibrarySeriesTransformer
|
|
202
|
+
from pynecore.transformers.closure_arguments_transformer import ClosureArgumentsTransformer
|
|
203
|
+
from pynecore.transformers.function_isolation import FunctionIsolationTransformer
|
|
204
|
+
from pynecore.transformers.module_property import ModulePropertyTransformer
|
|
205
|
+
from pynecore.transformers.series import SeriesTransformer
|
|
206
|
+
from pynecore.transformers.script_requirements import ScriptRequirementsTransformer
|
|
207
|
+
from pynecore.transformers.unused_series_detector import UnusedSeriesDetectorTransformer
|
|
208
|
+
from pynecore.transformers.persistent import PersistentTransformer
|
|
209
|
+
from pynecore.transformers.input_transformer import InputTransformer
|
|
210
|
+
from pynecore.transformers.safe_convert_transformer import SafeConvertTransformer
|
|
211
|
+
from pynecore.transformers.safe_division_transformer import SafeDivisionTransformer
|
|
212
|
+
from pynecore.transformers.ne_guard import NeGuardTransformer
|
|
213
|
+
from pynecore.transformers.slot_layout import ModuleLayout, apply_layout
|
|
214
|
+
from pynecore.transformers.locations import fix_locations
|
|
215
|
+
|
|
216
|
+
# Shared slot allocator of the module (see slot_layout.py); the
|
|
217
|
+
# state-contributing transformers fill it, apply_layout emits it
|
|
218
|
+
slot_layout = ModuleLayout()
|
|
219
|
+
|
|
220
|
+
transformed = ImportLifterTransformer().visit(transformed)
|
|
221
|
+
transformed = TypeCheckingStripperTransformer().visit(transformed)
|
|
222
|
+
# The builtin-namespace fallback must run before import normalization
|
|
223
|
+
# so the lib.<ns>.<name> chains it emits get their imports added there
|
|
224
|
+
transformed = BuiltinShadowTransformer().visit(transformed)
|
|
225
|
+
transformed = ImportNormalizerTransformer().visit(transformed)
|
|
226
|
+
# Per-call evaluation of lib.*-referencing parameter defaults; must
|
|
227
|
+
# precede the series/isolation passes so the moved expressions get
|
|
228
|
+
# their series slots and call-site anchors like any body statement
|
|
229
|
+
transformed = DynamicDefaultTransformer().visit(transformed)
|
|
230
|
+
# Lazy-context history hoist must run before call-site anchoring:
|
|
231
|
+
# the hoisted statements are the anchorable call sites
|
|
232
|
+
transformed = InlineSeriesHoistTransformer().visit(transformed)
|
|
233
|
+
# Pine instantiation semantics: clone security-bearing functions
|
|
234
|
+
# per call site so each call site gets its own security contexts
|
|
235
|
+
transformed = SecurityInstantiationTransformer().visit(transformed)
|
|
236
|
+
transformed = SecurityTransformer().visit(transformed)
|
|
237
|
+
transformed = PersistentSeriesTransformer().visit(transformed)
|
|
238
|
+
transformed = LibrarySeriesTransformer().visit(transformed)
|
|
239
|
+
transformed = ModulePropertyTransformer().visit(transformed)
|
|
240
|
+
transformed = ClosureArgumentsTransformer().visit(transformed)
|
|
241
|
+
transformed = UnusedSeriesDetectorTransformer().optimize(transformed)
|
|
242
|
+
transformed = SeriesTransformer(slot_layout).visit(transformed)
|
|
243
|
+
transformed = PersistentTransformer(slot_layout).visit(transformed)
|
|
244
|
+
# Call-site classification needs the var/series slots, so the
|
|
245
|
+
# isolation transformer must run after Persistent and Series
|
|
246
|
+
transformed = FunctionIsolationTransformer(slot_layout).visit(transformed)
|
|
247
|
+
transformed = ScriptRequirementsTransformer().visit(transformed)
|
|
248
|
+
transformed = InputTransformer().visit(transformed)
|
|
249
|
+
transformed = SafeConvertTransformer().visit(transformed)
|
|
250
|
+
transformed = SafeDivisionTransformer().visit(transformed)
|
|
251
|
+
# After SafeDivision so wrapped operands (safe_div calls) are bound
|
|
252
|
+
# once by the walrus instead of evaluating twice. Only user/compiled
|
|
253
|
+
# scripts get the guard: pynecore's own lib modules use the raw
|
|
254
|
+
# ``x != x`` nan idiom deliberately, and wrapping it would invert it
|
|
255
|
+
if not path.is_relative_to(Path(__file__).parent.parent):
|
|
256
|
+
transformed = NeGuardTransformer().visit(transformed)
|
|
257
|
+
transformed = apply_layout(transformed, slot_layout)
|
|
258
|
+
|
|
259
|
+
# Debugger-safe variant of ast.fix_missing_locations: synthetic
|
|
260
|
+
# nodes get point anchors, so no prologue bytecode maps onto the
|
|
261
|
+
# function's last line (see transformers/locations.py)
|
|
262
|
+
fix_locations(transformed)
|
|
263
|
+
|
|
264
|
+
# Debug output if requested. The pretty dump and the saved copy go
|
|
265
|
+
# through the display rewrite (named index constants instead of
|
|
266
|
+
# literal slot indexes); the RAW dump stays the exact emission —
|
|
267
|
+
# the AST golden tests compare against it.
|
|
268
|
+
if os.environ.get('PYNE_AST_DEBUG'):
|
|
269
|
+
from pynecore.transformers.display_rewrite import display_dump
|
|
270
|
+
print("-" * 100)
|
|
271
|
+
print(f"Transformed {path}:")
|
|
272
|
+
try:
|
|
273
|
+
from rich.syntax import Syntax # type: ignore
|
|
274
|
+
from rich import print as rprint # type: ignore
|
|
275
|
+
rprint(Syntax(display_dump(transformed, slot_layout), "python",
|
|
276
|
+
word_wrap=True, line_numbers=False))
|
|
277
|
+
except ImportError:
|
|
278
|
+
print(display_dump(transformed, slot_layout))
|
|
279
|
+
print("-" * 100)
|
|
280
|
+
elif raw_filter := os.environ.get('PYNE_AST_DEBUG_RAW'):
|
|
281
|
+
# '1' dumps every transformed module; any other value is a source
|
|
282
|
+
# path filter so a capture is not polluted by modules imported
|
|
283
|
+
# during the transform (callee resolution imports lib submodules)
|
|
284
|
+
if raw_filter == '1' or Path(raw_filter).resolve() == path.resolve():
|
|
285
|
+
print(ast.unparse(transformed))
|
|
286
|
+
|
|
287
|
+
if os.environ.get('PYNE_AST_SAVE'):
|
|
288
|
+
from pynecore.transformers.display_rewrite import display_dump
|
|
289
|
+
Path("/tmp/pyne").mkdir(parents=True, exist_ok=True)
|
|
290
|
+
|
|
291
|
+
with open(f"/tmp/pyne/{path.stem}.py", "w") as f:
|
|
292
|
+
f.write(display_dump(transformed, slot_layout))
|
|
293
|
+
|
|
294
|
+
# Bake a pipeline-identity sentinel into the module body so a loaded code
|
|
295
|
+
# object can be distinguished from foreign or stale bytecode (see get_code).
|
|
296
|
+
# It must survive into the .pyc, so it is a plain assignment the compiler
|
|
297
|
+
# marshals like any other constant — no .pyc-format surgery needed. Added
|
|
298
|
+
# after the debug/save dumps above so those keep showing the semantic
|
|
299
|
+
# transform, free of this loader-level bookkeeping.
|
|
300
|
+
sentinel = ast.Assign(
|
|
301
|
+
targets=[ast.Name(id=_PYNE_SENTINEL, ctx=ast.Store())],
|
|
302
|
+
value=ast.Constant(value=_get_transform_pipeline_hash()),
|
|
303
|
+
)
|
|
304
|
+
# is_pyne_module guarantees body[0] is the module docstring; keep it first,
|
|
305
|
+
# and stay after any ``from __future__`` imports (which must lead the module).
|
|
306
|
+
insert_at = 1
|
|
307
|
+
while (insert_at < len(transformed.body)
|
|
308
|
+
and isinstance(transformed.body[insert_at], ast.ImportFrom)
|
|
309
|
+
and cast(ast.ImportFrom, transformed.body[insert_at]).module == '__future__'):
|
|
310
|
+
insert_at += 1
|
|
311
|
+
transformed.body.insert(insert_at, sentinel)
|
|
312
|
+
ast.fix_missing_locations(transformed)
|
|
313
|
+
|
|
314
|
+
tree = transformed
|
|
315
|
+
|
|
316
|
+
# Let Python handle bytecode caching
|
|
317
|
+
return compile(tree, path, 'exec', optimize=_optimize)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class PyneImportHook:
|
|
321
|
+
"""Import hook that uses PyneLoader"""
|
|
322
|
+
|
|
323
|
+
# noinspection PyMethodMayBeStatic,PyUnusedLocal
|
|
324
|
+
def find_spec(self, fullname: str, path, target=None):
|
|
325
|
+
"""Find and create module spec"""
|
|
326
|
+
if path is None:
|
|
327
|
+
path = sys.path
|
|
328
|
+
|
|
329
|
+
if "." in fullname:
|
|
330
|
+
*_, name = fullname.split(".")
|
|
331
|
+
else:
|
|
332
|
+
name = fullname
|
|
333
|
+
|
|
334
|
+
for entry in path:
|
|
335
|
+
if entry == "":
|
|
336
|
+
entry = "."
|
|
337
|
+
|
|
338
|
+
# Check both module.py and module/__init__.py
|
|
339
|
+
candidates = [
|
|
340
|
+
Path(entry) / f"{name}.py",
|
|
341
|
+
Path(entry) / name / "__init__.py"
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
for py_path in candidates:
|
|
345
|
+
if py_path.exists():
|
|
346
|
+
# Stale/foreign bytecode is handled content-based in
|
|
347
|
+
# ``PyneLoader.get_code`` (via the transform sentinel), so there is
|
|
348
|
+
# no per-path cache bookkeeping to do here.
|
|
349
|
+
return importlib.util.spec_from_file_location(
|
|
350
|
+
fullname,
|
|
351
|
+
py_path,
|
|
352
|
+
loader=PyneLoader(fullname, str(py_path))
|
|
353
|
+
)
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# Install the import hook
|
|
358
|
+
sys.meta_path.insert(0, PyneImportHook())
|