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 ADDED
@@ -0,0 +1,6 @@
1
+ from .shell import Shell, ShellResponse
2
+
3
+ __all__ = [
4
+ "Shell",
5
+ "ShellResponse",
6
+ ]
@@ -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