shared shell registry
This commit is contained in:
@@ -22,6 +22,7 @@ from .models import (
|
|||||||
ShellInfo,
|
ShellInfo,
|
||||||
ShellStatus,
|
ShellStatus,
|
||||||
)
|
)
|
||||||
|
from .registry import ShellRegistry
|
||||||
from .shell import (
|
from .shell import (
|
||||||
Shell,
|
Shell,
|
||||||
carefully_setup_shell,
|
carefully_setup_shell,
|
||||||
@@ -29,7 +30,7 @@ from .shell import (
|
|||||||
run_cell_and_collect,
|
run_cell_and_collect,
|
||||||
setup_shell,
|
setup_shell,
|
||||||
)
|
)
|
||||||
from .utils import generate_good_names
|
from .utils import generate_call_id, generate_good_names, generate_omfg_names
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"App",
|
"App",
|
||||||
@@ -49,12 +50,15 @@ __all__ = [
|
|||||||
"KernelMessage",
|
"KernelMessage",
|
||||||
"Shell",
|
"Shell",
|
||||||
"ShellInfo",
|
"ShellInfo",
|
||||||
|
"ShellRegistry",
|
||||||
"ShellStatus",
|
"ShellStatus",
|
||||||
"carefully_setup_shell",
|
"carefully_setup_shell",
|
||||||
"error_from_execution_result",
|
"error_from_execution_result",
|
||||||
"event_from_execution_result",
|
"event_from_execution_result",
|
||||||
"event_from_history_output",
|
"event_from_history_output",
|
||||||
|
"generate_call_id",
|
||||||
"generate_good_names",
|
"generate_good_names",
|
||||||
|
"generate_omfg_names",
|
||||||
"isolate_output_history",
|
"isolate_output_history",
|
||||||
"run_cell_and_collect",
|
"run_cell_and_collect",
|
||||||
"setup_shell",
|
"setup_shell",
|
||||||
|
|||||||
+27
-33
@@ -7,9 +7,10 @@ from time import monotonic
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from ipython_shell.utils import generate_good_names
|
from ipython_shell.utils import generate_call_id
|
||||||
|
|
||||||
from .models import AppInfo, CallOptions, CallResponse, ShellInfo
|
from .models import AppInfo, CallOptions, CallResponse, ShellInfo
|
||||||
|
from .registry import ShellRegistry
|
||||||
from .shell import Shell
|
from .shell import Shell
|
||||||
|
|
||||||
|
|
||||||
@@ -21,52 +22,45 @@ class App:
|
|||||||
self.created_at = datetime.now(UTC).isoformat()
|
self.created_at = datetime.now(UTC).isoformat()
|
||||||
self._started_at = monotonic()
|
self._started_at = monotonic()
|
||||||
self._total_calls = 0
|
self._total_calls = 0
|
||||||
self.shells: dict[str, Shell] = {}
|
self._shell_registry = ShellRegistry(Shell)
|
||||||
self.last_shell: str | None = None
|
self._issued_call_ids: set[str] = set()
|
||||||
|
|
||||||
def _new_shell(self) -> tuple[str, Shell]:
|
@property
|
||||||
name = generate_good_names()
|
def shells(self) -> dict[str, Shell]:
|
||||||
while name in self.shells:
|
"""Expose the managed shells for compatibility with the old API."""
|
||||||
name = generate_good_names()
|
return self._shell_registry.shells
|
||||||
shell = Shell(name)
|
|
||||||
self.shells[name] = shell
|
@property
|
||||||
self.last_shell = name
|
def last_shell(self) -> str | None:
|
||||||
return name, shell
|
"""Return the name of the most recently selected shell."""
|
||||||
|
return self._shell_registry.last_shell
|
||||||
|
|
||||||
def _select_shell(self, shell_name: str) -> tuple[str, Shell]:
|
def _select_shell(self, shell_name: str) -> tuple[str, Shell]:
|
||||||
if shell_name == "new":
|
return self._shell_registry.select(shell_name)
|
||||||
return self._new_shell()
|
|
||||||
|
|
||||||
if shell_name == "last":
|
|
||||||
if self.last_shell is None:
|
|
||||||
return self._new_shell()
|
|
||||||
return self.last_shell, self.shells[self.last_shell]
|
|
||||||
|
|
||||||
if shell_name not in self.shells:
|
|
||||||
self.shells[shell_name] = Shell(shell_name)
|
|
||||||
self.last_shell = shell_name
|
|
||||||
return shell_name, self.shells[shell_name]
|
|
||||||
|
|
||||||
def list_shells(self) -> list[ShellInfo]:
|
def list_shells(self) -> list[ShellInfo]:
|
||||||
"""Return frontend-safe metadata for all known shells."""
|
"""Return frontend-safe metadata for all known shells."""
|
||||||
return [shell.describe() for shell in self.shells.values()]
|
return self._shell_registry.list_shells()
|
||||||
|
|
||||||
def create_shell(self) -> ShellInfo:
|
def create_shell(self) -> ShellInfo:
|
||||||
"""Create a fresh named shell and return its metadata."""
|
"""Create a fresh named shell and return its metadata."""
|
||||||
_, shell = self._new_shell()
|
return self._shell_registry.create_shell()
|
||||||
return shell.describe()
|
|
||||||
|
|
||||||
def get_shell(self, shell_name: str) -> ShellInfo:
|
def get_shell(self, shell_name: str) -> ShellInfo:
|
||||||
"""Return frontend-safe metadata for a single shell."""
|
"""Return frontend-safe metadata for a single shell."""
|
||||||
if shell_name not in self.shells:
|
return self._shell_registry.get_shell(shell_name)
|
||||||
raise KeyError(f"Shell {shell_name} does not exist")
|
|
||||||
return self.shells[shell_name].describe()
|
|
||||||
|
|
||||||
def get_last_shell(self) -> ShellInfo:
|
def get_last_shell(self) -> ShellInfo:
|
||||||
"""Return metadata for the most recently selected shell."""
|
"""Return metadata for the most recently selected shell."""
|
||||||
if self.last_shell is None:
|
return self._shell_registry.get_last_shell()
|
||||||
raise KeyError("No shell exists yet")
|
|
||||||
return self.get_shell(self.last_shell)
|
def _new_call_id(self) -> str:
|
||||||
|
"""Generate a readable call ID that is unique for this app."""
|
||||||
|
call_id = generate_call_id()
|
||||||
|
while call_id in self._issued_call_ids:
|
||||||
|
call_id = generate_call_id()
|
||||||
|
self._issued_call_ids.add(call_id)
|
||||||
|
return call_id
|
||||||
|
|
||||||
def info(self) -> AppInfo:
|
def info(self) -> AppInfo:
|
||||||
"""Return runtime metadata without initializing or inspecting shells."""
|
"""Return runtime metadata without initializing or inspecting shells."""
|
||||||
@@ -96,7 +90,7 @@ class App:
|
|||||||
if options is None:
|
if options is None:
|
||||||
options = CallOptions()
|
options = CallOptions()
|
||||||
selected_name, shell = self._select_shell(shell_name)
|
selected_name, shell = self._select_shell(shell_name)
|
||||||
call_id = uuid4().hex
|
call_id = self._new_call_id()
|
||||||
self._total_calls += 1
|
self._total_calls += 1
|
||||||
execution, events = shell.run_cell(code, call_id=call_id)
|
execution, events = shell.run_cell(code, call_id=call_id)
|
||||||
return CallResponse(
|
return CallResponse(
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from time import monotonic
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from ..utils import generate_good_names
|
from ..models import AppInfo, ShellInfo
|
||||||
|
from ..registry import ShellRegistry
|
||||||
|
from ..utils import generate_call_id
|
||||||
from .shell import JupyterShell
|
from .shell import JupyterShell
|
||||||
from .transport import InputRequest, KernelMessage
|
from .transport import InputRequest, KernelMessage
|
||||||
|
|
||||||
@@ -10,36 +14,61 @@ class JupyterApp:
|
|||||||
"""Coordinate named persistent shells backed by Jupyter kernels."""
|
"""Coordinate named persistent shells backed by Jupyter kernels."""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.shells: dict[str, JupyterShell] = {}
|
self.app_id = uuid4().hex
|
||||||
self.last_shell: str | None = None
|
self.created_at = datetime.now(UTC).isoformat()
|
||||||
|
self._started_at = monotonic()
|
||||||
|
self._total_calls = 0
|
||||||
|
self._shell_registry = ShellRegistry(JupyterShell)
|
||||||
self._calls: dict[str, JupyterShell] = {}
|
self._calls: dict[str, JupyterShell] = {}
|
||||||
|
self._issued_call_ids: set[str] = set()
|
||||||
|
|
||||||
def _new_shell(self) -> tuple[str, JupyterShell]:
|
@property
|
||||||
name = generate_good_names()
|
def shells(self) -> dict[str, JupyterShell]:
|
||||||
while name in self.shells:
|
"""Expose the managed shells for compatibility with the sync app."""
|
||||||
name = generate_good_names()
|
return self._shell_registry.shells
|
||||||
shell = JupyterShell(name)
|
|
||||||
self.shells[name] = shell
|
@property
|
||||||
self.last_shell = name
|
def last_shell(self) -> str | None:
|
||||||
return name, shell
|
"""Return the name of the most recently selected shell."""
|
||||||
|
return self._shell_registry.last_shell
|
||||||
|
|
||||||
def _select_shell(self, shell_name: str) -> tuple[str, JupyterShell]:
|
def _select_shell(self, shell_name: str) -> tuple[str, JupyterShell]:
|
||||||
if shell_name == "new":
|
return self._shell_registry.select(shell_name)
|
||||||
return self._new_shell()
|
|
||||||
if shell_name == "last":
|
|
||||||
if self.last_shell is None:
|
|
||||||
return self._new_shell()
|
|
||||||
return self.last_shell, self.shells[self.last_shell]
|
|
||||||
if shell_name not in self.shells:
|
|
||||||
self.shells[shell_name] = JupyterShell(shell_name)
|
|
||||||
self.last_shell = shell_name
|
|
||||||
return shell_name, self.shells[shell_name]
|
|
||||||
|
|
||||||
def get_shell(self, shell_name: str) -> JupyterShell:
|
def list_shells(self) -> list[ShellInfo]:
|
||||||
"""Return a named Jupyter shell."""
|
"""Return metadata for every known Jupyter shell."""
|
||||||
if shell_name not in self.shells:
|
return self._shell_registry.list_shells()
|
||||||
raise KeyError(f"Shell {shell_name} does not exist")
|
|
||||||
return self.shells[shell_name]
|
def create_shell(self) -> ShellInfo:
|
||||||
|
"""Create a fresh Jupyter shell and return its metadata."""
|
||||||
|
return self._shell_registry.create_shell()
|
||||||
|
|
||||||
|
def get_shell(self, shell_name: str) -> ShellInfo:
|
||||||
|
"""Return frontend-safe metadata for one Jupyter shell."""
|
||||||
|
return self._shell_registry.get_shell(shell_name)
|
||||||
|
|
||||||
|
def get_last_shell(self) -> ShellInfo:
|
||||||
|
"""Return metadata for the most recently selected Jupyter shell."""
|
||||||
|
return self._shell_registry.get_last_shell()
|
||||||
|
|
||||||
|
def info(self) -> AppInfo:
|
||||||
|
"""Return runtime metadata without starting any kernels."""
|
||||||
|
return AppInfo(
|
||||||
|
app_id=self.app_id,
|
||||||
|
created_at=self.created_at,
|
||||||
|
uptime_seconds=monotonic() - self._started_at,
|
||||||
|
shell_count=len(self.shells),
|
||||||
|
last_shell=self.last_shell,
|
||||||
|
total_calls=self._total_calls,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _new_call_id(self) -> str:
|
||||||
|
"""Generate a readable call ID that is unique for this app."""
|
||||||
|
call_id = generate_call_id()
|
||||||
|
while call_id in self._issued_call_ids:
|
||||||
|
call_id = generate_call_id()
|
||||||
|
self._issued_call_ids.add(call_id)
|
||||||
|
return call_id
|
||||||
|
|
||||||
async def run_code_stream(
|
async def run_code_stream(
|
||||||
self,
|
self,
|
||||||
@@ -48,7 +77,8 @@ class JupyterApp:
|
|||||||
) -> AsyncIterator[KernelMessage | InputRequest]:
|
) -> AsyncIterator[KernelMessage | InputRequest]:
|
||||||
"""Execute code and yield its subprocess messages."""
|
"""Execute code and yield its subprocess messages."""
|
||||||
_, shell = self._select_shell(shell_name)
|
_, shell = self._select_shell(shell_name)
|
||||||
call_id = uuid4().hex
|
call_id = self._new_call_id()
|
||||||
|
self._total_calls += 1
|
||||||
self._calls[call_id] = shell
|
self._calls[call_id] = shell
|
||||||
try:
|
try:
|
||||||
async for message in shell.run_cell_stream(code, call_id=call_id):
|
async for message in shell.run_cell_stream(code, call_id=call_id):
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from ..models import ShellInfo, ShellStatus
|
||||||
from .transport import InputRequest, JupyterTransport, KernelMessage
|
from .transport import InputRequest, JupyterTransport, KernelMessage
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +12,11 @@ class JupyterShell:
|
|||||||
|
|
||||||
def __init__(self, name: str, transport: JupyterTransport | None = None) -> None:
|
def __init__(self, name: str, transport: JupyterTransport | None = None) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.shell_id = uuid4().hex
|
||||||
|
self.created_at = datetime.now(UTC).isoformat()
|
||||||
|
self.last_used_at: str | None = None
|
||||||
|
self.status: ShellStatus = "ready"
|
||||||
|
self._last_execution_count = 0
|
||||||
self.transport = transport or JupyterTransport()
|
self.transport = transport or JupyterTransport()
|
||||||
self._active_call_id: str | None = None
|
self._active_call_id: str | None = None
|
||||||
|
|
||||||
@@ -26,6 +34,7 @@ class JupyterShell:
|
|||||||
"""Yield kernel messages while one cell executes."""
|
"""Yield kernel messages while one cell executes."""
|
||||||
await self.start()
|
await self.start()
|
||||||
self._active_call_id = call_id
|
self._active_call_id = call_id
|
||||||
|
self.status = "running"
|
||||||
try:
|
try:
|
||||||
kernel_call_id = await self.transport.execute(code)
|
kernel_call_id = await self.transport.execute(code)
|
||||||
async for message in self.transport.messages_for(kernel_call_id):
|
async for message in self.transport.messages_for(kernel_call_id):
|
||||||
@@ -33,10 +42,25 @@ class JupyterShell:
|
|||||||
yield replace(message, call_id=call_id)
|
yield replace(message, call_id=call_id)
|
||||||
else:
|
else:
|
||||||
# Keep the kernel's execution ID private to the transport.
|
# Keep the kernel's execution ID private to the transport.
|
||||||
yield replace(message, parent_id=call_id)
|
public_message = replace(message, parent_id=call_id)
|
||||||
|
if public_message.msg_type == "execute_reply":
|
||||||
|
self._last_execution_count = int(
|
||||||
|
public_message.content.get(
|
||||||
|
"execution_count", self._last_execution_count
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.status = (
|
||||||
|
"error"
|
||||||
|
if public_message.content.get("status") == "error"
|
||||||
|
else "ready"
|
||||||
|
)
|
||||||
|
self.last_used_at = datetime.now(UTC).isoformat()
|
||||||
|
yield public_message
|
||||||
finally:
|
finally:
|
||||||
if self._active_call_id == call_id:
|
if self._active_call_id == call_id:
|
||||||
self._active_call_id = None
|
self._active_call_id = None
|
||||||
|
if self.status == "running" and self._active_call_id is None:
|
||||||
|
self.status = "ready"
|
||||||
|
|
||||||
async def reply_to_input(self, call_id: str, value: str) -> None:
|
async def reply_to_input(self, call_id: str, value: str) -> None:
|
||||||
"""Send input to the cell currently waiting in this shell."""
|
"""Send input to the cell currently waiting in this shell."""
|
||||||
@@ -47,3 +71,23 @@ class JupyterShell:
|
|||||||
async def shutdown(self) -> None:
|
async def shutdown(self) -> None:
|
||||||
"""Stop this shell's kernel process."""
|
"""Stop this shell's kernel process."""
|
||||||
await self.transport.shutdown()
|
await self.transport.shutdown()
|
||||||
|
|
||||||
|
def describe(self) -> ShellInfo:
|
||||||
|
"""Return metadata without starting an unused kernel."""
|
||||||
|
return ShellInfo(
|
||||||
|
shell_id=self.shell_id,
|
||||||
|
name=self.name,
|
||||||
|
status=self.status,
|
||||||
|
execution_count=self._last_execution_count,
|
||||||
|
created_at=self.created_at,
|
||||||
|
last_used_at=self.last_used_at,
|
||||||
|
capabilities=(
|
||||||
|
"stdout",
|
||||||
|
"stderr",
|
||||||
|
"display_data",
|
||||||
|
"execute_result",
|
||||||
|
"error",
|
||||||
|
"matplotlib-inline",
|
||||||
|
"input",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Protocol
|
||||||
|
|
||||||
|
from .models import ShellInfo
|
||||||
|
from .utils import generate_good_names
|
||||||
|
|
||||||
|
|
||||||
|
class DescribableShell(Protocol):
|
||||||
|
"""The small shell interface needed by the shared registry."""
|
||||||
|
|
||||||
|
def describe(self) -> ShellInfo: ...
|
||||||
|
|
||||||
|
|
||||||
|
class ShellRegistry[ShellT: DescribableShell]:
|
||||||
|
"""Own named shells and resolve ``new``, ``last``, and explicit names."""
|
||||||
|
|
||||||
|
def __init__(self, factory: Callable[[str], ShellT]) -> None:
|
||||||
|
self._factory = factory
|
||||||
|
self.shells: dict[str, ShellT] = {}
|
||||||
|
self.last_shell: str | None = None
|
||||||
|
|
||||||
|
def new_shell(self) -> tuple[str, ShellT]:
|
||||||
|
"""Create a fresh generated shell and select it."""
|
||||||
|
name = generate_good_names()
|
||||||
|
while name in self.shells:
|
||||||
|
name = generate_good_names()
|
||||||
|
shell = self._factory(name)
|
||||||
|
self.shells[name] = shell
|
||||||
|
self.last_shell = name
|
||||||
|
return name, shell
|
||||||
|
|
||||||
|
def select(self, shell_name: str) -> tuple[str, ShellT]:
|
||||||
|
"""Resolve a selector, creating explicit names when necessary."""
|
||||||
|
if shell_name == "new":
|
||||||
|
return self.new_shell()
|
||||||
|
if shell_name == "last":
|
||||||
|
if self.last_shell is None:
|
||||||
|
return self.new_shell()
|
||||||
|
return self.last_shell, self.shells[self.last_shell]
|
||||||
|
if shell_name not in self.shells:
|
||||||
|
self.shells[shell_name] = self._factory(shell_name)
|
||||||
|
self.last_shell = shell_name
|
||||||
|
return shell_name, self.shells[shell_name]
|
||||||
|
|
||||||
|
def list_shells(self) -> list[ShellInfo]:
|
||||||
|
"""Return metadata for every known shell."""
|
||||||
|
return [shell.describe() for shell in self.shells.values()]
|
||||||
|
|
||||||
|
def create_shell(self) -> ShellInfo:
|
||||||
|
"""Create a fresh generated shell and return its metadata."""
|
||||||
|
_, shell = self.new_shell()
|
||||||
|
return shell.describe()
|
||||||
|
|
||||||
|
def get_shell(self, shell_name: str) -> ShellInfo:
|
||||||
|
"""Return metadata for one existing shell."""
|
||||||
|
try:
|
||||||
|
shell = self.shells[shell_name]
|
||||||
|
except KeyError as error:
|
||||||
|
raise KeyError(f"Shell {shell_name} does not exist") from error
|
||||||
|
return shell.describe()
|
||||||
|
|
||||||
|
def get_last_shell(self) -> ShellInfo:
|
||||||
|
"""Return metadata for the most recently selected shell."""
|
||||||
|
if self.last_shell is None:
|
||||||
|
raise KeyError("No shell exists yet")
|
||||||
|
return self.get_shell(self.last_shell)
|
||||||
@@ -29,17 +29,42 @@ def generate_good_names() -> str:
|
|||||||
|
|
||||||
type WordCategory = Literal["adjectives", "nouns", "verbs"]
|
type WordCategory = Literal["adjectives", "nouns", "verbs"]
|
||||||
type WordCategory1 = Literal["adjective", "noun", "verb"]
|
type WordCategory1 = Literal["adjective", "noun", "verb"]
|
||||||
|
type WordCategoryShort = Literal["adj", "n", "v"]
|
||||||
|
type NamePart = WordCategory | WordCategory1 | WordCategoryShort | int
|
||||||
|
|
||||||
|
_CATEGORY_NAMES: dict[
|
||||||
|
WordCategory | WordCategory1 | WordCategoryShort, WordCategory
|
||||||
|
] = {
|
||||||
|
"adjectives": "adjectives",
|
||||||
|
"adjective": "adjectives",
|
||||||
|
"adj": "adjectives",
|
||||||
|
"nouns": "nouns",
|
||||||
|
"noun": "nouns",
|
||||||
|
"n": "nouns",
|
||||||
|
"verbs": "verbs",
|
||||||
|
"verb": "verbs",
|
||||||
|
"v": "verbs",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def generate_omfg_names(uhh: Sequence[WordCategory | WordCategory1 | int]) -> str:
|
def generate_omfg_names(recipe: Sequence[NamePart]) -> str:
|
||||||
"""Generate a readable, collision-resistant shell name.
|
"""Generate a readable, collision-resistant shell name.
|
||||||
|
|
||||||
Example: generate_omfg_names(["adjectives", "nouns", 4]) -> "happy-dog-1a2b"
|
Example: generate_omfg_names(["adj", "n", "v", 4]) -> "happy-dog-run-1a2b"
|
||||||
"""
|
"""
|
||||||
words: list[str] = []
|
words: list[str] = []
|
||||||
for category in uhh:
|
for category in recipe:
|
||||||
if isinstance(category, int):
|
if isinstance(category, int):
|
||||||
words.append(secrets.token_hex((category + 1) // 2)[:category]) # what?
|
if category < 1:
|
||||||
|
raise ValueError("Hex suffix length must be positive")
|
||||||
|
words.append(secrets.token_hex((category + 1) // 2)[:category])
|
||||||
else:
|
else:
|
||||||
words.append(_WORD_GENERATOR.word(include_categories=[category]))
|
words.append(
|
||||||
|
_WORD_GENERATOR.word(include_categories=[_CATEGORY_NAMES[category]])
|
||||||
|
)
|
||||||
return "-".join(words)
|
return "-".join(words)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_call_id() -> str:
|
||||||
|
"""Generate a readable application-level identifier for one call."""
|
||||||
|
return generate_omfg_names(["adj", "n", "v", 4])
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ class AppTests(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_call_ids_are_readable(self):
|
||||||
|
app = App()
|
||||||
|
|
||||||
|
with contextlib.redirect_stdout(io.StringIO()):
|
||||||
|
call = app.run_code("1 + 1", shell_name="new")
|
||||||
|
|
||||||
|
self.assertRegex(call.call_id, r"^[a-z]+-[a-z]+-[a-z]+-[0-9a-f]{4}$")
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from ipython_shell.jupyter.app import JupyterApp
|
from ipython_shell.jupyter.app import JupyterApp
|
||||||
@@ -48,6 +49,25 @@ class JupyterShellTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
|
|
||||||
class JupyterAppTests(unittest.IsolatedAsyncioTestCase):
|
class JupyterAppTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def test_app_exposes_shared_shell_metadata_api(self):
|
||||||
|
app = JupyterApp()
|
||||||
|
try:
|
||||||
|
created = app.create_shell()
|
||||||
|
listed = app.list_shells()
|
||||||
|
last = app.get_last_shell()
|
||||||
|
selected = app.get_shell(created.name)
|
||||||
|
info = app.info()
|
||||||
|
finally:
|
||||||
|
await app.shutdown()
|
||||||
|
|
||||||
|
self.assertEqual(len(listed), 1)
|
||||||
|
self.assertEqual(created.shell_id, listed[0].shell_id)
|
||||||
|
self.assertEqual(created.shell_id, last.shell_id)
|
||||||
|
self.assertEqual(created.shell_id, selected.shell_id)
|
||||||
|
self.assertEqual(created.execution_count, 0)
|
||||||
|
self.assertEqual(info.shell_count, 1)
|
||||||
|
self.assertEqual(info.last_shell, created.name)
|
||||||
|
|
||||||
async def test_named_shells_have_isolated_namespaces(self):
|
async def test_named_shells_have_isolated_namespaces(self):
|
||||||
app = JupyterApp()
|
app = JupyterApp()
|
||||||
try:
|
try:
|
||||||
@@ -110,7 +130,10 @@ class JupyterAppTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
self.assertIsInstance(input_request, InputRequest)
|
self.assertIsInstance(input_request, InputRequest)
|
||||||
assert isinstance(input_request, InputRequest)
|
assert isinstance(input_request, InputRequest)
|
||||||
self.assertTrue(input_request.call_id)
|
self.assertRegex(
|
||||||
|
input_request.call_id,
|
||||||
|
re.compile(r"^[a-z]+-[a-z]+-[a-z]+-[0-9a-f]{4}$"),
|
||||||
|
)
|
||||||
self.assertEqual(input_request.prompt, "name? ")
|
self.assertEqual(input_request.prompt, "name? ")
|
||||||
|
|
||||||
await app.reply_to_input(input_request.call_id, "Ada")
|
await app.reply_to_input(input_request.call_id, "Ada")
|
||||||
|
|||||||
Reference in New Issue
Block a user