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
from ipython_demo import App
from ipython_demo import CallOptions
app = App()
call = app.run_code(
"print('hello'); 2 + 2",
shell_name="new",
collapsed=False,
options=CallOptions(collapsed=False),
)
```
`call` contains the submitted `code`, `call_id`, `shell_name`, `collapsed`
state, a typed `result`, and ordered typed `events`. Event sequences are
`call` contains the submitted `code`, `call_id`, `shell_name`, typed `options`,
a typed `result`, and ordered typed `events`. Event sequences are
stable within a call, so a frontend can use `(call_id, sequence)` as its
idempotency and React key pair.
+2 -5
View File
@@ -10,6 +10,7 @@ from .models import (
CallError,
CallEvent,
CallEventKind,
CallOptions,
CallResponse,
CallResult,
ShellInfo,
@@ -27,6 +28,7 @@ __all__ = [
"CallEvent",
"CallEventKind",
"CallError",
"CallOptions",
"CallResponse",
"CallResult",
"error_from_execution_result",
@@ -40,8 +42,3 @@ __all__ = [
"run_cell_and_collect",
"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 .models import CallResponse, ShellInfo
from .models import CallOptions, CallResponse, ShellInfo
from .shell import Shell
@@ -59,9 +59,11 @@ class App:
code: str,
shell_name: str = "last",
*,
collapsed: bool = False,
options: CallOptions | None = None,
) -> CallResponse:
"""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)
call_id = uuid4().hex
result, events = shell.run_cell(code, call_id=call_id)
@@ -69,7 +71,7 @@ class App:
call_id=call_id,
shell_name=selected_name,
code=code,
collapsed=collapsed,
options=options,
result=result,
events=events,
shell=shell.describe(),
@@ -84,6 +86,10 @@ def main() -> None:
with contextlib.redirect_stdout(io.StringIO()):
calls = [
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))
+8 -1
View File
@@ -15,6 +15,13 @@ CallEventKind = Literal[
ShellStatus = Literal["ready", "running", "error"]
@dataclass
class CallOptions:
"""Presentation and execution hints attached to one call."""
collapsed: bool = False
@dataclass
class CallEvent:
"""One ordered, UI-safe event emitted while a call executes."""
@@ -54,7 +61,7 @@ class CallResponse:
call_id: str
shell_name: str
code: str
collapsed: bool
options: CallOptions
result: CallResult
events: list[CallEvent]
shell: ShellInfo
+12 -16
View File
@@ -1,5 +1,5 @@
from collections import defaultdict
from datetime import datetime, timezone
from datetime import UTC, datetime
from uuid import uuid4
from IPython.core.interactiveshell import InteractiveShell
@@ -25,7 +25,7 @@ class Shell:
def __init__(self, name: str):
self.name = name
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.status: ShellStatus = "ready"
self._last_execution_count = 0
@@ -55,7 +55,7 @@ class Shell:
self.status = "running"
try:
call_result, events = run_cell_and_collect(
self.init_shell(),
self.shell,
code,
call_id=call_id,
)
@@ -64,7 +64,7 @@ class Shell:
raise
else:
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
return call_result, events
@@ -114,9 +114,11 @@ def run_cell_and_collect(
isolate_output_history(shell)
execution = shell.run_cell(code, store_history=True)
records = tuple(
history_manager.outputs.get(execution.execution_count, ())
) if execution.execution_count is not None else ()
records = (
tuple(history_manager.outputs.get(execution.execution_count, ()))
if execution.execution_count is not None
else ()
)
events = [
event_from_history_output(
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().
if plt:
activate_matplotlib(backend)
shell.magics_manager.registry[
"ExecutionMagics"
].default_runner = mpl_runner(shell.safe_execfile)
shell.magics_manager.registry["ExecutionMagics"].default_runner = mpl_runner(
shell.safe_execfile
)
# This connects matplotlib-inline to THIS shell.
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
shell.user_ns.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.
+11 -8
View File
@@ -9,7 +9,7 @@ import unittest
from unittest.mock import Mock, patch
from ipython_demo.app import App
from ipython_demo.models import CallResponse
from ipython_demo.models import CallOptions, CallResponse
class AppTests(unittest.TestCase):
@@ -41,7 +41,7 @@ class AppTests(unittest.TestCase):
payload = json.loads(completed.stdout)
self.assertEqual(len(payload), 2)
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):
app = App()
@@ -50,17 +50,20 @@ class AppTests(unittest.TestCase):
call = app.run_code(
"print('hello'); 2 + 2",
shell_name="new",
collapsed=True,
options=CallOptions(collapsed=True),
)
self.assertIsInstance(call, CallResponse)
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([event.kind for event in call.events], [
"stdout",
"execute_result",
])
self.assertEqual(
[event.kind for event in call.events],
[
"stdout",
"execute_result",
],
)
def test_last_reuses_shell_but_each_call_has_new_id(self):
app = App()
+12 -5
View File
@@ -39,7 +39,9 @@ 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)
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()
@@ -66,9 +68,12 @@ class ShellTests(unittest.TestCase):
sequence=4,
)
self.assertEqual(stdout, models_module.CallEvent(
call_id="call-1", sequence=3, kind="stdout", text="hello\n"
))
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):
@@ -159,7 +164,9 @@ class ShellTests(unittest.TestCase):
wrapper.init_shell()
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(
capture,
Generated
-23
View File
@@ -125,7 +125,6 @@ dependencies = [
{ name = "ipython" },
{ name = "matplotlib" },
{ name = "matplotlib-inline" },
{ name = "sympy" },
{ name = "wonderwords" },
]
@@ -134,7 +133,6 @@ requires-dist = [
{ name = "ipython", specifier = ">=9.16.1" },
{ name = "matplotlib", specifier = ">=3.11.1" },
{ name = "matplotlib-inline", specifier = ">=0.1.7" },
{ name = "sympy", specifier = ">=1.14.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" },
]
[[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]]
name = "numpy"
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" },
]
[[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]]
name = "traitlets"
version = "5.16.1"