darkpyonix 0.3.0__py3-none-win_amd64.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.
- darkpyonix/__init__.py +43 -0
- darkpyonix/__main__.py +263 -0
- darkpyonix/_binding.py +134 -0
- darkpyonix/_capture.py +510 -0
- darkpyonix/_client.py +215 -0
- darkpyonix/_collab.py +890 -0
- darkpyonix/_command.py +203 -0
- darkpyonix/_discovery.py +327 -0
- darkpyonix/_display.py +132 -0
- darkpyonix/_document.py +123 -0
- darkpyonix/_events.py +77 -0
- darkpyonix/_executor.py +714 -0
- darkpyonix/_figures.py +80 -0
- darkpyonix/_home.py +159 -0
- darkpyonix/_host.py +38 -0
- darkpyonix/_hostctx.py +85 -0
- darkpyonix/_launcher.py +150 -0
- darkpyonix/_lock.py +70 -0
- darkpyonix/_markdown.py +29 -0
- darkpyonix/_misc.py +66 -0
- darkpyonix/_model.py +86 -0
- darkpyonix/_mplbackend.py +53 -0
- darkpyonix/_params.py +201 -0
- darkpyonix/_peer.py +57 -0
- darkpyonix/_protocol.py +198 -0
- darkpyonix/_runs.py +748 -0
- darkpyonix/_server.py +564 -0
- darkpyonix/_streams.py +469 -0
- darkpyonix/_tokens.py +193 -0
- darkpyonix/_winpipe.py +516 -0
- darkpyonix/_wsync.py +453 -0
- darkpyonix/format/__init__.py +128 -0
- darkpyonix/format/_parser.py +119 -0
- darkpyonix-0.3.0.data/scripts/darkpyonix.exe +0 -0
- darkpyonix-0.3.0.dist-info/METADATA +140 -0
- darkpyonix-0.3.0.dist-info/RECORD +38 -0
- darkpyonix-0.3.0.dist-info/WHEEL +4 -0
- darkpyonix-0.3.0.dist-info/licenses/LICENSE +202 -0
darkpyonix/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""DarkPyonix runtime API for notebook files (SPEC FR-F2..F6, FORMAT §4).
|
|
2
|
+
|
|
3
|
+
Standard library only (INTENT §2). Importing this package must stay cheap and must never
|
|
4
|
+
import any kernel-process module (``_server``, ``_executor``, ...): notebook code and the kernel
|
|
5
|
+
import it from interpreters where nothing but the standard library is available. The public names are loaded lazily
|
|
6
|
+
from private modules on first access (PEP 562).
|
|
7
|
+
|
|
8
|
+
Inside a kernel the calls talk to ``darkpyonix._hostctx``; anywhere else they behave
|
|
9
|
+
as under plain ``python file.py`` (SPEC FR-F5).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.3.0"
|
|
13
|
+
|
|
14
|
+
_EXPORTS = {
|
|
15
|
+
"markdown": "_markdown",
|
|
16
|
+
"params": "_params",
|
|
17
|
+
"binding": "_binding",
|
|
18
|
+
"run_command": "_command",
|
|
19
|
+
"uv": "_command",
|
|
20
|
+
"pip": "_command",
|
|
21
|
+
"display": "_misc",
|
|
22
|
+
"run_parallel": "_misc",
|
|
23
|
+
"run_cinterop": "_misc",
|
|
24
|
+
"run_cppinterop": "_misc",
|
|
25
|
+
"run_rustinterop": "_misc",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
__all__ = sorted(_EXPORTS)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name):
|
|
32
|
+
module = _EXPORTS.get(name)
|
|
33
|
+
if module is None:
|
|
34
|
+
raise AttributeError("module 'darkpyonix' has no attribute %r" % (name,))
|
|
35
|
+
import importlib
|
|
36
|
+
|
|
37
|
+
value = getattr(importlib.import_module("darkpyonix." + module), name)
|
|
38
|
+
globals()[name] = value
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __dir__():
|
|
43
|
+
return sorted(set(globals()) | set(_EXPORTS))
|
darkpyonix/__main__.py
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""The DarkPyonix kernel process (INTENT D1–D3, D12; PROTOCOL §2–§3).
|
|
2
|
+
|
|
3
|
+
Started as ``python -m darkpyonix`` (or by ``_launcher.bootstrap_command``). Owns the per-file
|
|
4
|
+
lock, the shared document (SPEC §10a, ``_collab.DocumentState``), the run store, the executor
|
|
5
|
+
(user code on the main thread), the DKP/1 control server and discovery. Standard library only;
|
|
6
|
+
Python 3.8+.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import signal
|
|
15
|
+
import socket
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from darkpyonix import _home
|
|
22
|
+
from darkpyonix import _lock as lock
|
|
23
|
+
from darkpyonix._collab import DocumentState
|
|
24
|
+
from darkpyonix._discovery import DiscoveryResponder
|
|
25
|
+
from darkpyonix._events import EventLog
|
|
26
|
+
from darkpyonix._executor import Executor
|
|
27
|
+
from darkpyonix._launcher import bootstrap_command
|
|
28
|
+
from darkpyonix._protocol import (
|
|
29
|
+
EXIT_ALREADY_RUNNING, KERNEL_VERSION, DKPError, canonical_path, kernel_id_for, now_iso,
|
|
30
|
+
)
|
|
31
|
+
from darkpyonix._runs import RunStore, runs_dir_for
|
|
32
|
+
from darkpyonix._tokens import TokenStore
|
|
33
|
+
|
|
34
|
+
FINISHED = ("ok", "error", "interrupted", "cancelled", "crashed")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _log(stream, message: str) -> None:
|
|
38
|
+
try:
|
|
39
|
+
stream.write("[darkpyonix-kernel %s] %s\n" % (now_iso(), message))
|
|
40
|
+
stream.flush()
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Kernel(object):
|
|
46
|
+
def __init__(self, path: str) -> None:
|
|
47
|
+
self.path = canonical_path(path)
|
|
48
|
+
self.kernel_id = kernel_id_for(self.path)
|
|
49
|
+
self.user_tag = _home.user_tag()
|
|
50
|
+
self.lock = lock.FileLock(lock.lock_path(self.kernel_id))
|
|
51
|
+
self.events = EventLog()
|
|
52
|
+
self.collab = None # type: Optional[DocumentState]
|
|
53
|
+
self.store = None # type: Optional[RunStore]
|
|
54
|
+
self.tokens = None # type: Optional[TokenStore]
|
|
55
|
+
self.executor = None # type: Optional[Executor]
|
|
56
|
+
self.server = None
|
|
57
|
+
self.discovery = None # type: Optional[DiscoveryResponder]
|
|
58
|
+
self.extra_handlers = {} # type: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]]
|
|
59
|
+
self.started_at = now_iso()
|
|
60
|
+
self.python = {
|
|
61
|
+
"version": platform.python_version(),
|
|
62
|
+
"implementation": platform.python_implementation(),
|
|
63
|
+
"executable": sys.executable,
|
|
64
|
+
}
|
|
65
|
+
self.host = socket.gethostname()
|
|
66
|
+
self.diag = sys.stderr
|
|
67
|
+
self._shutdown_started = threading.Event()
|
|
68
|
+
|
|
69
|
+
# -------------------------------------------------------------- discovery info
|
|
70
|
+
|
|
71
|
+
def info(self) -> Dict[str, Any]:
|
|
72
|
+
st = self.executor.status() if self.executor is not None else {"status": "starting"}
|
|
73
|
+
return {
|
|
74
|
+
"kernel_id": self.kernel_id,
|
|
75
|
+
"path": self.path,
|
|
76
|
+
"pid": os.getpid(),
|
|
77
|
+
"control": self.server.address if self.server is not None else "",
|
|
78
|
+
"status": st.get("status", "starting"),
|
|
79
|
+
"run_id": st.get("run_id"),
|
|
80
|
+
"python": self.python,
|
|
81
|
+
"kernel_version": KERNEL_VERSION,
|
|
82
|
+
"runs_dir": runs_dir_for(self.path),
|
|
83
|
+
"started_at": self.started_at,
|
|
84
|
+
"host": self.host,
|
|
85
|
+
"home": _home.home(),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
def emit(self, type: str, data: Dict[str, Any]) -> None:
|
|
89
|
+
self.events.append(type, data)
|
|
90
|
+
if type == "kernel.status" and self.discovery is not None:
|
|
91
|
+
self.discovery.announce_now()
|
|
92
|
+
|
|
93
|
+
# -------------------------------------------------------------- handlers
|
|
94
|
+
|
|
95
|
+
def _status(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
96
|
+
out = self.info()
|
|
97
|
+
out.update(self.executor.status())
|
|
98
|
+
return out
|
|
99
|
+
|
|
100
|
+
def _runs_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
|
+
limit = int(params.get("limit", 20))
|
|
102
|
+
return {"runs": self.store.list(limit)}
|
|
103
|
+
|
|
104
|
+
def _runs_get(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
105
|
+
ref = params.get("run_id")
|
|
106
|
+
if not isinstance(ref, str):
|
|
107
|
+
raise DKPError("bad_request", "run_id is required")
|
|
108
|
+
return self.store.get(ref)
|
|
109
|
+
|
|
110
|
+
def _runs_wait(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
111
|
+
"""SPEC FR-S7: return when the run finishes, or after ``timeout`` seconds."""
|
|
112
|
+
ref = params.get("run_id")
|
|
113
|
+
timeout = max(1.0, min(float(params.get("timeout", 60)), 300.0))
|
|
114
|
+
if ref == "current":
|
|
115
|
+
ref = self.executor.status().get("run_id")
|
|
116
|
+
if ref is None:
|
|
117
|
+
raise DKPError("not_found", "no run is executing")
|
|
118
|
+
deadline = time.monotonic() + timeout
|
|
119
|
+
while True:
|
|
120
|
+
summary = self._summary(ref)
|
|
121
|
+
if summary is not None and summary.get("status") in FINISHED:
|
|
122
|
+
return {"status": summary["status"], "run_id": summary["run_id"], "run": summary}
|
|
123
|
+
if time.monotonic() >= deadline:
|
|
124
|
+
if summary is None:
|
|
125
|
+
raise DKPError("not_found", "unknown run %r" % ref)
|
|
126
|
+
return {"status": summary.get("status", "running"), "run_id": summary["run_id"],
|
|
127
|
+
"run": summary}
|
|
128
|
+
time.sleep(0.1)
|
|
129
|
+
|
|
130
|
+
def _summary(self, ref: str) -> Optional[Dict[str, Any]]:
|
|
131
|
+
for s in self.store.list(500):
|
|
132
|
+
if ref == "latest" and s.get("status") in FINISHED:
|
|
133
|
+
return s
|
|
134
|
+
if s.get("run_id") == ref:
|
|
135
|
+
return s
|
|
136
|
+
st = self.executor.status()
|
|
137
|
+
if ref in st.get("queue", []):
|
|
138
|
+
return {"run_id": ref, "status": "queued"}
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
def _run(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
142
|
+
"""PROTOCOL §3.3 ``run``; ``cell_ids`` (FR-S6) name cells of the shared document."""
|
|
143
|
+
params = dict(params or {}) if isinstance(params, dict) else params
|
|
144
|
+
if isinstance(params, dict) and params.get("cell_ids") is not None:
|
|
145
|
+
ids = params.pop("cell_ids")
|
|
146
|
+
if not isinstance(ids, list) or not ids or not all(isinstance(c, str) for c in ids):
|
|
147
|
+
raise DKPError("bad_request", "cell_ids must be a non-empty list of strings")
|
|
148
|
+
if params.get("cells") is not None:
|
|
149
|
+
raise DKPError("bad_request", "give cells or cell_ids, not both")
|
|
150
|
+
params["cells"] = self.collab.indices_for(ids)
|
|
151
|
+
params["mode"] = "cells"
|
|
152
|
+
self.collab.flush() # debounced edits reach the file before the run reads it
|
|
153
|
+
return self.executor.submit(params)
|
|
154
|
+
|
|
155
|
+
def _before_load(self) -> List[str]:
|
|
156
|
+
"""Executor hook, right before a (possibly queued) run reads the file."""
|
|
157
|
+
self.collab.flush()
|
|
158
|
+
return self.collab.cell_ids()
|
|
159
|
+
|
|
160
|
+
def _shutdown(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
161
|
+
self.request_shutdown()
|
|
162
|
+
return {"shutting_down": True}
|
|
163
|
+
|
|
164
|
+
def handlers(self) -> Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]]:
|
|
165
|
+
ex = self.executor
|
|
166
|
+
h = {
|
|
167
|
+
"status": self._status,
|
|
168
|
+
"run": self._run,
|
|
169
|
+
"cancel": lambda p: ex.cancel(p.get("run_id")),
|
|
170
|
+
"interrupt": lambda p: ex.interrupt(p.get("client")),
|
|
171
|
+
"restart": lambda p: ex.restart(bool(p.get("hard", False))),
|
|
172
|
+
"shutdown": self._shutdown,
|
|
173
|
+
"namespace": lambda p: ex.namespace(int(p.get("limit", 200))),
|
|
174
|
+
"runs.list": self._runs_list,
|
|
175
|
+
"runs.get": self._runs_get,
|
|
176
|
+
"runs.wait": self._runs_wait,
|
|
177
|
+
} # type: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]]
|
|
178
|
+
h.update(self.extra_handlers)
|
|
179
|
+
return h
|
|
180
|
+
|
|
181
|
+
def request_shutdown(self) -> None:
|
|
182
|
+
if self._shutdown_started.is_set():
|
|
183
|
+
return
|
|
184
|
+
self._shutdown_started.set()
|
|
185
|
+
threading.Thread(target=self.executor.shutdown, name="dpx-shutdown", daemon=True).start()
|
|
186
|
+
|
|
187
|
+
# -------------------------------------------------------------- lifecycle
|
|
188
|
+
|
|
189
|
+
def run(self) -> int:
|
|
190
|
+
if not self.lock.acquire():
|
|
191
|
+
from darkpyonix import _discovery as discovery
|
|
192
|
+
found = discovery.discover(self.kernel_id, timeout=0.5)
|
|
193
|
+
existing = found[0] if found else {"kernel_id": self.kernel_id}
|
|
194
|
+
sys.stderr.write(json.dumps(existing, ensure_ascii=False) + "\n")
|
|
195
|
+
sys.stderr.flush()
|
|
196
|
+
return EXIT_ALREADY_RUNNING
|
|
197
|
+
try:
|
|
198
|
+
_home.expose_package()
|
|
199
|
+
self.collab = DocumentState(self.path, self.events.append,
|
|
200
|
+
seq_provider=lambda: self.events.seq)
|
|
201
|
+
self.extra_handlers.update(self.collab.handlers())
|
|
202
|
+
self.tokens = TokenStore(self.path) # FR-K10
|
|
203
|
+
self.extra_handlers.update(self.tokens.handlers())
|
|
204
|
+
self.store = RunStore(self.path, self.kernel_id)
|
|
205
|
+
crashed = self.store.recover_crashed()
|
|
206
|
+
if crashed:
|
|
207
|
+
_log(self.diag, "marked crashed runs: %s" % ", ".join(crashed))
|
|
208
|
+
self.executor = Executor(self.path, self.kernel_id, self.emit, self.store,
|
|
209
|
+
self.python, self.host)
|
|
210
|
+
self.executor.before_load = self._before_load
|
|
211
|
+
self.diag = getattr(self.executor, "original_stderr", sys.stderr)
|
|
212
|
+
|
|
213
|
+
from darkpyonix._server import ControlServer
|
|
214
|
+
self.server = ControlServer(self.kernel_id, self.events, self.handlers())
|
|
215
|
+
self.tokens.close_streams = self.server.streams.close_share # FR-K9, PR-6
|
|
216
|
+
self.collab.start()
|
|
217
|
+
self.tokens.start()
|
|
218
|
+
self.server.start()
|
|
219
|
+
self.discovery = DiscoveryResponder(self.info, self.user_tag)
|
|
220
|
+
self.discovery.start()
|
|
221
|
+
|
|
222
|
+
signal.signal(signal.SIGTERM, lambda signum, frame: self.request_shutdown())
|
|
223
|
+
if hasattr(signal, "SIGHUP"):
|
|
224
|
+
signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
|
225
|
+
_log(self.diag, "kernel %s for %s on %s" % (self.kernel_id, self.path, self.server.address))
|
|
226
|
+
|
|
227
|
+
self.executor.run_forever() # main thread until shutdown / hard restart
|
|
228
|
+
finally:
|
|
229
|
+
hard = bool(self.executor is not None and self.executor.hard_restart_requested)
|
|
230
|
+
self._teardown()
|
|
231
|
+
if hard:
|
|
232
|
+
_log(sys.stderr, "hard restart")
|
|
233
|
+
cmd = bootstrap_command(sys.executable, self.path)
|
|
234
|
+
os.execv(cmd[0], cmd)
|
|
235
|
+
return 0
|
|
236
|
+
|
|
237
|
+
def _teardown(self) -> None:
|
|
238
|
+
for step in (
|
|
239
|
+
lambda: self.store.flush() if self.store is not None else None,
|
|
240
|
+
lambda: self.discovery.stop(bye=True) if self.discovery is not None else None,
|
|
241
|
+
lambda: self.server.stop() if self.server is not None else None,
|
|
242
|
+
lambda: self.collab.stop() if self.collab is not None else None,
|
|
243
|
+
lambda: self.tokens.stop() if self.tokens is not None else None,
|
|
244
|
+
lambda: self.lock.release(),
|
|
245
|
+
):
|
|
246
|
+
try:
|
|
247
|
+
step()
|
|
248
|
+
except Exception as e: # never let teardown keep the lock
|
|
249
|
+
_log(sys.stderr, "teardown: %r" % (e,))
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
253
|
+
parser = argparse.ArgumentParser(prog="darkpyonix-kernel")
|
|
254
|
+
parser.add_argument("--file", required=True)
|
|
255
|
+
args, _unknown = parser.parse_known_args(argv)
|
|
256
|
+
if not os.path.isfile(args.file):
|
|
257
|
+
sys.stderr.write("darkpyonix-kernel: no such file: %s\n" % args.file)
|
|
258
|
+
return 2
|
|
259
|
+
return Kernel(args.file).run()
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
if __name__ == "__main__":
|
|
263
|
+
sys.exit(main(sys.argv[1:]))
|
darkpyonix/_binding.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""``@darkpyonix.binding`` (SPEC FR-F4, FORMAT §3.4, issue #6).
|
|
2
|
+
|
|
3
|
+
The decorator re-evaluates the decorated ``def`` / ``class`` from its source in a snapshot of
|
|
4
|
+
the defining module's globals taken at definition time. Imports and earlier bindings are in
|
|
5
|
+
the snapshot; ``[code]`` variables assigned later are not, so referencing them raises
|
|
6
|
+
``NameError``. Only ``binding`` (and decorators written above it, which the original statement
|
|
7
|
+
applies afterwards anyway) is stripped; decorators below it are re-applied.
|
|
8
|
+
|
|
9
|
+
Works the same in a kernel and under plain ``python file.py`` (SPEC FR-F5). Python 3.8
|
|
10
|
+
compatible: no ``ast.unparse``; decorator lines are blanked in the original source instead, so
|
|
11
|
+
line numbers in tracebacks still point at the file.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import __future__
|
|
16
|
+
import ast
|
|
17
|
+
import builtins
|
|
18
|
+
import linecache
|
|
19
|
+
import sys
|
|
20
|
+
import textwrap
|
|
21
|
+
import warnings
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_FUTURE_FLAGS = 0
|
|
25
|
+
for _feature in __future__.all_feature_names:
|
|
26
|
+
_FUTURE_FLAGS |= getattr(__future__, _feature).compiler_flag
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Unresolved(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _resolve(expr, frame):
|
|
34
|
+
"""Resolve a dotted-name decorator expression without evaluating anything else."""
|
|
35
|
+
if isinstance(expr, ast.Name):
|
|
36
|
+
for scope in (frame.f_locals, frame.f_globals):
|
|
37
|
+
if expr.id in scope:
|
|
38
|
+
return scope[expr.id]
|
|
39
|
+
if hasattr(builtins, expr.id):
|
|
40
|
+
return getattr(builtins, expr.id)
|
|
41
|
+
raise _Unresolved(expr.id)
|
|
42
|
+
if isinstance(expr, ast.Attribute):
|
|
43
|
+
base = _resolve(expr.value, frame)
|
|
44
|
+
try:
|
|
45
|
+
return getattr(base, expr.attr)
|
|
46
|
+
except Exception:
|
|
47
|
+
raise _Unresolved(expr.attr) from None
|
|
48
|
+
raise _Unresolved(type(expr).__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _first_line(node):
|
|
52
|
+
return min([node.lineno] + [d.lineno for d in node.decorator_list])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _find_node(tree, name, lineno):
|
|
56
|
+
"""The def/class named ``name`` whose span (decorators included) contains ``lineno``."""
|
|
57
|
+
kinds = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
58
|
+
best = None
|
|
59
|
+
for node in ast.walk(tree):
|
|
60
|
+
if isinstance(node, kinds) and node.name == name and node.decorator_list:
|
|
61
|
+
end = getattr(node, "end_lineno", None) or node.lineno
|
|
62
|
+
if _first_line(node) <= lineno <= end:
|
|
63
|
+
# Innermost match wins (a nested definition with the same name).
|
|
64
|
+
if best is None or _first_line(node) >= _first_line(best):
|
|
65
|
+
best = node
|
|
66
|
+
return best
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _source_for(obj, frame):
|
|
70
|
+
"""(filename, all lines, node) for the decorated definition, or None."""
|
|
71
|
+
filename = frame.f_code.co_filename
|
|
72
|
+
lines = linecache.getlines(filename, frame.f_globals)
|
|
73
|
+
if not lines:
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
tree = ast.parse("".join(lines), filename)
|
|
77
|
+
except (SyntaxError, ValueError):
|
|
78
|
+
return None
|
|
79
|
+
node = _find_node(tree, obj.__name__, frame.f_lineno)
|
|
80
|
+
if node is None:
|
|
81
|
+
code = getattr(obj, "__code__", None)
|
|
82
|
+
if code is not None and code.co_filename == filename:
|
|
83
|
+
node = _find_node(tree, obj.__name__, code.co_firstlineno)
|
|
84
|
+
if node is None:
|
|
85
|
+
return None
|
|
86
|
+
return filename, lines, node
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def binding(obj):
|
|
90
|
+
"""Re-evaluate ``obj`` (a def or class) so it cannot see later ``[code]`` variables."""
|
|
91
|
+
name = getattr(obj, "__name__", None)
|
|
92
|
+
frame = sys._getframe(1)
|
|
93
|
+
try:
|
|
94
|
+
found = _source_for(obj, frame) if name else None
|
|
95
|
+
if found is None:
|
|
96
|
+
raise _Unresolved("source of %r is not available" % (name or obj,))
|
|
97
|
+
filename, lines, node = found
|
|
98
|
+
|
|
99
|
+
cut = -1
|
|
100
|
+
for i, dec in enumerate(node.decorator_list):
|
|
101
|
+
try:
|
|
102
|
+
if _resolve(dec, frame) is binding:
|
|
103
|
+
cut = i
|
|
104
|
+
except _Unresolved:
|
|
105
|
+
continue
|
|
106
|
+
if cut < 0:
|
|
107
|
+
raise _Unresolved("could not find the binding decorator on %r" % name)
|
|
108
|
+
|
|
109
|
+
first = _first_line(node)
|
|
110
|
+
end = node.end_lineno
|
|
111
|
+
segment = list(lines[first - 1:end])
|
|
112
|
+
# Blank the binding decorator and those above it; they are applied by the original
|
|
113
|
+
# statement. Decorators below it are kept and re-applied in the snapshot.
|
|
114
|
+
for dec in node.decorator_list[:cut + 1]:
|
|
115
|
+
# The '@' sits on the decorator's first line (or the line before a parenthesised one).
|
|
116
|
+
start = dec.lineno
|
|
117
|
+
while start > first and not segment[start - first].lstrip().startswith("@"):
|
|
118
|
+
start -= 1
|
|
119
|
+
for ln in range(start, dec.end_lineno + 1):
|
|
120
|
+
segment[ln - first] = "\n"
|
|
121
|
+
src = textwrap.dedent("".join(segment))
|
|
122
|
+
src = "\n" * (first - 1) + src
|
|
123
|
+
# Same __future__ features as the defining code (e.g. ``annotations``).
|
|
124
|
+
flags = frame.f_code.co_flags & _FUTURE_FLAGS
|
|
125
|
+
code = compile(src, filename, "exec", flags, dont_inherit=True)
|
|
126
|
+
except (_Unresolved, SyntaxError, OSError, TypeError, ValueError) as exc:
|
|
127
|
+
warnings.warn("darkpyonix.binding: returning %r unchanged (%s)" % (name or obj, exc), stacklevel=2)
|
|
128
|
+
return obj
|
|
129
|
+
finally:
|
|
130
|
+
del frame
|
|
131
|
+
|
|
132
|
+
snapshot = dict(sys._getframe(1).f_globals)
|
|
133
|
+
exec(code, snapshot)
|
|
134
|
+
return snapshot[name]
|