CallOptions
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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.
|
|
||||||
|
|||||||
+11
-8
@@ -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(
|
||||||
"stdout",
|
[event.kind for event in call.events],
|
||||||
"execute_result",
|
[
|
||||||
])
|
"stdout",
|
||||||
|
"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()
|
||||||
|
|||||||
+12
-5
@@ -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(
|
||||||
call_id="call-1", sequence=3, kind="stdout", text="hello\n"
|
stdout,
|
||||||
))
|
models_module.CallEvent(
|
||||||
|
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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user