pynteract 0.1.0__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.
- pynteract/__init__.py +6 -0
- pynteract/builtin_magics.py +260 -0
- pynteract/cli.py +112 -0
- pynteract/collector.py +109 -0
- pynteract/context.py +35 -0
- pynteract/future_utils.py +60 -0
- pynteract/magics.py +229 -0
- pynteract/namespace_utils.py +142 -0
- pynteract/ptk.py +484 -0
- pynteract/shell.py +822 -0
- pynteract/streams.py +461 -0
- pynteract/traceback_utils.py +168 -0
- pynteract/user_config.py +32 -0
- pynteract/utils.py +37 -0
- pynteract-0.1.0.dist-info/METADATA +386 -0
- pynteract-0.1.0.dist-info/RECORD +20 -0
- pynteract-0.1.0.dist-info/WHEEL +5 -0
- pynteract-0.1.0.dist-info/entry_points.txt +2 -0
- pynteract-0.1.0.dist-info/licenses/LICENSE +21 -0
- pynteract-0.1.0.dist-info/top_level.txt +1 -0
pynteract/__init__.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import timeit
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from .utils import Thread
|
|
14
|
+
from . import namespace_utils
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .shell import Shell
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def register_builtin_magics(shell: "Shell") -> None:
|
|
22
|
+
"""Register a small set of commonly-used IPython-like magics on `shell`."""
|
|
23
|
+
|
|
24
|
+
@shell.register_magic(name="pwd", mode="line")
|
|
25
|
+
def _pwd(_: str) -> str:
|
|
26
|
+
return os.getcwd()
|
|
27
|
+
|
|
28
|
+
prev_cwd: dict[str, str] = {"value": os.getcwd()}
|
|
29
|
+
|
|
30
|
+
@shell.register_magic(name="cd", mode="line")
|
|
31
|
+
def _cd(arg: str) -> str:
|
|
32
|
+
target = (arg or "").strip()
|
|
33
|
+
if not target:
|
|
34
|
+
target = os.path.expanduser("~")
|
|
35
|
+
elif target == "-":
|
|
36
|
+
target = prev_cwd["value"]
|
|
37
|
+
target = os.path.expanduser(os.path.expandvars(target))
|
|
38
|
+
prev_cwd["value"] = os.getcwd()
|
|
39
|
+
os.chdir(target)
|
|
40
|
+
return os.getcwd()
|
|
41
|
+
|
|
42
|
+
@shell.register_magic(name="ls", mode="line")
|
|
43
|
+
def _ls(arg: str) -> str:
|
|
44
|
+
parts = shlex.split(arg) if arg.strip() else []
|
|
45
|
+
path = Path(parts[0]) if parts else Path(".")
|
|
46
|
+
entries = sorted(path.iterdir(), key=lambda p: p.name)
|
|
47
|
+
return "\n".join(p.name + ("/" if p.is_dir() else "") for p in entries)
|
|
48
|
+
|
|
49
|
+
@shell.register_magic(name="env", mode="line")
|
|
50
|
+
def _env(arg: str) -> Any:
|
|
51
|
+
text = (arg or "").strip()
|
|
52
|
+
if not text:
|
|
53
|
+
return dict(os.environ)
|
|
54
|
+
if text.startswith("-u "):
|
|
55
|
+
key = text[3:].strip()
|
|
56
|
+
os.environ.pop(key, None)
|
|
57
|
+
return ""
|
|
58
|
+
if "=" in text:
|
|
59
|
+
key, value = text.split("=", 1)
|
|
60
|
+
os.environ[key] = value
|
|
61
|
+
return value
|
|
62
|
+
return os.environ.get(text, "")
|
|
63
|
+
|
|
64
|
+
def _user_symbols() -> dict[str, Any]:
|
|
65
|
+
ns = shell.namespace
|
|
66
|
+
hidden_prefixes = ("__",)
|
|
67
|
+
hidden_names = {"_", "__", "___", "__builtins__", "__magics__", "__shell__", "display", "magic"}
|
|
68
|
+
return {
|
|
69
|
+
k: v
|
|
70
|
+
for k, v in ns.items()
|
|
71
|
+
if k not in hidden_names and not any(k.startswith(p) for p in hidden_prefixes)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@shell.register_magic(name="who", mode="line")
|
|
75
|
+
def _who(_: str) -> str:
|
|
76
|
+
names = sorted(_user_symbols().keys())
|
|
77
|
+
return " ".join(names)
|
|
78
|
+
|
|
79
|
+
@shell.register_magic(name="whos", mode="line")
|
|
80
|
+
def _whos(_: str) -> str:
|
|
81
|
+
rows = [("Name", "Type", "Repr")]
|
|
82
|
+
for name, value in sorted(_user_symbols().items(), key=lambda kv: kv[0]):
|
|
83
|
+
rep = repr(value)
|
|
84
|
+
if len(rep) > 60:
|
|
85
|
+
rep = rep[:57] + "..."
|
|
86
|
+
rows.append((name, type(value).__name__, rep))
|
|
87
|
+
widths = [max(len(r[i]) for r in rows) for i in range(3)]
|
|
88
|
+
return "\n".join(
|
|
89
|
+
f"{r[0]:<{widths[0]}} {r[1]:<{widths[1]}} {r[2]}" for r in rows
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _compile_for_exec(code: str, filename: str) -> tuple[object, bool]:
|
|
93
|
+
tree = ast.parse(code, filename=filename, mode="exec")
|
|
94
|
+
is_expr = len(tree.body) == 1 and isinstance(tree.body[0], ast.Expr)
|
|
95
|
+
flags = getattr(shell, "_futures").flags
|
|
96
|
+
if is_expr:
|
|
97
|
+
compiled = compile(
|
|
98
|
+
ast.Expression(tree.body[0].value),
|
|
99
|
+
filename=filename,
|
|
100
|
+
mode="eval",
|
|
101
|
+
flags=flags,
|
|
102
|
+
dont_inherit=True,
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
compiled = compile(tree, filename=filename, mode="exec", flags=flags, dont_inherit=True)
|
|
106
|
+
return compiled, is_expr
|
|
107
|
+
|
|
108
|
+
@shell.register_magic(name="time", mode="line")
|
|
109
|
+
def _time(arg: str) -> str:
|
|
110
|
+
code = (arg or "").strip()
|
|
111
|
+
if not code:
|
|
112
|
+
return "Usage: %time <code>"
|
|
113
|
+
filename = "<pynteract-%time>"
|
|
114
|
+
compiled, is_expr = _compile_for_exec(code, filename)
|
|
115
|
+
start = time.perf_counter()
|
|
116
|
+
if is_expr:
|
|
117
|
+
eval(compiled, shell.namespace, shell.namespace)
|
|
118
|
+
else:
|
|
119
|
+
exec(compiled, shell.namespace, shell.namespace)
|
|
120
|
+
elapsed = time.perf_counter() - start
|
|
121
|
+
return f"Wall time: {elapsed:.6f}s"
|
|
122
|
+
|
|
123
|
+
@shell.register_magic(name="timeit", mode="line")
|
|
124
|
+
def _timeit(arg: str) -> str:
|
|
125
|
+
parts = shlex.split(arg) if arg.strip() else []
|
|
126
|
+
n = 1000
|
|
127
|
+
r = 3
|
|
128
|
+
code_parts: list[str] = []
|
|
129
|
+
i = 0
|
|
130
|
+
while i < len(parts):
|
|
131
|
+
if parts[i] == "-n" and i + 1 < len(parts):
|
|
132
|
+
n = int(parts[i + 1])
|
|
133
|
+
i += 2
|
|
134
|
+
continue
|
|
135
|
+
if parts[i] == "-r" and i + 1 < len(parts):
|
|
136
|
+
r = int(parts[i + 1])
|
|
137
|
+
i += 2
|
|
138
|
+
continue
|
|
139
|
+
code_parts.append(parts[i])
|
|
140
|
+
i += 1
|
|
141
|
+
code = " ".join(code_parts).strip()
|
|
142
|
+
if not code:
|
|
143
|
+
return "Usage: %timeit [-n N] [-r R] <code>"
|
|
144
|
+
|
|
145
|
+
filename = "<pynteract-%timeit>"
|
|
146
|
+
compiled, is_expr = _compile_for_exec(code, filename)
|
|
147
|
+
|
|
148
|
+
def run_once() -> None:
|
|
149
|
+
if is_expr:
|
|
150
|
+
eval(compiled, shell.namespace, shell.namespace)
|
|
151
|
+
else:
|
|
152
|
+
exec(compiled, shell.namespace, shell.namespace)
|
|
153
|
+
|
|
154
|
+
timer = timeit.Timer(run_once)
|
|
155
|
+
runs = timer.repeat(repeat=r, number=n)
|
|
156
|
+
best = min(runs) / n
|
|
157
|
+
return f"Best of {r}: {best:.6g}s per loop"
|
|
158
|
+
|
|
159
|
+
@shell.register_magic(name="bash", mode="cell")
|
|
160
|
+
def _bash_cell(script: str) -> int:
|
|
161
|
+
process = subprocess.Popen(
|
|
162
|
+
["bash", "-lc", script],
|
|
163
|
+
stdout=subprocess.PIPE,
|
|
164
|
+
stderr=subprocess.PIPE,
|
|
165
|
+
text=True,
|
|
166
|
+
bufsize=1,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def _stream_output(stream, out_stream):
|
|
170
|
+
for line in iter(stream.readline, ""):
|
|
171
|
+
out_stream.write(line)
|
|
172
|
+
out_stream.flush()
|
|
173
|
+
stream.close()
|
|
174
|
+
|
|
175
|
+
t_out = Thread(target=_stream_output, args=(process.stdout, sys.stdout))
|
|
176
|
+
t_err = Thread(target=_stream_output, args=(process.stderr, sys.stderr))
|
|
177
|
+
t_out.start()
|
|
178
|
+
t_err.start()
|
|
179
|
+
t_out.join()
|
|
180
|
+
t_err.join()
|
|
181
|
+
return process.wait()
|
|
182
|
+
|
|
183
|
+
@shell.register_magic(name="run", mode="line")
|
|
184
|
+
def _run(arg: str) -> str:
|
|
185
|
+
"""Execute a Python script, IPython-style.
|
|
186
|
+
|
|
187
|
+
- `%run script.py [args...]` runs in a fresh `__main__` namespace, then merges symbols into the current shell.
|
|
188
|
+
- `%run -i script.py [args...]` runs in the current shell namespace.
|
|
189
|
+
"""
|
|
190
|
+
parts = shlex.split(arg) if arg.strip() else []
|
|
191
|
+
if not parts:
|
|
192
|
+
return "Usage: %run [-i] script.py [args...]"
|
|
193
|
+
|
|
194
|
+
interactive = False
|
|
195
|
+
if parts and parts[0] == "-i":
|
|
196
|
+
interactive = True
|
|
197
|
+
parts = parts[1:]
|
|
198
|
+
|
|
199
|
+
if not parts:
|
|
200
|
+
return "Usage: %run [-i] script.py [args...]"
|
|
201
|
+
|
|
202
|
+
script_path = Path(os.path.expandvars(os.path.expanduser(parts[0]))).resolve()
|
|
203
|
+
argv = [str(script_path), *parts[1:]]
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
source = script_path.read_text(encoding="utf-8", errors="replace")
|
|
207
|
+
except OSError as exc:
|
|
208
|
+
raise FileNotFoundError(f"Could not read script: {script_path}") from exc
|
|
209
|
+
|
|
210
|
+
saved_argv = sys.argv[:]
|
|
211
|
+
saved_main = sys.modules.get("__main__")
|
|
212
|
+
saved_file = shell.namespace.get("__file__")
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
sys.argv = argv
|
|
216
|
+
if interactive:
|
|
217
|
+
target_ns = shell.namespace
|
|
218
|
+
shell.run(source, globals=target_ns, locals=target_ns, filename=str(script_path))
|
|
219
|
+
else:
|
|
220
|
+
target_ns: dict[str, Any] = {"__name__": "__main__"}
|
|
221
|
+
for key in ("__builtins__", "display", "__magics__", "__shell__", "magic"):
|
|
222
|
+
if key in shell.namespace:
|
|
223
|
+
target_ns[key] = shell.namespace[key]
|
|
224
|
+
|
|
225
|
+
namespace_utils.NamespaceManager.prepare_namespace(
|
|
226
|
+
target_ns, filename=str(script_path), module_name="__main__"
|
|
227
|
+
)
|
|
228
|
+
shell.run(source, globals=target_ns, locals=target_ns, filename=str(script_path))
|
|
229
|
+
|
|
230
|
+
hidden = {
|
|
231
|
+
"__builtins__",
|
|
232
|
+
"__doc__",
|
|
233
|
+
"__file__",
|
|
234
|
+
"__name__",
|
|
235
|
+
"__package__",
|
|
236
|
+
"__spec__",
|
|
237
|
+
"__cached__",
|
|
238
|
+
"__loader__",
|
|
239
|
+
"__magics__",
|
|
240
|
+
"__shell__",
|
|
241
|
+
"display",
|
|
242
|
+
"magic",
|
|
243
|
+
"_",
|
|
244
|
+
"__",
|
|
245
|
+
"___",
|
|
246
|
+
}
|
|
247
|
+
for name, value in target_ns.items():
|
|
248
|
+
if name in hidden or name.startswith("__"):
|
|
249
|
+
continue
|
|
250
|
+
shell.namespace[name] = value
|
|
251
|
+
finally:
|
|
252
|
+
sys.argv = saved_argv
|
|
253
|
+
if saved_file is not None:
|
|
254
|
+
shell.namespace["__file__"] = saved_file
|
|
255
|
+
if saved_main is None:
|
|
256
|
+
sys.modules.pop("__main__", None)
|
|
257
|
+
else:
|
|
258
|
+
sys.modules["__main__"] = saved_main
|
|
259
|
+
|
|
260
|
+
return ""
|
pynteract/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
import __main__
|
|
6
|
+
from importlib import metadata
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .shell import Shell
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_shell(*, namespace: dict | None = None) -> Shell:
|
|
13
|
+
"""Return a Shell bound to a `__main__` module namespace."""
|
|
14
|
+
if namespace is None:
|
|
15
|
+
namespace = __main__.__dict__
|
|
16
|
+
return Shell(module_name="__main__", namespace=namespace)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run_script(shell: Shell, script: Path, argv: list[str]) -> int:
|
|
20
|
+
code = script.read_text(encoding="utf-8", errors="replace")
|
|
21
|
+
try:
|
|
22
|
+
sys.argv = argv
|
|
23
|
+
resp = shell.run(code, filename=str(script))
|
|
24
|
+
return 0 if resp.exception is None else 1
|
|
25
|
+
finally:
|
|
26
|
+
# Caller decides whether to restore sys.argv / sys.modules['__main__'].
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
31
|
+
parser = argparse.ArgumentParser(prog="pynteract", description="Interactive pynteract shell")
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--version",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Print version and exit.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-i",
|
|
39
|
+
"--interact",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help="Enter interactive mode after running a script.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"script",
|
|
45
|
+
nargs="?",
|
|
46
|
+
help="Python script to run.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"script_args",
|
|
50
|
+
nargs=argparse.REMAINDER,
|
|
51
|
+
help=argparse.SUPPRESS,
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--no-interact",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help=argparse.SUPPRESS,
|
|
57
|
+
)
|
|
58
|
+
return parser
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(argv: list[str] | None = None) -> int:
|
|
62
|
+
parser = _build_parser()
|
|
63
|
+
args = parser.parse_args(argv)
|
|
64
|
+
|
|
65
|
+
if args.version:
|
|
66
|
+
try:
|
|
67
|
+
version = metadata.version("pynteract")
|
|
68
|
+
except Exception:
|
|
69
|
+
version = "unknown"
|
|
70
|
+
sys.stdout.write(f"{version}\n")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
if args.script:
|
|
74
|
+
script_path = Path(args.script)
|
|
75
|
+
if not script_path.exists():
|
|
76
|
+
sys.stderr.write(f"pynteract: no such file: {script_path}\n")
|
|
77
|
+
return 2
|
|
78
|
+
script_path = script_path.resolve()
|
|
79
|
+
script_argv = [str(script_path), *list(args.script_args or [])]
|
|
80
|
+
|
|
81
|
+
# Run scripts in a fresh __main__ namespace (like `python script.py` / IPython %run).
|
|
82
|
+
saved_argv = sys.argv[:]
|
|
83
|
+
saved_main = sys.modules.get("__main__")
|
|
84
|
+
shell = build_shell(namespace={})
|
|
85
|
+
sys.argv = script_argv
|
|
86
|
+
|
|
87
|
+
if args.interact:
|
|
88
|
+
startup_exit = shell._run_startup(announce=False)
|
|
89
|
+
if startup_exit != 0:
|
|
90
|
+
sys.argv = saved_argv
|
|
91
|
+
if saved_main is None:
|
|
92
|
+
sys.modules.pop("__main__", None)
|
|
93
|
+
else:
|
|
94
|
+
sys.modules["__main__"] = saved_main
|
|
95
|
+
return startup_exit
|
|
96
|
+
|
|
97
|
+
exit_code = _run_script(shell, script_path, script_argv)
|
|
98
|
+
|
|
99
|
+
if args.no_interact or not args.interact:
|
|
100
|
+
sys.argv = saved_argv
|
|
101
|
+
if saved_main is None:
|
|
102
|
+
sys.modules.pop("__main__", None)
|
|
103
|
+
else:
|
|
104
|
+
sys.modules["__main__"] = saved_main
|
|
105
|
+
return exit_code
|
|
106
|
+
# Else: keep script argv and __main__ module and fall through into interactive.
|
|
107
|
+
else:
|
|
108
|
+
shell = build_shell()
|
|
109
|
+
if args.no_interact:
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
return int(shell.interact())
|
pynteract/collector.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from .streams import Stream, StdinProxy, stdout_write, stderr_write, stdin_readline
|
|
3
|
+
from .context import RUN_CONTEXT
|
|
4
|
+
|
|
5
|
+
class Collector:
|
|
6
|
+
"""
|
|
7
|
+
Manages stdout and stderr redirection within a context manager.
|
|
8
|
+
|
|
9
|
+
This class captures all output and exceptions, allowing fine control over their handling and display.
|
|
10
|
+
|
|
11
|
+
Attributes:
|
|
12
|
+
shell: Shell instance whose hooks are consulted dynamically.
|
|
13
|
+
stdout_stream (Stream): Custom stream for capturing stdout.
|
|
14
|
+
stderr_stream (Stream): Custom stream for capturing stderr.
|
|
15
|
+
exception (Exception): Stores any exception raised during execution.
|
|
16
|
+
|
|
17
|
+
Methods:
|
|
18
|
+
get_stdout(): Returns all that was written to the stdout stream.
|
|
19
|
+
get_stderr(): Returns all that was written to the stderr stream.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, shell):
|
|
22
|
+
if shell is None:
|
|
23
|
+
raise TypeError("Collector requires a shell instance")
|
|
24
|
+
self.shell = shell
|
|
25
|
+
delegate_stdout = sys.stdout
|
|
26
|
+
delegate_stderr = sys.stderr
|
|
27
|
+
self.stdin_proxy = StdinProxy(shell, default_hook=stdin_readline)
|
|
28
|
+
self.stdout_stream = Stream(shell, hook_key="stdout_hook", default_hook=stdout_write, delegate=delegate_stdout)
|
|
29
|
+
self.stderr_stream = Stream(shell, hook_key="stderr_hook", default_hook=stderr_write, delegate=delegate_stderr)
|
|
30
|
+
self.exception = None
|
|
31
|
+
|
|
32
|
+
def get_stdout(self):
|
|
33
|
+
"""
|
|
34
|
+
Returns all that was written to the stdout stream.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: The entire content written to stdout.
|
|
38
|
+
"""
|
|
39
|
+
return self.stdout_stream.get_value()
|
|
40
|
+
|
|
41
|
+
def get_stderr(self):
|
|
42
|
+
"""
|
|
43
|
+
Returns all that was written to the stderr stream.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
str: The entire content written to stderr.
|
|
47
|
+
"""
|
|
48
|
+
return self.stderr_stream.get_value()
|
|
49
|
+
|
|
50
|
+
def __enter__(self):
|
|
51
|
+
"""
|
|
52
|
+
Implements using the collector as a context manager.
|
|
53
|
+
|
|
54
|
+
This method redirects sys.stdout and sys.stderr to stdout and stderr Streams
|
|
55
|
+
and, when ``stdin_hook`` is provided, installs a ``StdinProxy`` as sys.stdin.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Collector: The Collector instance.
|
|
59
|
+
"""
|
|
60
|
+
self.saved_stdout = sys.stdout
|
|
61
|
+
self.saved_stderr = sys.stderr
|
|
62
|
+
self.saved_stdin = sys.stdin
|
|
63
|
+
sys.stdout = self.stdout_stream
|
|
64
|
+
sys.stderr = self.stderr_stream
|
|
65
|
+
sys.stdin = self.stdin_proxy
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
69
|
+
"""
|
|
70
|
+
Flushes the streams, restores the standard streams and gracefully processes any pending exception.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
exc_type: The type of the exception.
|
|
74
|
+
exc_value: The exception instance.
|
|
75
|
+
exc_traceback: The traceback object.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
bool: True if the exception was handled, False otherwise.
|
|
79
|
+
"""
|
|
80
|
+
# Flush streams
|
|
81
|
+
self.stdout_stream.flush()
|
|
82
|
+
self.stderr_stream.flush()
|
|
83
|
+
# Restore standard streams
|
|
84
|
+
sys.stdout = self.saved_stdout
|
|
85
|
+
sys.stderr = self.saved_stderr
|
|
86
|
+
sys.stdin = self.saved_stdin
|
|
87
|
+
# Deal with any unhandled exception
|
|
88
|
+
if exc_type is not None:
|
|
89
|
+
# Save the exception
|
|
90
|
+
self.exception = exc_value
|
|
91
|
+
message, enriched = self.shell._build_enriched_traceback(exc_type, exc_value, exc_traceback)
|
|
92
|
+
exc_value.enriched_traceback = enriched
|
|
93
|
+
exc_value.enriched_traceback_string = message
|
|
94
|
+
|
|
95
|
+
# Send the traceback to stderr_stream and flush
|
|
96
|
+
self.stderr_stream.write(message)
|
|
97
|
+
self.stderr_stream.flush()
|
|
98
|
+
# Send the exception to the exception hook
|
|
99
|
+
hooks = getattr(self.shell, "hooks", None)
|
|
100
|
+
if not isinstance(hooks, dict):
|
|
101
|
+
raise TypeError("shell.hooks must be a dict")
|
|
102
|
+
exception_hook = hooks.get("exception_hook")
|
|
103
|
+
if exception_hook:
|
|
104
|
+
invoke = getattr(self.shell, "_invoke_hook", None)
|
|
105
|
+
if callable(invoke):
|
|
106
|
+
invoke(exception_hook, exc_value, ctx=RUN_CONTEXT.get())
|
|
107
|
+
else:
|
|
108
|
+
exception_hook(exc_value)
|
|
109
|
+
return True # Suppress exception propagation
|
pynteract/context.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextvars
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
# Active capture streams for the current context (stdout, stderr).
|
|
8
|
+
CAPTURE_STREAMS: contextvars.ContextVar[tuple[Any, Any] | None] = contextvars.ContextVar(
|
|
9
|
+
"pynteract_capture_streams",
|
|
10
|
+
default=None,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, slots=True)
|
|
14
|
+
class RunContext:
|
|
15
|
+
name: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Current run context (routing name for hooks).
|
|
19
|
+
RUN_CONTEXT: contextvars.ContextVar[RunContext | None] = contextvars.ContextVar(
|
|
20
|
+
"pynteract_run_context",
|
|
21
|
+
default=None,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# If True, suppress stdout/stderr hook calls for this run (still captures output).
|
|
25
|
+
SILENT_STDIO: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
26
|
+
"pynteract_silent_stdio",
|
|
27
|
+
default=False,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Whether code printed a prompt without a trailing newline (common with builtin
|
|
31
|
+
# input(prompt)), so stdin reads can start on a fresh line.
|
|
32
|
+
PENDING_STDIN_PROMPT: contextvars.ContextVar[bool] = contextvars.ContextVar(
|
|
33
|
+
"pynteract_pending_stdin_prompt",
|
|
34
|
+
default=False,
|
|
35
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import __future__
|
|
4
|
+
import ast
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class FutureManager:
|
|
10
|
+
"""Track session-wide __future__ compiler flags for an interactive shell."""
|
|
11
|
+
|
|
12
|
+
flags: int = 0
|
|
13
|
+
allow_leading_docstring: bool = True
|
|
14
|
+
_seen_non_future_stmt: bool = False
|
|
15
|
+
_seen_docstring: bool = False
|
|
16
|
+
|
|
17
|
+
def reset(self) -> None:
|
|
18
|
+
self.flags = 0
|
|
19
|
+
self._seen_non_future_stmt = False
|
|
20
|
+
self._seen_docstring = False
|
|
21
|
+
|
|
22
|
+
def begin_block(self) -> None:
|
|
23
|
+
"""Reset per-block state (flags are session-wide and persist)."""
|
|
24
|
+
self._seen_non_future_stmt = False
|
|
25
|
+
self._seen_docstring = False
|
|
26
|
+
|
|
27
|
+
def process_node(self, node: ast.AST) -> None:
|
|
28
|
+
"""Validate/process a single node for __future__ semantics.
|
|
29
|
+
|
|
30
|
+
Notebook/REPL rule: `from __future__ import ...` must be the first node of the input.
|
|
31
|
+
By default, we emulate module semantics and permit exactly one leading string literal
|
|
32
|
+
("docstring position") before any future imports.
|
|
33
|
+
|
|
34
|
+
If ``allow_leading_docstring`` is False, a leading string literal counts as a statement
|
|
35
|
+
(it can be displayed as output) and therefore blocks future imports that follow.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if (
|
|
39
|
+
self.allow_leading_docstring
|
|
40
|
+
and not self._seen_non_future_stmt
|
|
41
|
+
and not self._seen_docstring
|
|
42
|
+
and isinstance(node, ast.Expr)
|
|
43
|
+
and isinstance(node.value, ast.Constant)
|
|
44
|
+
and isinstance(node.value.value, str)
|
|
45
|
+
):
|
|
46
|
+
self._seen_docstring = True
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
if isinstance(node, ast.ImportFrom) and node.module == "__future__":
|
|
50
|
+
if self._seen_non_future_stmt:
|
|
51
|
+
raise SyntaxError("from __future__ imports must occur at the beginning of the input")
|
|
52
|
+
for alias in node.names:
|
|
53
|
+
feature = getattr(__future__, alias.name, None)
|
|
54
|
+
flag = getattr(feature, "compiler_flag", 0) if feature is not None else 0
|
|
55
|
+
if not flag:
|
|
56
|
+
raise SyntaxError(f"future feature {alias.name!r} is not defined")
|
|
57
|
+
self.flags |= int(flag)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self._seen_non_future_stmt = True
|