CallOptions

This commit is contained in:
lda
2026-08-30 03:39:26 +07:00 Verified
parent 2587b07b4a
commit 282dc5b4e8
8 changed files with 59 additions and 65 deletions
+4 -3
View File
@@ -4,17 +4,18 @@ The application-facing API is:
```python ```python
from ipython_demo import App from ipython_demo import App
from ipython_demo import CallOptions
app = App() app = App()
call = app.run_code( call = app.run_code(
"print('hello'); 2 + 2", "print('hello'); 2 + 2",
shell_name="new", shell_name="new",
collapsed=False, options=CallOptions(collapsed=False),
) )
``` ```
`call` contains the submitted `code`, `call_id`, `shell_name`, `collapsed` `call` contains the submitted `code`, `call_id`, `shell_name`, typed `options`,
state, a typed `result`, and ordered typed `events`. Event sequences are a typed `result`, and ordered typed `events`. Event sequences are
stable within a call, so a frontend can use `(call_id, sequence)` as its stable within a call, so a frontend can use `(call_id, sequence)` as its
idempotency and React key pair. idempotency and React key pair.
+2 -5
View File
@@ -10,6 +10,7 @@ from .models import (
CallError, CallError,
CallEvent, CallEvent,
CallEventKind, CallEventKind,
CallOptions,
CallResponse, CallResponse,
CallResult, CallResult,
ShellInfo, ShellInfo,
@@ -27,6 +28,7 @@ __all__ = [
"CallEvent", "CallEvent",
"CallEventKind", "CallEventKind",
"CallError", "CallError",
"CallOptions",
"CallResponse", "CallResponse",
"CallResult", "CallResult",
"error_from_execution_result", "error_from_execution_result",
@@ -40,8 +42,3 @@ __all__ = [
"run_cell_and_collect", "run_cell_and_collect",
"setup_shell", "setup_shell",
] ]
def main() -> None:
"""Run the package's minimal command-line entry point."""
print("Hello from ipython_demo!")
+10 -4
View File
@@ -6,7 +6,7 @@ from uuid import uuid4
from wonderwords import RandomWord from wonderwords import RandomWord
from .models import CallResponse, ShellInfo from .models import CallOptions, CallResponse, ShellInfo
from .shell import Shell from .shell import Shell
@@ -59,9 +59,11 @@ class App:
code: str, code: str,
shell_name: str = "last", shell_name: str = "last",
*, *,
collapsed: bool = False, options: CallOptions | None = None,
) -> CallResponse: ) -> CallResponse:
"""Run code in the selected shell and return the complete call payload.""" """Run code in the selected shell and return the complete call payload."""
if options is None:
options = CallOptions()
selected_name, shell = self._select_shell(shell_name) selected_name, shell = self._select_shell(shell_name)
call_id = uuid4().hex call_id = uuid4().hex
result, events = shell.run_cell(code, call_id=call_id) result, events = shell.run_cell(code, call_id=call_id)
@@ -69,7 +71,7 @@ class App:
call_id=call_id, call_id=call_id,
shell_name=selected_name, shell_name=selected_name,
code=code, code=code,
collapsed=collapsed, options=options,
result=result, result=result,
events=events, events=events,
shell=shell.describe(), shell=shell.describe(),
@@ -84,6 +86,10 @@ def main() -> None:
with contextlib.redirect_stdout(io.StringIO()): with contextlib.redirect_stdout(io.StringIO()):
calls = [ calls = [
app.run_code("print('hello from the shell'); 2 + 2", shell_name="new"), app.run_code("print('hello from the shell'); 2 + 2", shell_name="new"),
app.run_code("40 + 2", shell_name="last", collapsed=True), app.run_code(
"40 + 2",
shell_name="last",
options=CallOptions(collapsed=True),
),
] ]
print(json.dumps([asdict(call) for call in calls], indent=2, default=repr)) print(json.dumps([asdict(call) for call in calls], indent=2, default=repr))
+8 -1
View File
@@ -15,6 +15,13 @@ CallEventKind = Literal[
ShellStatus = Literal["ready", "running", "error"] ShellStatus = Literal["ready", "running", "error"]
@dataclass
class CallOptions:
"""Presentation and execution hints attached to one call."""
collapsed: bool = False
@dataclass @dataclass
class CallEvent: class CallEvent:
"""One ordered, UI-safe event emitted while a call executes.""" """One ordered, UI-safe event emitted while a call executes."""
@@ -54,7 +61,7 @@ class CallResponse:
call_id: str call_id: str
shell_name: str shell_name: str
code: str code: str
collapsed: bool options: CallOptions
result: CallResult result: CallResult
events: list[CallEvent] events: list[CallEvent]
shell: ShellInfo shell: ShellInfo
+12 -16
View File
@@ -1,5 +1,5 @@
from collections import defaultdict from collections import defaultdict
from datetime import datetime, timezone from datetime import UTC, datetime
from uuid import uuid4 from uuid import uuid4
from IPython.core.interactiveshell import InteractiveShell from IPython.core.interactiveshell import InteractiveShell
@@ -25,7 +25,7 @@ class Shell:
def __init__(self, name: str): def __init__(self, name: str):
self.name = name self.name = name
self.shell_id = uuid4().hex self.shell_id = uuid4().hex
self.created_at = datetime.now(timezone.utc).isoformat() self.created_at = datetime.now(UTC).isoformat()
self.last_used_at: str | None = None self.last_used_at: str | None = None
self.status: ShellStatus = "ready" self.status: ShellStatus = "ready"
self._last_execution_count = 0 self._last_execution_count = 0
@@ -55,7 +55,7 @@ class Shell:
self.status = "running" self.status = "running"
try: try:
call_result, events = run_cell_and_collect( call_result, events = run_cell_and_collect(
self.init_shell(), self.shell,
code, code,
call_id=call_id, call_id=call_id,
) )
@@ -64,7 +64,7 @@ class Shell:
raise raise
else: else:
self.status = "ready" self.status = "ready"
self.last_used_at = datetime.now(timezone.utc).isoformat() self.last_used_at = datetime.now(UTC).isoformat()
self._last_execution_count = call_result.execution_count or 0 self._last_execution_count = call_result.execution_count or 0
return call_result, events return call_result, events
@@ -114,9 +114,11 @@ def run_cell_and_collect(
isolate_output_history(shell) isolate_output_history(shell)
execution = shell.run_cell(code, store_history=True) execution = shell.run_cell(code, store_history=True)
records = tuple( records = (
history_manager.outputs.get(execution.execution_count, ()) tuple(history_manager.outputs.get(execution.execution_count, ()))
) if execution.execution_count is not None else () if execution.execution_count is not None
else ()
)
events = [ events = [
event_from_history_output( event_from_history_output(
record, record,
@@ -170,9 +172,9 @@ def setup_shell(shell: InteractiveShell, plt: bool = False, pylab: bool = True):
# This is the non-GUI part of enable_matplotlib(). # This is the non-GUI part of enable_matplotlib().
if plt: if plt:
activate_matplotlib(backend) activate_matplotlib(backend)
shell.magics_manager.registry[ shell.magics_manager.registry["ExecutionMagics"].default_runner = mpl_runner(
"ExecutionMagics" shell.safe_execfile
].default_runner = mpl_runner(shell.safe_execfile) )
# This connects matplotlib-inline to THIS shell. # This connects matplotlib-inline to THIS shell.
configure_inline_support(shell, backend) configure_inline_support(shell, backend)
@@ -190,9 +192,3 @@ def setup_shell(shell: InteractiveShell, plt: bool = False, pylab: bool = True):
import_pylab(pylab_ns, import_all=False) # no import * allowed import_pylab(pylab_ns, import_all=False) # no import * allowed
shell.user_ns.update(pylab_ns) shell.user_ns.update(pylab_ns)
shell.user_ns_hidden.update(pylab_ns) shell.user_ns_hidden.update(pylab_ns)
# tisi = InteractiveShell.instance()
# default_shell = Shell("default_shell")
# default_shell.init_shell(tisi)
# use me, or dont. idc. but we need to give the default instance the select_figure_formats because i neeeeeeeeeeeeeeeeeeeed it.
+9 -6
View File
@@ -9,7 +9,7 @@ import unittest
from unittest.mock import Mock, patch from unittest.mock import Mock, patch
from ipython_demo.app import App from ipython_demo.app import App
from ipython_demo.models import CallResponse from ipython_demo.models import CallOptions, CallResponse
class AppTests(unittest.TestCase): class AppTests(unittest.TestCase):
@@ -41,7 +41,7 @@ class AppTests(unittest.TestCase):
payload = json.loads(completed.stdout) payload = json.loads(completed.stdout)
self.assertEqual(len(payload), 2) self.assertEqual(len(payload), 2)
self.assertEqual(payload[0]["events"][0]["kind"], "stdout") self.assertEqual(payload[0]["events"][0]["kind"], "stdout")
self.assertTrue(payload[1]["collapsed"]) self.assertTrue(payload[1]["options"]["collapsed"])
def test_run_code_returns_frontend_facing_call_response(self): def test_run_code_returns_frontend_facing_call_response(self):
app = App() app = App()
@@ -50,17 +50,20 @@ class AppTests(unittest.TestCase):
call = app.run_code( call = app.run_code(
"print('hello'); 2 + 2", "print('hello'); 2 + 2",
shell_name="new", shell_name="new",
collapsed=True, options=CallOptions(collapsed=True),
) )
self.assertIsInstance(call, CallResponse) self.assertIsInstance(call, CallResponse)
self.assertEqual(call.code, "print('hello'); 2 + 2") self.assertEqual(call.code, "print('hello'); 2 + 2")
self.assertTrue(call.collapsed) self.assertTrue(call.options.collapsed)
self.assertEqual(call.result.result, 4) self.assertEqual(call.result.result, 4)
self.assertEqual([event.kind for event in call.events], [ self.assertEqual(
[event.kind for event in call.events],
[
"stdout", "stdout",
"execute_result", "execute_result",
]) ],
)
def test_last_reuses_shell_but_each_call_has_new_id(self): def test_last_reuses_shell_but_each_call_has_new_id(self):
app = App() app = App()
+11 -4
View File
@@ -39,7 +39,9 @@ class ShellTests(unittest.TestCase):
def test_init_shell_has_no_seed_escape_hatch(self): def test_init_shell_has_no_seed_escape_hatch(self):
import inspect import inspect
self.assertNotIn("seed", inspect.signature(shell_module.Shell.init_shell).parameters) self.assertNotIn(
"seed", inspect.signature(shell_module.Shell.init_shell).parameters
)
def test_package_import_has_no_experiment_side_effects(self): def test_package_import_has_no_experiment_side_effects(self):
env = os.environ.copy() env = os.environ.copy()
@@ -66,9 +68,12 @@ class ShellTests(unittest.TestCase):
sequence=4, sequence=4,
) )
self.assertEqual(stdout, models_module.CallEvent( self.assertEqual(
stdout,
models_module.CallEvent(
call_id="call-1", sequence=3, kind="stdout", text="hello\n" call_id="call-1", sequence=3, kind="stdout", text="hello\n"
)) ),
)
self.assertEqual(result.data, {"text/plain": "42"}) self.assertEqual(result.data, {"text/plain": "42"})
def test_run_cell_and_collect_returns_execution_and_events(self): def test_run_cell_and_collect_returns_execution_and_events(self):
@@ -159,7 +164,9 @@ class ShellTests(unittest.TestCase):
wrapper.init_shell() wrapper.init_shell()
def fail_capture_output(*args, **kwargs): def fail_capture_output(*args, **kwargs):
raise AssertionError("Shell.run_cell must use the shell-local output records") raise AssertionError(
"Shell.run_cell must use the shell-local output records"
)
with patch.object( with patch.object(
capture, capture,
Generated
-23
View File
@@ -125,7 +125,6 @@ dependencies = [
{ name = "ipython" }, { name = "ipython" },
{ name = "matplotlib" }, { name = "matplotlib" },
{ name = "matplotlib-inline" }, { name = "matplotlib-inline" },
{ name = "sympy" },
{ name = "wonderwords" }, { name = "wonderwords" },
] ]
@@ -134,7 +133,6 @@ requires-dist = [
{ name = "ipython", specifier = ">=9.16.1" }, { name = "ipython", specifier = ">=9.16.1" },
{ name = "matplotlib", specifier = ">=3.11.1" }, { name = "matplotlib", specifier = ">=3.11.1" },
{ name = "matplotlib-inline", specifier = ">=0.1.7" }, { name = "matplotlib-inline", specifier = ">=0.1.7" },
{ name = "sympy", specifier = ">=1.14.0" },
{ name = "wonderwords", specifier = ">=2.2.0" }, { name = "wonderwords", specifier = ">=2.2.0" },
] ]
@@ -281,15 +279,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" }, { url = "https://files.pythonhosted.org/packages/41/09/5b161152e2d90f7b87f781c2e1267494aef9c32498df793f73ad0a0a494a/matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6", size = 9534, upload-time = "2026-05-08T17:33:32.055Z" },
] ]
[[package]]
name = "mpmath"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
]
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "2.5.2" version = "2.5.2"
@@ -526,18 +515,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
] ]
[[package]]
name = "sympy"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
]
[[package]] [[package]]
name = "traitlets" name = "traitlets"
version = "5.16.1" version = "5.16.1"