codebind 0.6.7__tar.gz → 0.6.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codebind-0.6.7 → codebind-0.6.9}/PKG-INFO +2 -2
- {codebind-0.6.7 → codebind-0.6.9}/README.md +1 -1
- {codebind-0.6.7 → codebind-0.6.9}/pyproject.toml +1 -1
- {codebind-0.6.7 → codebind-0.6.9}/pyproject.toml.orig +1 -1
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/execution.py +9 -4
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/extension.py +37 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/session.py +35 -9
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/terminal.py +161 -8
- {codebind-0.6.7 → codebind-0.6.9}/LICENSE +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/install.json +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/package.json +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/static/590.dae4561c5427a855.js +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/static/remoteEntry.c10098fb6e87897c.js +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/static/style.js +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/static/third-party-licenses.json +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/jupyterlab/.yarnrc.yml +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/jupyterlab/install.json +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/jupyterlab/package.json +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/jupyterlab/src/index.ts +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/jupyterlab/tsconfig.json +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/jupyterlab/yarn.lock +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/__init__.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/cli.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/configuration.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/conversation.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/display.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/images.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/instructions.md +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/jupyter.py +0 -0
- {codebind-0.6.7 → codebind-0.6.9}/src/codebind/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebind
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.9
|
|
4
4
|
Summary: A frontend-neutral model loop for persistent IPython sessions.
|
|
5
5
|
Keywords: ai,ipython,llm,repl,agents
|
|
6
6
|
Author: Giovanni Gravili
|
|
@@ -75,7 +75,7 @@ Inspect this project and tell me what to implement first.
|
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
Terminal conversations remain live for the current IPython process. Previous terminal inputs and outputs, including Python cells you ran yourself, enter the model's context when you ask a question. Durable automatic resumption belongs to notebooks, where the notebook file provides an unambiguous conversation identity.
|
|
78
|
-
Model-executed cells appear with IPython's native input counter, syntax highlighting, output prompts, streams, and tracebacks. Assistant Markdown is rendered through IPython's terminal MIME renderer. A terminal is a stream rather than a notebook document, so these displays are not editable notebook cells.
|
|
78
|
+
Model-executed cells appear with IPython's native input counter, syntax highlighting, output prompts, streams, and tracebacks. Assistant Markdown appears before the following tool cell, and tool output appears as it is written. To keep terminal scrollback usable, Codebind shows at most 10 gray lines of each model-executed cell's output, then displays a notice such as `… 990 more output lines. Run %output 8 to view all.` The complete output remains available to the model and in IPython history; `%output 8` opens its full text in IPython's pager. User-entered Python cells keep IPython's normal display behavior. Assistant Markdown is rendered through IPython's terminal MIME renderer. A terminal is a stream rather than a notebook document, so these displays are not editable notebook cells.
|
|
79
79
|
|
|
80
80
|
## JupyterLab
|
|
81
81
|
|
|
@@ -47,7 +47,7 @@ Inspect this project and tell me what to implement first.
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Terminal conversations remain live for the current IPython process. Previous terminal inputs and outputs, including Python cells you ran yourself, enter the model's context when you ask a question. Durable automatic resumption belongs to notebooks, where the notebook file provides an unambiguous conversation identity.
|
|
50
|
-
Model-executed cells appear with IPython's native input counter, syntax highlighting, output prompts, streams, and tracebacks. Assistant Markdown is rendered through IPython's terminal MIME renderer. A terminal is a stream rather than a notebook document, so these displays are not editable notebook cells.
|
|
50
|
+
Model-executed cells appear with IPython's native input counter, syntax highlighting, output prompts, streams, and tracebacks. Assistant Markdown appears before the following tool cell, and tool output appears as it is written. To keep terminal scrollback usable, Codebind shows at most 10 gray lines of each model-executed cell's output, then displays a notice such as `… 990 more output lines. Run %output 8 to view all.` The complete output remains available to the model and in IPython history; `%output 8` opens its full text in IPython's pager. User-entered Python cells keep IPython's normal display behavior. Assistant Markdown is rendered through IPython's terminal MIME renderer. A terminal is a stream rather than a notebook document, so these displays are not editable notebook cells.
|
|
51
51
|
|
|
52
52
|
## JupyterLab
|
|
53
53
|
|
|
@@ -16,7 +16,7 @@ from IPython.utils.capture import capture_output
|
|
|
16
16
|
from .display import display_cell
|
|
17
17
|
from .images import PreparedImage, prepare_mime_image
|
|
18
18
|
from .jupyter import JupyterLabBridge
|
|
19
|
-
from .terminal import record_tool_output, show_input
|
|
19
|
+
from .terminal import preview_tool_output, record_tool_output, show_input
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
_MAXIMUM_IMAGES_PER_REPORT = 32
|
|
@@ -219,9 +219,11 @@ class IPythonExecutor:
|
|
|
219
219
|
|
|
220
220
|
if self.bridge is None and isinstance(self.shell, TerminalInteractiveShell):
|
|
221
221
|
show_input(self.shell, cell)
|
|
222
|
-
with record_tool_output(self.shell):
|
|
222
|
+
with preview_tool_output() as preview, record_tool_output(self.shell):
|
|
223
223
|
result = self.shell.run_cell(cell, store_history=True)
|
|
224
|
-
|
|
224
|
+
report = self._native_report(result)
|
|
225
|
+
preview.show_notice(result.execution_count, report.error)
|
|
226
|
+
return report
|
|
225
227
|
|
|
226
228
|
bridge = self.bridge
|
|
227
229
|
cell_id = bridge.start_code_cell(cell) if bridge is not None else None
|
|
@@ -260,6 +262,7 @@ class IPythonExecutor:
|
|
|
260
262
|
transformed = self.shell.transform_cell(cell)
|
|
261
263
|
result = None
|
|
262
264
|
with (
|
|
265
|
+
preview_tool_output() as preview,
|
|
263
266
|
record_tool_output(self.shell),
|
|
264
267
|
self.shell._tee(channel="stdout"),
|
|
265
268
|
self.shell._tee(channel="stderr"),
|
|
@@ -273,7 +276,9 @@ class IPythonExecutor:
|
|
|
273
276
|
finally:
|
|
274
277
|
self.shell.events.trigger("post_execute")
|
|
275
278
|
self.shell.events.trigger("post_run_cell", result)
|
|
276
|
-
|
|
279
|
+
report = self._native_report(result)
|
|
280
|
+
preview.show_notice(result.execution_count, report.error)
|
|
281
|
+
return report
|
|
277
282
|
|
|
278
283
|
bridge = self.bridge
|
|
279
284
|
cell_id = bridge.start_code_cell(cell) if bridge is not None else None
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import sys
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
8
9
|
from IPython.core.interactiveshell import InteractiveShell
|
|
10
|
+
from IPython.core.page import page
|
|
9
11
|
from IPython.terminal.interactiveshell import TerminalInteractiveShell
|
|
10
12
|
from langchain_core.language_models import BaseChatModel
|
|
11
13
|
|
|
@@ -20,6 +22,9 @@ _STATE_ATTRIBUTE = "_codebind_extension_state"
|
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
def _close_state(state: dict[str, Any]) -> None:
|
|
25
|
+
restore_output_magic = state.get("restore_output_magic")
|
|
26
|
+
if callable(restore_output_magic):
|
|
27
|
+
restore_output_magic()
|
|
23
28
|
terminal_history = state.get("terminal_history")
|
|
24
29
|
if isinstance(terminal_history, TerminalCellHistory):
|
|
25
30
|
terminal_history.close()
|
|
@@ -89,6 +94,7 @@ def load_ipython_extension(ipython: InteractiveShell) -> None:
|
|
|
89
94
|
"restore_terminal_display": None,
|
|
90
95
|
"restore_terminal_question_mode": None,
|
|
91
96
|
"terminal_history": None,
|
|
97
|
+
"restore_output_magic": None,
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
if manager is None:
|
|
@@ -104,6 +110,37 @@ def load_ipython_extension(ipython: InteractiveShell) -> None:
|
|
|
104
110
|
)
|
|
105
111
|
state["restore_terminal_display"] = install_markdown_renderer(ipython)
|
|
106
112
|
state["restore_terminal_question_mode"] = install_question_mode(ipython)
|
|
113
|
+
|
|
114
|
+
line_magics = ipython.magics_manager.magics["line"]
|
|
115
|
+
previous_output_magic = line_magics.get("output")
|
|
116
|
+
|
|
117
|
+
def output_magic(line: str) -> None:
|
|
118
|
+
number_text = line.strip()
|
|
119
|
+
if not number_text.isdecimal() or int(number_text) < 1:
|
|
120
|
+
raise ValueError("Usage: %output <cell number>")
|
|
121
|
+
terminal_history = state["terminal_history"]
|
|
122
|
+
if not isinstance(terminal_history, TerminalCellHistory):
|
|
123
|
+
raise RuntimeError("Terminal history is unavailable")
|
|
124
|
+
number = int(number_text)
|
|
125
|
+
try:
|
|
126
|
+
full_output = terminal_history.output_text(number)
|
|
127
|
+
except KeyError as error:
|
|
128
|
+
raise ValueError(f"No recorded cell In [{number}]") from error
|
|
129
|
+
if full_output:
|
|
130
|
+
page(f"\x1b[90m{full_output}\x1b[0m" if sys.stdout.isatty() else full_output)
|
|
131
|
+
else:
|
|
132
|
+
print(f"In [{number}] has no text output.")
|
|
133
|
+
|
|
134
|
+
ipython.register_magic_function(output_magic, "line", "output")
|
|
135
|
+
|
|
136
|
+
def restore_output_magic() -> None:
|
|
137
|
+
if line_magics.get("output") is output_magic:
|
|
138
|
+
if previous_output_magic is None:
|
|
139
|
+
line_magics.pop("output", None)
|
|
140
|
+
else:
|
|
141
|
+
line_magics["output"] = previous_output_magic
|
|
142
|
+
|
|
143
|
+
state["restore_output_magic"] = restore_output_magic
|
|
107
144
|
else:
|
|
108
145
|
|
|
109
146
|
def accept_comm(comm: Any, message: dict[str, Any]) -> None:
|
|
@@ -274,8 +274,24 @@ class Session:
|
|
|
274
274
|
for identifier in self._pending_calls(turn_id):
|
|
275
275
|
self._append_message_sync(self._interrupted_tool_message(identifier), turn_id)
|
|
276
276
|
|
|
277
|
-
def _update_assistant_stream(
|
|
278
|
-
|
|
277
|
+
def _update_assistant_stream(
|
|
278
|
+
self,
|
|
279
|
+
cell_id: str | None,
|
|
280
|
+
text: str,
|
|
281
|
+
shown: str,
|
|
282
|
+
terminal_displayed: list[str] | None = None,
|
|
283
|
+
) -> str | None:
|
|
284
|
+
if not text:
|
|
285
|
+
return cell_id
|
|
286
|
+
if self.bridge is None:
|
|
287
|
+
previous = terminal_displayed[0] if terminal_displayed is not None else shown
|
|
288
|
+
if previous.startswith(text):
|
|
289
|
+
return cell_id
|
|
290
|
+
addition = text[len(previous) :] if text.startswith(previous) else text
|
|
291
|
+
if addition:
|
|
292
|
+
self._display_assistant(addition)
|
|
293
|
+
if terminal_displayed is not None:
|
|
294
|
+
terminal_displayed[0] = text
|
|
279
295
|
return cell_id
|
|
280
296
|
if cell_id is None:
|
|
281
297
|
return self.bridge.start_markdown_cell(text)
|
|
@@ -284,7 +300,9 @@ class Session:
|
|
|
284
300
|
|
|
285
301
|
def _finish_assistant_stream(self, cell_id: str | None, response: AIMessage) -> None:
|
|
286
302
|
answer = _message_text(response)
|
|
287
|
-
if
|
|
303
|
+
if self.bridge is None:
|
|
304
|
+
return
|
|
305
|
+
if cell_id is not None:
|
|
288
306
|
self.bridge.finish_markdown_cell(cell_id, answer)
|
|
289
307
|
elif answer:
|
|
290
308
|
self._display_assistant(answer)
|
|
@@ -302,9 +320,10 @@ class Session:
|
|
|
302
320
|
self,
|
|
303
321
|
model: BaseChatModel,
|
|
304
322
|
) -> tuple[AIMessage, str | None]:
|
|
323
|
+
terminal_displayed = [""]
|
|
305
324
|
for retry in range(len(_MODEL_RETRY_DELAYS) + 1):
|
|
306
325
|
try:
|
|
307
|
-
return self._stream_model(self._bind(model), model)
|
|
326
|
+
return self._stream_model(self._bind(model), model, terminal_displayed)
|
|
308
327
|
except TransientProviderError as error:
|
|
309
328
|
if retry == len(_MODEL_RETRY_DELAYS):
|
|
310
329
|
raise
|
|
@@ -316,9 +335,10 @@ class Session:
|
|
|
316
335
|
self,
|
|
317
336
|
model: BaseChatModel,
|
|
318
337
|
) -> tuple[AIMessage, str | None]:
|
|
338
|
+
terminal_displayed = [""]
|
|
319
339
|
for retry in range(len(_MODEL_RETRY_DELAYS) + 1):
|
|
320
340
|
try:
|
|
321
|
-
return await self._astream_model(self._bind(model), model)
|
|
341
|
+
return await self._astream_model(self._bind(model), model, terminal_displayed)
|
|
322
342
|
except TransientProviderError as error:
|
|
323
343
|
if retry == len(_MODEL_RETRY_DELAYS):
|
|
324
344
|
raise
|
|
@@ -344,6 +364,7 @@ class Session:
|
|
|
344
364
|
self,
|
|
345
365
|
bound_model: Runnable[Any, BaseMessage],
|
|
346
366
|
model: BaseChatModel,
|
|
367
|
+
terminal_displayed: list[str] | None = None,
|
|
347
368
|
) -> tuple[AIMessage, str | None]:
|
|
348
369
|
aggregate: AIMessageChunk | None = None
|
|
349
370
|
response: AIMessage | None = None
|
|
@@ -366,12 +387,14 @@ class Session:
|
|
|
366
387
|
raise TypeError("model stream must return AIMessage chunks")
|
|
367
388
|
text = _message_text(current)
|
|
368
389
|
if tool_call_started and text != shown:
|
|
369
|
-
cell_id = self._update_assistant_stream(
|
|
390
|
+
cell_id = self._update_assistant_stream(
|
|
391
|
+
cell_id, text, shown, terminal_displayed
|
|
392
|
+
)
|
|
370
393
|
shown = text
|
|
371
394
|
completed = self._completed_stream_message(aggregate, response)
|
|
372
395
|
answer = _message_text(completed)
|
|
373
396
|
if answer != shown:
|
|
374
|
-
cell_id = self._update_assistant_stream(cell_id, answer)
|
|
397
|
+
cell_id = self._update_assistant_stream(cell_id, answer, shown, terminal_displayed)
|
|
375
398
|
return completed, cell_id
|
|
376
399
|
except BaseException:
|
|
377
400
|
self._cancel_assistant_stream(cell_id)
|
|
@@ -381,6 +404,7 @@ class Session:
|
|
|
381
404
|
self,
|
|
382
405
|
bound_model: Runnable[Any, BaseMessage],
|
|
383
406
|
model: BaseChatModel,
|
|
407
|
+
terminal_displayed: list[str] | None = None,
|
|
384
408
|
) -> tuple[AIMessage, str | None]:
|
|
385
409
|
aggregate: AIMessageChunk | None = None
|
|
386
410
|
response: AIMessage | None = None
|
|
@@ -403,12 +427,14 @@ class Session:
|
|
|
403
427
|
raise TypeError("model stream must return AIMessage chunks")
|
|
404
428
|
text = _message_text(current)
|
|
405
429
|
if tool_call_started and text != shown:
|
|
406
|
-
cell_id = self._update_assistant_stream(
|
|
430
|
+
cell_id = self._update_assistant_stream(
|
|
431
|
+
cell_id, text, shown, terminal_displayed
|
|
432
|
+
)
|
|
407
433
|
shown = text
|
|
408
434
|
completed = self._completed_stream_message(aggregate, response)
|
|
409
435
|
answer = _message_text(completed)
|
|
410
436
|
if answer != shown:
|
|
411
|
-
cell_id = self._update_assistant_stream(cell_id, answer)
|
|
437
|
+
cell_id = self._update_assistant_stream(cell_id, answer, shown, terminal_displayed)
|
|
412
438
|
return completed, cell_id
|
|
413
439
|
except BaseException:
|
|
414
440
|
self._cancel_assistant_stream(cell_id)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
|
+
import shutil
|
|
6
7
|
import sys
|
|
7
8
|
from collections.abc import Callable, Iterator
|
|
8
9
|
from contextlib import contextmanager
|
|
@@ -13,7 +14,7 @@ from IPython.terminal.interactiveshell import IPythonPTLexer, TerminalInteractiv
|
|
|
13
14
|
from prompt_toolkit.enums import DEFAULT_BUFFER
|
|
14
15
|
from prompt_toolkit.filters import Condition, has_focus
|
|
15
16
|
from prompt_toolkit.document import Document
|
|
16
|
-
from prompt_toolkit.formatted_text import FormattedText, PygmentsTokens
|
|
17
|
+
from prompt_toolkit.formatted_text import ANSI, FormattedText, PygmentsTokens, to_formatted_text
|
|
17
18
|
from prompt_toolkit.shortcuts import print_formatted_text
|
|
18
19
|
from prompt_toolkit.styles import DynamicStyle, Style, merge_styles
|
|
19
20
|
from rich.console import Console
|
|
@@ -22,10 +23,29 @@ from rich.markdown import Markdown
|
|
|
22
23
|
|
|
23
24
|
_MISSING = object()
|
|
24
25
|
_IMAGE_FORMATS = ("image/png", "image/jpeg", "image/svg+xml")
|
|
26
|
+
_MAXIMUM_VISIBLE_OUTPUT_LINES = 10
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def
|
|
28
|
-
""
|
|
29
|
+
def _plain_ansi(value: str) -> str:
|
|
30
|
+
return "".join(text for _, text in to_formatted_text(ANSI(value)))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _split_incomplete_ansi(value: str) -> tuple[str, str]:
|
|
34
|
+
last_escape = value.rfind("\x1b")
|
|
35
|
+
if last_escape < 0:
|
|
36
|
+
return value, ""
|
|
37
|
+
tail = value[last_escape:]
|
|
38
|
+
if tail == "\x1b":
|
|
39
|
+
return value[:last_escape], tail
|
|
40
|
+
if tail.startswith("\x1b[") and not any("@" <= char <= "~" for char in tail[2:]):
|
|
41
|
+
return value[:last_escape], tail
|
|
42
|
+
if tail.startswith("\x1b]") and "\x07" not in tail and "\x1b\\" not in tail:
|
|
43
|
+
return value[:last_escape], tail
|
|
44
|
+
return value, ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _magic_input(transformed: str) -> tuple[str, str] | None:
|
|
48
|
+
"""Recognize a complete IPython magic call in transformed history."""
|
|
29
49
|
try:
|
|
30
50
|
statements = ast.parse(transformed).body
|
|
31
51
|
except SyntaxError:
|
|
@@ -42,14 +62,14 @@ def _question_text(transformed: str) -> str | None:
|
|
|
42
62
|
and call.func.value.func.id == "get_ipython"
|
|
43
63
|
and bool(call.args)
|
|
44
64
|
and isinstance(call.args[0], ast.Constant)
|
|
45
|
-
and call.args[0].value
|
|
65
|
+
and isinstance(call.args[0].value, str)
|
|
46
66
|
):
|
|
47
67
|
return None
|
|
48
68
|
text_index = 2 if call.func.attr == "run_cell_magic" else 1
|
|
49
69
|
if len(call.args) <= text_index or not isinstance(call.args[text_index], ast.Constant):
|
|
50
70
|
return None
|
|
51
71
|
text = call.args[text_index].value
|
|
52
|
-
return text.strip() if isinstance(text, str) else None
|
|
72
|
+
return (call.args[0].value, text.strip()) if isinstance(text, str) else None
|
|
53
73
|
|
|
54
74
|
|
|
55
75
|
def _history_cell(
|
|
@@ -63,9 +83,11 @@ def _history_cell(
|
|
|
63
83
|
return None
|
|
64
84
|
history = shell.history_manager
|
|
65
85
|
identifier = f"terminal-{number}"
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
return {"id": identifier, "type": "markdown", "source":
|
|
86
|
+
magic = _magic_input(transformed)
|
|
87
|
+
if magic is not None and magic[0] == "question":
|
|
88
|
+
return {"id": identifier, "type": "markdown", "source": magic[1]}
|
|
89
|
+
if magic is not None and magic[0] == "output":
|
|
90
|
+
return {"id": identifier, "type": "raw", "source": source}
|
|
69
91
|
|
|
70
92
|
outputs: list[dict[str, object]] = []
|
|
71
93
|
for record in history.outputs.get(number, ()):
|
|
@@ -139,10 +161,138 @@ class TerminalCellHistory:
|
|
|
139
161
|
def snapshot(self) -> list[dict[str, object]]:
|
|
140
162
|
return [deepcopy(self._cells[number]) for number in sorted(self._cells)]
|
|
141
163
|
|
|
164
|
+
def output_text(self, number: int) -> str:
|
|
165
|
+
"""Format one completed cell's recorded output for IPython's pager."""
|
|
166
|
+
cell = self._cells.get(number)
|
|
167
|
+
if cell is None:
|
|
168
|
+
raise KeyError(number)
|
|
169
|
+
parts: list[str] = []
|
|
170
|
+
for output in cell.get("outputs", []):
|
|
171
|
+
kind = output["output_type"]
|
|
172
|
+
if kind == "stream":
|
|
173
|
+
parts.append(output["text"])
|
|
174
|
+
elif kind in {"execute_result", "display_data"}:
|
|
175
|
+
data = output["data"]
|
|
176
|
+
text = data.get("text/plain", data.get("text/markdown"))
|
|
177
|
+
if text is None:
|
|
178
|
+
continue
|
|
179
|
+
prefix = f"Out[{number}]: " if kind == "execute_result" else ""
|
|
180
|
+
parts.append(prefix + str(text) + "\n")
|
|
181
|
+
elif kind == "error":
|
|
182
|
+
traceback = output.get("traceback") or [
|
|
183
|
+
f"{output.get('ename', 'Error')}: {output.get('evalue', '')}"
|
|
184
|
+
]
|
|
185
|
+
parts.append("\n".join(traceback) + "\n")
|
|
186
|
+
return _plain_ansi("".join(parts))
|
|
187
|
+
|
|
142
188
|
def close(self) -> None:
|
|
143
189
|
self.shell.events.unregister("post_run_cell", self._callback)
|
|
144
190
|
|
|
145
191
|
|
|
192
|
+
class _OutputPreview:
|
|
193
|
+
"""Limit only terminal painting; IPython still records every written byte."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, maximum_lines: int, maximum_characters: int) -> None:
|
|
196
|
+
self.maximum_lines = maximum_lines
|
|
197
|
+
self.maximum_characters = maximum_characters
|
|
198
|
+
self.visible_lines = 0
|
|
199
|
+
self.visible_characters = 0
|
|
200
|
+
self.hidden_line_breaks = 0
|
|
201
|
+
self.hidden_has_tail = False
|
|
202
|
+
self.hidden_characters = 0
|
|
203
|
+
self.truncated = False
|
|
204
|
+
self.visible_ends_line = True
|
|
205
|
+
|
|
206
|
+
def _hide(self, data: str) -> None:
|
|
207
|
+
self.truncated = True
|
|
208
|
+
self.hidden_characters += len(data)
|
|
209
|
+
self.hidden_line_breaks += data.count("\n") + data.count("\r") - data.count("\r\n")
|
|
210
|
+
self.hidden_has_tail = not data.endswith(("\n", "\r"))
|
|
211
|
+
|
|
212
|
+
def write(self, stream, data: str) -> int:
|
|
213
|
+
for piece in data.splitlines(keepends=True):
|
|
214
|
+
remaining = self.maximum_characters - self.visible_characters
|
|
215
|
+
if self.truncated or self.visible_lines >= self.maximum_lines or remaining <= 0:
|
|
216
|
+
self._hide(piece)
|
|
217
|
+
continue
|
|
218
|
+
visible = piece[:remaining]
|
|
219
|
+
if len(visible) < len(piece) and "\x1b" in visible:
|
|
220
|
+
# Never leave a partially printed ANSI escape sequence on screen.
|
|
221
|
+
visible = ""
|
|
222
|
+
if visible:
|
|
223
|
+
if stream.isatty():
|
|
224
|
+
stream.write("\x1b[90m")
|
|
225
|
+
stream.write(visible)
|
|
226
|
+
if stream.isatty():
|
|
227
|
+
stream.write("\x1b[0m")
|
|
228
|
+
self.visible_characters += len(visible)
|
|
229
|
+
self.visible_ends_line = visible.endswith(("\n", "\r"))
|
|
230
|
+
if self.visible_ends_line:
|
|
231
|
+
self.visible_lines += 1
|
|
232
|
+
if len(visible) < len(piece):
|
|
233
|
+
self._hide(piece[len(visible) :])
|
|
234
|
+
return len(data)
|
|
235
|
+
|
|
236
|
+
def show_notice(self, number: int, error: dict[str, str] | None = None) -> None:
|
|
237
|
+
if self.visible_characters and not self.visible_ends_line:
|
|
238
|
+
sys.stdout.write("\n")
|
|
239
|
+
self.visible_ends_line = True
|
|
240
|
+
if not self.hidden_characters:
|
|
241
|
+
return
|
|
242
|
+
if sys.stdout.isatty():
|
|
243
|
+
sys.stdout.write("\x1b[0m")
|
|
244
|
+
hidden_lines = self.hidden_line_breaks + int(self.hidden_has_tail)
|
|
245
|
+
label = "line" if hidden_lines == 1 else "lines"
|
|
246
|
+
sys.stdout.write(
|
|
247
|
+
f"… {hidden_lines:,} more output {label}. Run %output {number} to view all.\n"
|
|
248
|
+
)
|
|
249
|
+
if error is not None:
|
|
250
|
+
if sys.stdout.isatty():
|
|
251
|
+
sys.stdout.write("\x1b[90m")
|
|
252
|
+
sys.stdout.write(f"{error['type']}: {error['message']}\n")
|
|
253
|
+
if sys.stdout.isatty():
|
|
254
|
+
sys.stdout.write("\x1b[0m")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class _PreviewStream:
|
|
258
|
+
def __init__(self, stream, preview: _OutputPreview) -> None:
|
|
259
|
+
self.stream = stream
|
|
260
|
+
self.preview = preview
|
|
261
|
+
self.pending = ""
|
|
262
|
+
|
|
263
|
+
def write(self, data: str) -> int:
|
|
264
|
+
complete, self.pending = _split_incomplete_ansi(self.pending + data)
|
|
265
|
+
if complete:
|
|
266
|
+
self.preview.write(self.stream, _plain_ansi(complete))
|
|
267
|
+
return len(data)
|
|
268
|
+
|
|
269
|
+
def flush(self) -> None:
|
|
270
|
+
self.stream.flush()
|
|
271
|
+
|
|
272
|
+
def __getattr__(self, name: str):
|
|
273
|
+
return getattr(self.stream, name)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@contextmanager
|
|
277
|
+
def preview_tool_output(
|
|
278
|
+
maximum_lines: int = _MAXIMUM_VISIBLE_OUTPUT_LINES,
|
|
279
|
+
) -> Iterator[_OutputPreview]:
|
|
280
|
+
"""Show a compact preview while keeping the tool cell's complete native output."""
|
|
281
|
+
columns = shutil.get_terminal_size(fallback=(80, 24)).columns
|
|
282
|
+
preview = _OutputPreview(maximum_lines, maximum_lines * max(20, columns))
|
|
283
|
+
stdout, stderr = sys.stdout, sys.stderr
|
|
284
|
+
preview_stdout = _PreviewStream(stdout, preview)
|
|
285
|
+
preview_stderr = _PreviewStream(stderr, preview)
|
|
286
|
+
sys.stdout = preview_stdout
|
|
287
|
+
sys.stderr = preview_stderr
|
|
288
|
+
try:
|
|
289
|
+
yield preview
|
|
290
|
+
finally:
|
|
291
|
+
preview_stdout.flush()
|
|
292
|
+
preview_stderr.flush()
|
|
293
|
+
sys.stdout, sys.stderr = stdout, stderr
|
|
294
|
+
|
|
295
|
+
|
|
146
296
|
def install_question_mode(shell: TerminalInteractiveShell) -> Callable[[], None]:
|
|
147
297
|
"""Let Shift+Tab switch the terminal prompt between Python and questions."""
|
|
148
298
|
prompt_session = shell.pt_app
|
|
@@ -245,6 +395,7 @@ def show_input(shell: TerminalInteractiveShell, cell: str) -> None:
|
|
|
245
395
|
style=style,
|
|
246
396
|
color_depth=shell.color_depth,
|
|
247
397
|
)
|
|
398
|
+
sys.stdout.write("\n")
|
|
248
399
|
|
|
249
400
|
|
|
250
401
|
def install_markdown_renderer(shell: TerminalInteractiveShell) -> Callable[[], None]:
|
|
@@ -256,6 +407,7 @@ def install_markdown_renderer(shell: TerminalInteractiveShell) -> Callable[[], N
|
|
|
256
407
|
previous_renderer = shell.mime_renderers.get(mime, _MISSING)
|
|
257
408
|
|
|
258
409
|
def render(text: str, _metadata: object) -> None:
|
|
410
|
+
sys.stdout.write("\n")
|
|
259
411
|
Console(file=sys.stdout, soft_wrap=True, no_color=shell.colors.lower() == "nocolor").print(
|
|
260
412
|
Markdown(text)
|
|
261
413
|
)
|
|
@@ -305,6 +457,7 @@ __all__ = [
|
|
|
305
457
|
"TerminalCellHistory",
|
|
306
458
|
"install_markdown_renderer",
|
|
307
459
|
"install_question_mode",
|
|
460
|
+
"preview_tool_output",
|
|
308
461
|
"record_tool_output",
|
|
309
462
|
"show_input",
|
|
310
463
|
]
|
|
File without changes
|
{codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/install.json
RENAMED
|
File without changes
|
{codebind-0.6.7 → codebind-0.6.9}/data/share/jupyter/labextensions/codebind-jupyterlab/package.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|