CallOptions
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
+9
-6
@@ -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], [
|
||||
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()
|
||||
|
||||
+11
-4
@@ -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(
|
||||
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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user