import contextlib import importlib.util import io import os import subprocess import sys import types import unittest from pathlib import Path from unittest.mock import patch from IPython.core.interactiveshell import InteractiveShell from IPython.core.history import HistoryOutput from IPython.utils import capture PACKAGE_ROOT = Path(__file__).parents[1] / "src" / "ipython_demo" package = types.ModuleType("ipython_demo") package.__path__ = [str(PACKAGE_ROOT)] sys.modules["ipython_demo"] = package def load_package_module(name: str): path = PACKAGE_ROOT / f"{name}.py" spec = importlib.util.spec_from_file_location(f"ipython_demo.{name}", path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module models_module = load_package_module("models") events_module = load_package_module("events") shell_module = load_package_module("shell") class ShellTests(unittest.TestCase): def test_init_shell_has_no_seed_escape_hatch(self): import inspect self.assertNotIn( "seed", inspect.signature(shell_module.Shell.init_shell).parameters ) def test_package_import_has_no_experiment_side_effects(self): env = os.environ.copy() env["PYTHONPATH"] = str(PACKAGE_ROOT.parent) completed = subprocess.run( [sys.executable, "-c", "import ipython_demo; print('imported')"], capture_output=True, text=True, env=env, check=True, ) self.assertEqual(completed.stdout, "imported\n") def test_event_from_history_output_preserves_streams_and_mime(self): stdout = events_module.event_from_history_output( HistoryOutput("out_stream", {"stream": ["hello", "\n"]}), call_id="call-1", sequence=3, ) result = events_module.event_from_history_output( HistoryOutput("execute_result", {"text/plain": "42"}), call_id="call-1", sequence=4, ) self.assertEqual( stdout, models_module.CallEvent( call_id="call-1", sequence=3, kind="stdout", text="hello\n" ), ) self.assertEqual(result.data, {"text/plain": "42"}) def test_run_cell_and_collect_returns_execution_and_events(self): shell = InteractiveShell() shell_module.setup_shell(shell, plt=False, pylab=False) with contextlib.redirect_stdout(io.StringIO()): execution, events = shell_module.run_cell_and_collect( shell, "print('hello'); 42", call_id="call-1", ) self.assertIsInstance(execution, models_module.CallResult) self.assertEqual(execution.result, 42) self.assertEqual([event.kind for event in events], ["stdout", "execute_result"]) self.assertEqual(events[0].text, "hello\n") self.assertEqual(events[1].data, {"text/plain": "42"}) def test_run_cell_and_collect_converts_execution_error(self): shell = InteractiveShell() shell_module.setup_shell(shell, plt=False, pylab=False) with contextlib.redirect_stdout(io.StringIO()): execution, events = shell_module.run_cell_and_collect( shell, "raise ValueError('boom')", call_id="call-2", ) self.assertIsInstance(execution.error, models_module.CallError) self.assertEqual(execution.error.ename, "ValueError") self.assertEqual(execution.error.evalue, "boom") self.assertEqual(events[-1].kind, "error") self.assertEqual(events[-1].data["ename"], "ValueError") self.assertEqual(events[-1].data["evalue"], "boom") def test_setup_shell_isolates_history_output_storage(self): first = InteractiveShell() second = InteractiveShell() shell_module.setup_shell(first, plt=False, pylab=False) shell_module.setup_shell(second, plt=False, pylab=False) self.assertIsNot(first.history_manager.outputs, second.history_manager.outputs) def test_setup_shell_configures_mime_output_without_enable_gui(self): shell = InteractiveShell() def fail_enable_gui(*args, **kwargs): raise AssertionError("base InteractiveShell must not require enable_gui") with patch.object(InteractiveShell, "enable_gui", fail_enable_gui): shell_module.setup_shell(shell, plt=True, pylab=False) with contextlib.redirect_stdout(io.StringIO()): result = shell.run_cell("2 + 2", store_history=True) self.assertEqual(result.result, 4) records = shell.history_manager.outputs[result.execution_count] self.assertEqual([record.output_type for record in records], ["execute_result"]) self.assertEqual(records[0].bundle["text/plain"], "4") def test_setup_shell_keeps_pylab_namespace_setup_without_enable_gui(self): shell = InteractiveShell() def fail_enable_gui(*args, **kwargs): raise AssertionError("pylab setup must not require enable_gui") with patch.object(InteractiveShell, "enable_gui", fail_enable_gui): shell_module.setup_shell(shell, plt=True, pylab=True) self.assertIn("plt", shell.user_ns) self.assertIn("np", shell.user_ns) self.assertIs(shell.user_ns["plt"], shell.user_ns_hidden["plt"]) self.assertIs(shell.user_ns["np"], shell.user_ns_hidden["np"]) def test_setup_shell_installs_matplotlib_runner_without_enable_gui(self): shell = InteractiveShell() shell_module.setup_shell(shell, plt=True, pylab=False) self.assertIsNotNone( shell.magics_manager.registry["ExecutionMagics"].default_runner ) def test_run_cell_does_not_use_global_capture_output(self): wrapper = shell_module.Shell("test") wrapper.init_shell() def fail_capture_output(*args, **kwargs): raise AssertionError( "Shell.run_cell must use the shell-local output records" ) with patch.object( capture, "capture_output", fail_capture_output, ): with contextlib.redirect_stdout(io.StringIO()): result, events = wrapper.run_cell( "print('hello'); 42", call_id="call-1", ) self.assertEqual(result.result, 42) self.assertEqual([event.kind for event in events], ["stdout", "execute_result"]) self.assertEqual(events[0].text, "hello\n") if __name__ == "__main__": unittest.main()