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.
Files changed (261) hide show
  1. opencode_pyneruntime-6.6.4.dist-info/METADATA +281 -0
  2. opencode_pyneruntime-6.6.4.dist-info/RECORD +261 -0
  3. opencode_pyneruntime-6.6.4.dist-info/WHEEL +5 -0
  4. opencode_pyneruntime-6.6.4.dist-info/entry_points.txt +6 -0
  5. opencode_pyneruntime-6.6.4.dist-info/licenses/LICENSE +201 -0
  6. opencode_pyneruntime-6.6.4.dist-info/licenses/NOTICE +21 -0
  7. opencode_pyneruntime-6.6.4.dist-info/top_level.txt +1 -0
  8. pynecore/__init__.py +6 -0
  9. pynecore/cli/__init__.py +2 -0
  10. pynecore/cli/app.py +238 -0
  11. pynecore/cli/commands/__init__.py +343 -0
  12. pynecore/cli/commands/benchmark.py +186 -0
  13. pynecore/cli/commands/compile.py +198 -0
  14. pynecore/cli/commands/data.py +857 -0
  15. pynecore/cli/commands/debug.py +63 -0
  16. pynecore/cli/commands/optimize.py +956 -0
  17. pynecore/cli/commands/plugin.py +242 -0
  18. pynecore/cli/commands/run.py +2006 -0
  19. pynecore/cli/pluggable.py +132 -0
  20. pynecore/cli/utils/__init__.py +0 -0
  21. pynecore/cli/utils/api_error_handler.py +168 -0
  22. pynecore/cli/utils/broker_picker.py +330 -0
  23. pynecore/cli/utils/error_hook.py +28 -0
  24. pynecore/cli/utils/keyreader.py +178 -0
  25. pynecore/cli/utils/provider_picker.py +19 -0
  26. pynecore/cli/utils/symbol_browser.py +1149 -0
  27. pynecore/core/__init__.py +0 -0
  28. pynecore/core/aggregator.py +257 -0
  29. pynecore/core/bar_magnifier.py +168 -0
  30. pynecore/core/broker/__init__.py +64 -0
  31. pynecore/core/broker/defaults.py +113 -0
  32. pynecore/core/broker/disappearance.py +927 -0
  33. pynecore/core/broker/emulator.py +345 -0
  34. pynecore/core/broker/exceptions.py +346 -0
  35. pynecore/core/broker/idempotency.py +401 -0
  36. pynecore/core/broker/intent_builder.py +334 -0
  37. pynecore/core/broker/journal.py +1785 -0
  38. pynecore/core/broker/models.py +1600 -0
  39. pynecore/core/broker/native_failsafe_manager.py +1436 -0
  40. pynecore/core/broker/one_way_emulator.py +1128 -0
  41. pynecore/core/broker/position.py +787 -0
  42. pynecore/core/broker/run_identity.py +126 -0
  43. pynecore/core/broker/software_entry_stop_engine.py +351 -0
  44. pynecore/core/broker/software_partial_bracket_engine.py +1379 -0
  45. pynecore/core/broker/spot_inventory.py +1327 -0
  46. pynecore/core/broker/storage.py +2655 -0
  47. pynecore/core/broker/store_helpers.py +2161 -0
  48. pynecore/core/broker/sync_engine.py +16070 -0
  49. pynecore/core/broker/validation.py +382 -0
  50. pynecore/core/class_property.py +7 -0
  51. pynecore/core/config.py +392 -0
  52. pynecore/core/csv_file.py +547 -0
  53. pynecore/core/currency.py +262 -0
  54. pynecore/core/data_converter.py +1002 -0
  55. pynecore/core/datetime.py +296 -0
  56. pynecore/core/download_info.py +71 -0
  57. pynecore/core/download_runner.py +274 -0
  58. pynecore/core/htf_aggregator.py +181 -0
  59. pynecore/core/import_hook.py +358 -0
  60. pynecore/core/instance_state.py +494 -0
  61. pynecore/core/live_ltf_collector.py +442 -0
  62. pynecore/core/live_ltf_window.py +189 -0
  63. pynecore/core/live_runner.py +1347 -0
  64. pynecore/core/module_property.py +26 -0
  65. pynecore/core/ohlcv_file.py +1888 -0
  66. pynecore/core/overload.py +371 -0
  67. pynecore/core/pine_cast.py +113 -0
  68. pynecore/core/pine_export.py +95 -0
  69. pynecore/core/pine_method.py +244 -0
  70. pynecore/core/pine_range.py +86 -0
  71. pynecore/core/pine_udt.py +69 -0
  72. pynecore/core/plugin/__init__.py +394 -0
  73. pynecore/core/plugin/broker.py +781 -0
  74. pynecore/core/plugin/cli.py +96 -0
  75. pynecore/core/plugin/live_provider.py +208 -0
  76. pynecore/core/plugin/provider.py +331 -0
  77. pynecore/core/provider_string.py +148 -0
  78. pynecore/core/random.py +40 -0
  79. pynecore/core/resampler.py +686 -0
  80. pynecore/core/safe_convert.py +64 -0
  81. pynecore/core/script.py +1011 -0
  82. pynecore/core/script_runner.py +3202 -0
  83. pynecore/core/security.py +1749 -0
  84. pynecore/core/security_process.py +1253 -0
  85. pynecore/core/security_shm.py +456 -0
  86. pynecore/core/series.py +417 -0
  87. pynecore/core/strategy_stats.py +669 -0
  88. pynecore/core/symbol_map.py +134 -0
  89. pynecore/core/syminfo.py +505 -0
  90. pynecore/core/viz.py +591 -0
  91. pynecore/lib/__init__.py +1771 -0
  92. pynecore/lib/_fixnan.py +32 -0
  93. pynecore/lib/_math_stateful.py +202 -0
  94. pynecore/lib/_timeframe_change.py +101 -0
  95. pynecore/lib/adjustment.py +6 -0
  96. pynecore/lib/alert.py +39 -0
  97. pynecore/lib/alert.pyi +14 -0
  98. pynecore/lib/array.py +1051 -0
  99. pynecore/lib/barmerge.py +60 -0
  100. pynecore/lib/barstate.py +30 -0
  101. pynecore/lib/box.py +415 -0
  102. pynecore/lib/chart.py +128 -0
  103. pynecore/lib/color.py +152 -0
  104. pynecore/lib/color.pyi +50 -0
  105. pynecore/lib/currency.py +62 -0
  106. pynecore/lib/dayofweek.py +36 -0
  107. pynecore/lib/dayofweek.pyi +18 -0
  108. pynecore/lib/display.py +8 -0
  109. pynecore/lib/dividends.py +9 -0
  110. pynecore/lib/earnings.py +11 -0
  111. pynecore/lib/extend.py +6 -0
  112. pynecore/lib/font.py +5 -0
  113. pynecore/lib/footprint.py +79 -0
  114. pynecore/lib/format.py +11 -0
  115. pynecore/lib/hline.py +67 -0
  116. pynecore/lib/hline.pyi +24 -0
  117. pynecore/lib/label.py +409 -0
  118. pynecore/lib/line.py +433 -0
  119. pynecore/lib/linefill.py +93 -0
  120. pynecore/lib/location.py +11 -0
  121. pynecore/lib/log.py +362 -0
  122. pynecore/lib/map.py +150 -0
  123. pynecore/lib/math.py +385 -0
  124. pynecore/lib/matrix.py +708 -0
  125. pynecore/lib/order.py +8 -0
  126. pynecore/lib/pivotpointtype.py +8 -0
  127. pynecore/lib/plot.py +95 -0
  128. pynecore/lib/plot.pyi +33 -0
  129. pynecore/lib/polyline.py +91 -0
  130. pynecore/lib/position.py +15 -0
  131. pynecore/lib/request.py +281 -0
  132. pynecore/lib/runtime.py +5 -0
  133. pynecore/lib/scale.py +9 -0
  134. pynecore/lib/session.py +267 -0
  135. pynecore/lib/session.pyi +12 -0
  136. pynecore/lib/shape.py +18 -0
  137. pynecore/lib/size.py +12 -0
  138. pynecore/lib/splits.py +4 -0
  139. pynecore/lib/strategy/__init__.py +4778 -0
  140. pynecore/lib/strategy/closedtrades.py +347 -0
  141. pynecore/lib/strategy/closedtrades.pyi +53 -0
  142. pynecore/lib/strategy/commission.py +9 -0
  143. pynecore/lib/strategy/direction.py +9 -0
  144. pynecore/lib/strategy/oca.py +13 -0
  145. pynecore/lib/strategy/opentrades.py +281 -0
  146. pynecore/lib/strategy/opentrades.pyi +49 -0
  147. pynecore/lib/strategy/risk.py +109 -0
  148. pynecore/lib/string.py +649 -0
  149. pynecore/lib/syminfo.py +84 -0
  150. pynecore/lib/ta.py +2230 -0
  151. pynecore/lib/table.py +290 -0
  152. pynecore/lib/text.py +17 -0
  153. pynecore/lib/ticker.py +207 -0
  154. pynecore/lib/timeframe.py +293 -0
  155. pynecore/lib/volume_row.py +67 -0
  156. pynecore/lib/xloc.py +4 -0
  157. pynecore/lib/yloc.py +5 -0
  158. pynecore/providers/__init__.py +0 -0
  159. pynecore/providers/ccxt.py +664 -0
  160. pynecore/providers/replay.py +187 -0
  161. pynecore/pynesys/__init__.py +0 -0
  162. pynecore/pynesys/api.py +498 -0
  163. pynecore/pynesys/compiler.py +112 -0
  164. pynecore/standalone.py +99 -0
  165. pynecore/testing/__init__.py +1 -0
  166. pynecore/testing/broker_lab/__init__.py +41 -0
  167. pynecore/testing/broker_lab/__main__.py +5 -0
  168. pynecore/testing/broker_lab/cli.py +87 -0
  169. pynecore/testing/broker_lab/generate.py +47 -0
  170. pynecore/testing/broker_lab/model.py +84 -0
  171. pynecore/testing/broker_lab/reference.py +645 -0
  172. pynecore/testing/broker_lab/runner.py +372 -0
  173. pynecore/testing/broker_lab/scheduler.py +50 -0
  174. pynecore/testing/broker_lab/subprocess.py +73 -0
  175. pynecore/transformers/__init__.py +0 -0
  176. pynecore/transformers/builtin_shadow.py +136 -0
  177. pynecore/transformers/closure_arguments_transformer.py +428 -0
  178. pynecore/transformers/display_rewrite.py +140 -0
  179. pynecore/transformers/dynamic_default.py +147 -0
  180. pynecore/transformers/function_isolation.py +757 -0
  181. pynecore/transformers/import_lifter.py +61 -0
  182. pynecore/transformers/import_normalizer.py +328 -0
  183. pynecore/transformers/inline_series_hoist.py +178 -0
  184. pynecore/transformers/input_transformer.py +175 -0
  185. pynecore/transformers/lib_series.py +201 -0
  186. pynecore/transformers/locations.py +70 -0
  187. pynecore/transformers/module_properties.json +3387 -0
  188. pynecore/transformers/module_property.py +221 -0
  189. pynecore/transformers/ne_guard.py +70 -0
  190. pynecore/transformers/persistent.py +320 -0
  191. pynecore/transformers/persistent_series.py +76 -0
  192. pynecore/transformers/safe_convert_transformer.py +97 -0
  193. pynecore/transformers/safe_division_transformer.py +95 -0
  194. pynecore/transformers/script_requirements.py +308 -0
  195. pynecore/transformers/security.py +752 -0
  196. pynecore/transformers/security_instantiation.py +274 -0
  197. pynecore/transformers/series.py +275 -0
  198. pynecore/transformers/slot_layout.py +381 -0
  199. pynecore/transformers/type_checking_stripper.py +25 -0
  200. pynecore/transformers/unused_series_detector.py +267 -0
  201. pynecore/types/__init__.py +21 -0
  202. pynecore/types/alert.py +5 -0
  203. pynecore/types/barmerge.py +5 -0
  204. pynecore/types/base.py +39 -0
  205. pynecore/types/box.py +37 -0
  206. pynecore/types/chart.py +17 -0
  207. pynecore/types/color.py +107 -0
  208. pynecore/types/currency.py +5 -0
  209. pynecore/types/datetime.py +6 -0
  210. pynecore/types/display.py +5 -0
  211. pynecore/types/dividends.py +5 -0
  212. pynecore/types/earnings.py +5 -0
  213. pynecore/types/extend.py +5 -0
  214. pynecore/types/font.py +5 -0
  215. pynecore/types/footprint.py +41 -0
  216. pynecore/types/format.py +5 -0
  217. pynecore/types/hline.py +24 -0
  218. pynecore/types/ib_persistent.py +8 -0
  219. pynecore/types/ib_persistent.pyi +10 -0
  220. pynecore/types/label.py +35 -0
  221. pynecore/types/line.py +32 -0
  222. pynecore/types/linefill.py +13 -0
  223. pynecore/types/location.py +5 -0
  224. pynecore/types/matrix.py +999 -0
  225. pynecore/types/na.py +237 -0
  226. pynecore/types/na.pyi +83 -0
  227. pynecore/types/ohlcv.py +12 -0
  228. pynecore/types/order.py +5 -0
  229. pynecore/types/persistent.py +8 -0
  230. pynecore/types/persistent.pyi +13 -0
  231. pynecore/types/pine_types.py +11 -0
  232. pynecore/types/pine_types.pyi +15 -0
  233. pynecore/types/pivotpointtype.py +5 -0
  234. pynecore/types/plot.py +12 -0
  235. pynecore/types/plot_meta.py +60 -0
  236. pynecore/types/polyline.py +40 -0
  237. pynecore/types/position.py +5 -0
  238. pynecore/types/scale.py +5 -0
  239. pynecore/types/script_type.py +15 -0
  240. pynecore/types/series.py +23 -0
  241. pynecore/types/series.pyi +19 -0
  242. pynecore/types/session.py +35 -0
  243. pynecore/types/shape.py +5 -0
  244. pynecore/types/size.py +5 -0
  245. pynecore/types/source.py +33 -0
  246. pynecore/types/splits.py +5 -0
  247. pynecore/types/strategy.py +45 -0
  248. pynecore/types/table.py +87 -0
  249. pynecore/types/text.py +13 -0
  250. pynecore/types/type_checker.py +7 -0
  251. pynecore/types/type_checker.pyi +48 -0
  252. pynecore/types/volume_row.py +36 -0
  253. pynecore/types/weekdays.py +11 -0
  254. pynecore/types/xloc.py +5 -0
  255. pynecore/types/yloc.py +5 -0
  256. pynecore/utils/__init__.py +0 -0
  257. pynecore/utils/file_utils.py +50 -0
  258. pynecore/utils/rich/__init__.py +0 -0
  259. pynecore/utils/rich/date_column.py +25 -0
  260. pynecore/utils/sequence_view.py +92 -0
  261. 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())