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
@@ -0,0 +1,5 @@
1
+ __version__ = "0.0.1"
2
+
3
+ from .extension import load_ipython_extension, unload_ipython_extension
4
+
5
+ __all__ = ["__version__", "load_ipython_extension", "unload_ipython_extension"]
ipythonng/cli.py ADDED
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from IPython import start_ipython
4
+
5
+
6
+ def main(): start_ipython(argv=["--ext", "ipythonng", *sys.argv[1:]])
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ipythonng = ipythonng.cli:main
@@ -0,0 +1 @@
1
+ ipythonng