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 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]