ipythonng 0.0.1__tar.gz

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.
@@ -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,37 @@
1
+ # ipythonng
2
+
3
+ `ipythonng` is a small IPython extension for terminal sessions that adds:
4
+
5
+ - `text/markdown` rendering with Rich
6
+ - `image/png` rendering via `kittytgp`
7
+ - matplotlib inline support
8
+ - Includes display objects, streams, and rich results in stored history
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install ipythonng
14
+ ```
15
+
16
+ ## Use as an extension
17
+
18
+ Add the extension and enable output logging in your IPython config:
19
+
20
+ ```python
21
+ c.InteractiveShellApp.extensions = ["ipythonng"]
22
+ c.HistoryManager.db_log_output = True
23
+ c.InteractiveShellApp.exec_lines = ["%matplotlib inline"] # if you like
24
+ ```
25
+
26
+ Or launch it ad hoc:
27
+
28
+ ```bash
29
+ ipython --ext ipythonng
30
+ ```
31
+
32
+ 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.
33
+
34
+ ## Convenience launcher
35
+
36
+ The package also installs an `ipythonng` command that simply starts IPython with
37
+ `--ext ipythonng`.
@@ -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"]
@@ -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:]])
@@ -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,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ ./ipythonng/__init__.py
4
+ ./ipythonng/cli.py
5
+ ./ipythonng/extension.py
6
+ ipythonng/__init__.py
7
+ ipythonng/cli.py
8
+ ipythonng/extension.py
9
+ ipythonng.egg-info/PKG-INFO
10
+ ipythonng.egg-info/SOURCES.txt
11
+ ipythonng.egg-info/dependency_links.txt
12
+ ipythonng.egg-info/entry_points.txt
13
+ ipythonng.egg-info/requires.txt
14
+ ipythonng.egg-info/top_level.txt
15
+ tests/test_ipythonng.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ ipythonng = ipythonng.cli:main
@@ -0,0 +1,3 @@
1
+ ipython>=9.0
2
+ kittytgp>=0.0.2
3
+ rich>=13.0
@@ -0,0 +1 @@
1
+ ipythonng
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ipythonng"
7
+ dynamic = ["version"]
8
+ description = "IPython extension for terminal markdown, kitty images, and richer output history"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "ipython>=9.0",
13
+ "kittytgp>=0.0.2",
14
+ "rich>=13.0",
15
+ ]
16
+
17
+ [project.scripts]
18
+ ipythonng = "ipythonng.cli:main"
19
+
20
+ [tool.setuptools]
21
+ package-dir = {"" = "."}
22
+ include-package-data = true
23
+
24
+ [tool.setuptools.dynamic]
25
+ version = { attr = "ipythonng.__version__" }
26
+
27
+ [tool.setuptools.packages.find]
28
+ include = ["ipythonng*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,210 @@
1
+ import base64
2
+ import io
3
+
4
+ import kittytgp.core as kittycore
5
+ import pytest
6
+ from IPython.terminal.interactiveshell import TerminalInteractiveShell
7
+ from kittytgp import build_render_bytes
8
+ from kittytgp.core import PLACEHOLDER
9
+ from traitlets.config import Config
10
+
11
+ from ipythonng import load_ipython_extension
12
+
13
+ PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAADCAQAAABi6S9dAAAADElEQVR42mNkYPhfDwADhgGAff/3fwAAAABJRU5ErkJggg=="
14
+ PNG_BYTES = base64.b64decode(PNG_B64)
15
+
16
+
17
+ class FakeTerminal(io.StringIO):
18
+ def isatty(self): return True
19
+
20
+
21
+ class GeometryProbe:
22
+ def fileno(self): return 1
23
+
24
+
25
+ @pytest.fixture
26
+ def shell(tmp_path):
27
+ TerminalInteractiveShell.clear_instance()
28
+ config = Config()
29
+ config.TerminalInteractiveShell.simple_prompt = True
30
+ config.HistoryManager.hist_file = str(tmp_path / "history.sqlite")
31
+ shell = TerminalInteractiveShell.instance(config=config)
32
+ shell.execution_count = 1
33
+ shell.history_manager.outputs.clear()
34
+ shell.history_manager.output_hist_reprs.clear()
35
+ shell.history_manager.exceptions.clear()
36
+ shell._ipythonng_stream = FakeTerminal()
37
+ load_ipython_extension(shell)
38
+ try: yield shell
39
+ finally:
40
+ shell.history_manager.writeout_cache()
41
+ shell.history_manager.end_session()
42
+ shell._atexit_once = lambda: None
43
+ TerminalInteractiveShell.clear_instance()
44
+
45
+
46
+ def test_history_flattens_streams_markdown_images_and_results(shell):
47
+ shell.run_cell(
48
+ """
49
+ from IPython.display import Markdown, Image, display
50
+ import base64
51
+ print("alpha")
52
+ display(Markdown("# Heading"))
53
+ display(Image(data=base64.b64decode(%r), format="png"))
54
+ print("omega")
55
+ 42
56
+ """
57
+ % PNG_B64,
58
+ store_history=True,
59
+ )
60
+
61
+ (_, _, (_, output)) = list(shell.history_manager.get_range(output=True))[-1]
62
+ assert output == "alpha\n# Heading\n[image/png]\nomega\n42"
63
+
64
+
65
+ def test_output_history_persists_flattened_output_across_sessions(shell):
66
+ shell.history_manager.db_log_output = True
67
+ result = shell.run_cell(
68
+ """
69
+ from IPython.display import Markdown, display
70
+ print("alpha")
71
+ display(Markdown("## Saved"))
72
+ """,
73
+ store_history=True,
74
+ )
75
+
76
+ shell.history_manager.writeout_cache()
77
+ shell.history_manager.reset()
78
+
79
+ execution_count = result.execution_count
80
+ entries = list(shell.history_manager.get_range(-1, execution_count, execution_count + 1, output=True))
81
+ assert entries[0][2][1] == "alpha\n## Saved"
82
+
83
+
84
+ def test_tracebacks_are_included_in_flattened_output(shell):
85
+ shell.history_manager.db_log_output = True
86
+ shell.run_cell("1/0", store_history=True)
87
+
88
+ (_, _, (_, output)) = list(shell.history_manager.get_range(output=True))[-1]
89
+ assert "ZeroDivisionError" in output
90
+ assert "division by zero" in output
91
+
92
+
93
+ def test_kitty_rendering_matches_kittytgp_outside_tmux(shell, monkeypatch):
94
+ monkeypatch.delenv("TMUX", raising=False)
95
+ monkeypatch.setattr(kittycore.secrets, "randbelow", lambda _: 0x123456 - 1)
96
+
97
+ shell.run_cell(
98
+ """
99
+ from IPython.display import Image, display
100
+ import base64
101
+ display(Image(data=base64.b64decode(%r), format="png"))
102
+ """
103
+ % PNG_B64,
104
+ store_history=True,
105
+ )
106
+
107
+ rendered = shell._ipythonng_stream.getvalue()
108
+ expected = build_render_bytes(PNG_BYTES, out=GeometryProbe(), cell_width_px=8, cell_height_px=16).decode("utf-8")
109
+ assert rendered == expected
110
+ assert PLACEHOLDER in rendered
111
+ assert "\x1bPtmux;" not in rendered
112
+ assert "[image/png]" not in rendered
113
+
114
+
115
+ def test_execute_result_images_start_on_new_line(shell, monkeypatch):
116
+ monkeypatch.delenv("TMUX", raising=False)
117
+ monkeypatch.setattr(kittycore.secrets, "randbelow", lambda _: 0x222222 - 1)
118
+
119
+ shell.run_cell(
120
+ """
121
+ from IPython.display import Image
122
+ import base64
123
+ Image(data=base64.b64decode(%r), format="png")
124
+ """
125
+ % PNG_B64,
126
+ store_history=True,
127
+ )
128
+
129
+ rendered = shell._ipythonng_stream.getvalue()
130
+ expected = build_render_bytes(PNG_BYTES, out=GeometryProbe(), cell_width_px=8, cell_height_px=16).decode("utf-8")
131
+ assert rendered == "\n" + expected
132
+
133
+
134
+ def test_kitty_rendering_matches_kittytgp_inside_tmux(shell, monkeypatch):
135
+ monkeypatch.setenv("TMUX", "/tmp/tmux")
136
+ monkeypatch.setattr(kittycore.secrets, "randbelow", lambda _: 0x654321 - 1)
137
+
138
+ shell.run_cell(
139
+ """
140
+ from IPython.display import Image, display
141
+ import base64
142
+ display(Image(data=base64.b64decode(%r), format="png"))
143
+ """
144
+ % PNG_B64,
145
+ store_history=True,
146
+ )
147
+
148
+ rendered = shell._ipythonng_stream.getvalue()
149
+ expected = build_render_bytes(PNG_BYTES, out=GeometryProbe(), cell_width_px=8, cell_height_px=16).decode("utf-8")
150
+ assert rendered == expected
151
+ assert PLACEHOLDER in rendered
152
+ assert "\x1bPtmux;" in rendered
153
+ assert "[image/png]" not in rendered
154
+
155
+
156
+ def test_matplotlib_inline_plots_render_via_image_png(shell, monkeypatch, tmp_path):
157
+ pytest.importorskip("matplotlib")
158
+ mplconfig = tmp_path / "mplconfig"
159
+ mplconfig.mkdir()
160
+ monkeypatch.setenv("MPLCONFIGDIR", str(mplconfig))
161
+ monkeypatch.delenv("TMUX", raising=False)
162
+ monkeypatch.setattr(kittycore.secrets, "randbelow", lambda _: 0x333333 - 1)
163
+
164
+ shell.run_line_magic("matplotlib", "inline")
165
+ shell.run_cell("import matplotlib.pyplot as plt\nplt.plot([1, 2, 3])", store_history=True)
166
+
167
+ rendered = shell._ipythonng_stream.getvalue()
168
+ (_, _, (_, output)) = list(shell.history_manager.get_range(output=True))[-1]
169
+ assert PLACEHOLDER in rendered
170
+ assert "\x1b_G" in rendered
171
+ assert "matplotlib.lines.Line2D" in output
172
+ assert "[image/png]" in output
173
+
174
+
175
+ def test_matplotlib_inline_magic_suppresses_no_event_loop_message(shell, monkeypatch, tmp_path, capsys):
176
+ pytest.importorskip("matplotlib")
177
+ mplconfig = tmp_path / "mplconfig"
178
+ mplconfig.mkdir()
179
+ monkeypatch.setenv("MPLCONFIGDIR", str(mplconfig))
180
+
181
+ capsys.readouterr()
182
+ shell.run_line_magic("matplotlib", "inline")
183
+ captured = capsys.readouterr()
184
+ assert "No event loop hook running." not in captured.out
185
+
186
+
187
+ def test_matplotlib_inline_after_prior_plot_renders_future_plots(shell, monkeypatch, tmp_path, capsys):
188
+ pytest.importorskip("matplotlib")
189
+ mplconfig = tmp_path / "mplconfig"
190
+ mplconfig.mkdir()
191
+ monkeypatch.setenv("MPLCONFIGDIR", str(mplconfig))
192
+ monkeypatch.delenv("TMUX", raising=False)
193
+ monkeypatch.setattr(kittycore.secrets, "randbelow", lambda _: 0x444444 - 1)
194
+
195
+ shell.run_cell("import matplotlib\nmatplotlib.use('agg')\nimport matplotlib.pyplot as plt\nplt.plot([1, 2, 3])", store_history=True)
196
+ shell._ipythonng_stream.seek(0)
197
+ shell._ipythonng_stream.truncate(0)
198
+
199
+ capsys.readouterr()
200
+ shell.run_line_magic("matplotlib", "inline")
201
+ captured = capsys.readouterr()
202
+ assert "No event loop hook running." not in captured.out
203
+
204
+ shell.run_cell("plt.plot([4, 5, 6])", store_history=True)
205
+
206
+ rendered = shell._ipythonng_stream.getvalue()
207
+ output_types = [o.output_type for o in shell.history_manager.outputs.get(shell.execution_count - 1, [])]
208
+ assert PLACEHOLDER in rendered
209
+ assert "\x1b_G" in rendered
210
+ assert "display_data" in output_types