ipythonng 0.0.1__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.
ipythonng/__init__.py
ADDED
ipythonng/cli.py
ADDED
ipythonng/extension.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import io
|
|
3
|
+
import sys
|
|
4
|
+
from contextlib import redirect_stdout
|
|
5
|
+
from types import MethodType
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from kittytgp import build_render_bytes
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.markdown import Markdown as RichMarkdown
|
|
11
|
+
|
|
12
|
+
_DEFAULT_CELL_SIZE = (8, 16)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _register_mime_renderer(shell, mime, handler):
|
|
16
|
+
active_types = shell.display_formatter.active_types
|
|
17
|
+
if mime not in active_types: active_types.append(mime)
|
|
18
|
+
formatter = shell.display_formatter.formatters.get(mime)
|
|
19
|
+
if formatter is not None: formatter.enabled = True
|
|
20
|
+
shell.mime_renderers[mime] = handler
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _is_tty(stream) -> bool:
|
|
24
|
+
isatty = getattr(stream, "isatty", None)
|
|
25
|
+
return bool(isatty and isatty())
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_inline_backend(backend: str | None) -> bool:
|
|
29
|
+
if not backend: return False
|
|
30
|
+
return backend.lower() in {"inline", "module://matplotlib_inline.backend_inline"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _RenderTarget:
|
|
34
|
+
def __init__(self, stream): self.stream = stream
|
|
35
|
+
|
|
36
|
+
def fileno(self) -> int:
|
|
37
|
+
for candidate in (getattr(self.stream, "buffer", None), self.stream, getattr(sys.__stdout__, "buffer", None), sys.__stdout__):
|
|
38
|
+
fileno = getattr(candidate, "fileno", None)
|
|
39
|
+
if fileno is None: continue
|
|
40
|
+
try: return fileno()
|
|
41
|
+
except Exception: continue
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IPythonNGExtension:
|
|
46
|
+
def __init__(self, shell):
|
|
47
|
+
self.shell = shell
|
|
48
|
+
self.history_manager = shell.history_manager
|
|
49
|
+
self._original_store_output = self.history_manager.store_output
|
|
50
|
+
self._pending_store_output = set()
|
|
51
|
+
self._registered_events = []
|
|
52
|
+
self._rendered_mimes = {}
|
|
53
|
+
self._handler_by_mime = {}
|
|
54
|
+
self._added_active_types = set()
|
|
55
|
+
self._original_enable_matplotlib = None
|
|
56
|
+
|
|
57
|
+
def load(self):
|
|
58
|
+
self._install_renderers()
|
|
59
|
+
self._install_history_patch()
|
|
60
|
+
self._install_matplotlib_patch()
|
|
61
|
+
|
|
62
|
+
def unload(self):
|
|
63
|
+
for event, callback in self._registered_events: self.shell.events.unregister(event, callback)
|
|
64
|
+
self._registered_events.clear()
|
|
65
|
+
|
|
66
|
+
self.history_manager.store_output = self._original_store_output
|
|
67
|
+
self._pending_store_output.clear()
|
|
68
|
+
if self._original_enable_matplotlib is not None: self.shell.enable_matplotlib = self._original_enable_matplotlib
|
|
69
|
+
|
|
70
|
+
for mime, original in self._rendered_mimes.items():
|
|
71
|
+
current = self.shell.mime_renderers.get(mime)
|
|
72
|
+
if current is self._handler_by_mime.get(mime):
|
|
73
|
+
if original is None: self.shell.mime_renderers.pop(mime, None)
|
|
74
|
+
else: self.shell.mime_renderers[mime] = original
|
|
75
|
+
|
|
76
|
+
for mime in self._added_active_types:
|
|
77
|
+
if mime in self.shell.display_formatter.active_types: self.shell.display_formatter.active_types.remove(mime)
|
|
78
|
+
|
|
79
|
+
def _install_renderers(self):
|
|
80
|
+
self._add_renderer("text/markdown", self._handle_text_markdown)
|
|
81
|
+
self._add_renderer("image/png", self._handle_image_png)
|
|
82
|
+
|
|
83
|
+
def _add_renderer(self, mime: str, handler):
|
|
84
|
+
self._rendered_mimes[mime] = self.shell.mime_renderers.get(mime)
|
|
85
|
+
self._handler_by_mime[mime] = handler
|
|
86
|
+
if mime not in self.shell.display_formatter.active_types: self._added_active_types.add(mime)
|
|
87
|
+
_register_mime_renderer(self.shell, mime, handler)
|
|
88
|
+
|
|
89
|
+
def _install_history_patch(self):
|
|
90
|
+
self.history_manager.store_output = MethodType(self._deferred_store_output, self.history_manager)
|
|
91
|
+
self.shell.events.register("post_run_cell", self._finalize_history)
|
|
92
|
+
self._registered_events.append(("post_run_cell", self._finalize_history))
|
|
93
|
+
|
|
94
|
+
def _install_matplotlib_patch(self):
|
|
95
|
+
self._original_enable_matplotlib = self.shell.enable_matplotlib
|
|
96
|
+
self.shell.enable_matplotlib = MethodType(lambda shell, gui=None: self._enable_matplotlib(shell, gui), self.shell)
|
|
97
|
+
|
|
98
|
+
def _deferred_store_output(self, history_manager, line_num: int) -> None:
|
|
99
|
+
if history_manager.db_log_output: self._pending_store_output.add(line_num)
|
|
100
|
+
|
|
101
|
+
def _output_stream(self): return getattr(self.shell, "_ipythonng_stream", sys.__stdout__)
|
|
102
|
+
|
|
103
|
+
def _write(self, text: str):
|
|
104
|
+
stream = self._output_stream()
|
|
105
|
+
stream.write(text)
|
|
106
|
+
flush = getattr(stream, "flush", None)
|
|
107
|
+
if flush is not None: flush()
|
|
108
|
+
|
|
109
|
+
def _render_markdown(self, markdown_text: str):
|
|
110
|
+
stream = self._output_stream()
|
|
111
|
+
console = Console(file=stream, force_terminal=_is_tty(stream), highlight=False, soft_wrap=True)
|
|
112
|
+
console.print(RichMarkdown(markdown_text))
|
|
113
|
+
|
|
114
|
+
def _current_matplotlib_backend(self) -> str | None:
|
|
115
|
+
try: import matplotlib
|
|
116
|
+
except Exception: return None
|
|
117
|
+
try: return matplotlib.get_backend()
|
|
118
|
+
except Exception: return None
|
|
119
|
+
|
|
120
|
+
def _close_matplotlib_figures(self):
|
|
121
|
+
try: from matplotlib import pyplot as plt
|
|
122
|
+
except Exception: return
|
|
123
|
+
try: plt.close("all")
|
|
124
|
+
except Exception: return
|
|
125
|
+
|
|
126
|
+
def _ensure_inline_figure_formats(self, shell):
|
|
127
|
+
try:
|
|
128
|
+
from IPython.core.pylabtools import select_figure_formats
|
|
129
|
+
from matplotlib.figure import Figure
|
|
130
|
+
from matplotlib_inline.config import InlineBackend
|
|
131
|
+
except Exception: return
|
|
132
|
+
png_formatter = shell.display_formatter.formatters["image/png"]
|
|
133
|
+
if Figure in png_formatter.type_printers: return
|
|
134
|
+
cfg = InlineBackend.instance(parent=shell)
|
|
135
|
+
select_figure_formats(shell, cfg.figure_formats, **cfg.print_figure_kwargs)
|
|
136
|
+
|
|
137
|
+
def _enable_matplotlib(self, shell, gui=None):
|
|
138
|
+
previous_backend = self._current_matplotlib_backend()
|
|
139
|
+
stdout = io.StringIO()
|
|
140
|
+
with redirect_stdout(stdout): result = self._original_enable_matplotlib(gui)
|
|
141
|
+
gui_name, backend = result
|
|
142
|
+
output = stdout.getvalue()
|
|
143
|
+
if _is_inline_backend(backend):
|
|
144
|
+
self._ensure_inline_figure_formats(shell)
|
|
145
|
+
output = "".join(line for line in output.splitlines(keepends=True) if line.strip() != "No event loop hook running.")
|
|
146
|
+
if not _is_inline_backend(previous_backend): self._close_matplotlib_figures()
|
|
147
|
+
if output: sys.stdout.write(output)
|
|
148
|
+
return gui_name, backend
|
|
149
|
+
|
|
150
|
+
def _needs_execute_result_newline(self) -> bool:
|
|
151
|
+
displayhook = getattr(self.shell, "displayhook", None)
|
|
152
|
+
return bool(displayhook and displayhook.is_active and not displayhook.prompt_end_newline)
|
|
153
|
+
|
|
154
|
+
def _render_png(self, png_b64: str, metadata: dict[str, Any] | None):
|
|
155
|
+
stream = self._output_stream()
|
|
156
|
+
if not _is_tty(stream):
|
|
157
|
+
self._write("[image/png]\n")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
target = _RenderTarget(stream)
|
|
161
|
+
try:
|
|
162
|
+
png_bytes = base64.b64decode(png_b64)
|
|
163
|
+
payload = build_render_bytes(png_bytes, out=target)
|
|
164
|
+
except RuntimeError:
|
|
165
|
+
try:
|
|
166
|
+
payload = build_render_bytes(png_bytes, out=target, cell_width_px=_DEFAULT_CELL_SIZE[0], cell_height_px=_DEFAULT_CELL_SIZE[1])
|
|
167
|
+
except Exception:
|
|
168
|
+
self._write("[image/png]\n")
|
|
169
|
+
return
|
|
170
|
+
except Exception:
|
|
171
|
+
self._write("[image/png]\n")
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
if self._needs_execute_result_newline(): self._write("\n")
|
|
175
|
+
self._write(payload.decode("utf-8"))
|
|
176
|
+
|
|
177
|
+
def _handle_text_markdown(self, markdown_text: str, metadata=None): self._render_markdown(markdown_text)
|
|
178
|
+
|
|
179
|
+
def _handle_image_png(self, png_b64: str, metadata=None): self._render_png(png_b64, metadata)
|
|
180
|
+
|
|
181
|
+
def _render_history_output(self, output) -> str:
|
|
182
|
+
if output.output_type in {"out_stream", "err_stream"}: return "".join(output.bundle.get("stream", []))
|
|
183
|
+
|
|
184
|
+
bundle = output.bundle
|
|
185
|
+
if "text/markdown" in bundle: return bundle["text/markdown"]
|
|
186
|
+
if "image/png" in bundle: return "[image/png]"
|
|
187
|
+
if "text/plain" in bundle: return bundle["text/plain"]
|
|
188
|
+
return ""
|
|
189
|
+
|
|
190
|
+
def _flatten_output(self, execution_count: int) -> str | None:
|
|
191
|
+
pieces = []
|
|
192
|
+
for output in self.history_manager.outputs.get(execution_count, []):
|
|
193
|
+
text = self._render_history_output(output)
|
|
194
|
+
if not text: continue
|
|
195
|
+
if pieces and not pieces[-1].endswith("\n") and not text.startswith("\n"): pieces.append("\n")
|
|
196
|
+
pieces.append(text)
|
|
197
|
+
|
|
198
|
+
exception = self.history_manager.exceptions.get(execution_count)
|
|
199
|
+
if exception:
|
|
200
|
+
traceback_text = "".join(exception.get("traceback", []))
|
|
201
|
+
if traceback_text:
|
|
202
|
+
if pieces and not pieces[-1].endswith("\n") and not traceback_text.startswith("\n"): pieces.append("\n")
|
|
203
|
+
pieces.append(traceback_text)
|
|
204
|
+
|
|
205
|
+
if not pieces: return None
|
|
206
|
+
return "".join(pieces)
|
|
207
|
+
|
|
208
|
+
def _finalize_history(self, result):
|
|
209
|
+
execution_count = getattr(result, "execution_count", None)
|
|
210
|
+
if execution_count is None: return
|
|
211
|
+
|
|
212
|
+
flat_output = self._flatten_output(execution_count)
|
|
213
|
+
if flat_output is None: self.history_manager.output_hist_reprs.pop(execution_count, None)
|
|
214
|
+
else: self.history_manager.output_hist_reprs[execution_count] = flat_output
|
|
215
|
+
|
|
216
|
+
should_store = self.history_manager.db_log_output and flat_output is not None and (
|
|
217
|
+
execution_count in self._pending_store_output or execution_count in self.history_manager.exceptions)
|
|
218
|
+
self._pending_store_output.discard(execution_count)
|
|
219
|
+
if should_store: self._original_store_output(execution_count)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def load_ipython_extension(shell):
|
|
223
|
+
if getattr(shell, "_ipythonng_extension", None) is not None: return
|
|
224
|
+
extension = IPythonNGExtension(shell)
|
|
225
|
+
extension.load()
|
|
226
|
+
shell._ipythonng_extension = extension
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def unload_ipython_extension(shell):
|
|
230
|
+
extension = getattr(shell, "_ipythonng_extension", None)
|
|
231
|
+
if extension is None: return
|
|
232
|
+
extension.unload()
|
|
233
|
+
del shell._ipythonng_extension
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ipythonng
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: IPython extension for terminal markdown, kitty images, and richer output history
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ipython>=9.0
|
|
8
|
+
Requires-Dist: kittytgp>=0.0.2
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
|
|
11
|
+
# ipythonng
|
|
12
|
+
|
|
13
|
+
`ipythonng` is a small IPython extension for terminal sessions that adds:
|
|
14
|
+
|
|
15
|
+
- `text/markdown` rendering with Rich
|
|
16
|
+
- `image/png` rendering via `kittytgp`
|
|
17
|
+
- matplotlib inline support
|
|
18
|
+
- Includes display objects, streams, and rich results in stored history
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install ipythonng
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Use as an extension
|
|
27
|
+
|
|
28
|
+
Add the extension and enable output logging in your IPython config:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
c.InteractiveShellApp.extensions = ["ipythonng"]
|
|
32
|
+
c.HistoryManager.db_log_output = True
|
|
33
|
+
c.InteractiveShellApp.exec_lines = ["%matplotlib inline"] # if you like
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or launch it ad hoc:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
ipython --ext ipythonng
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For matplotlib, `%matplotlib inline` works with the existing `image/png` renderer. No custom matplotlib backend is needed. Using `exec_lines` runs the magic after extensions load.
|
|
43
|
+
|
|
44
|
+
## Convenience launcher
|
|
45
|
+
|
|
46
|
+
The package also installs an `ipythonng` command that simply starts IPython with
|
|
47
|
+
`--ext ipythonng`.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
ipythonng/__init__.py,sha256=NuhM8Y9hkZ9mb13P9mGVqEhOGprJAR01TQKWHsxrfj0,176
|
|
2
|
+
ipythonng/cli.py,sha256=TSqNX08AT-c5JZm5pXKmvaMD1OXM77EKt2CULaf5jDs,118
|
|
3
|
+
ipythonng/extension.py,sha256=h-x1VyczG32HDGuBvECOJagSYqSj-GYjHz9Qqey1ezw,9801
|
|
4
|
+
ipythonng-0.0.1.dist-info/METADATA,sha256=8VGaURG_ofWew3fCXb1tdxpV12l-xTzbp7Pph3r1oqU,1239
|
|
5
|
+
ipythonng-0.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
ipythonng-0.0.1.dist-info/entry_points.txt,sha256=4ybUqRHBeUYcaE_VLHylfpBEv3olP_Gb8EVpzPSMoew,49
|
|
7
|
+
ipythonng-0.0.1.dist-info/top_level.txt,sha256=t6-Qvxay_9QGK_SPUTLGMZyycRcc6-Rzi_eV6dKF0Rk,10
|
|
8
|
+
ipythonng-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ipythonng
|