Skip to content

Commit 7bd133b

Browse files
committed
Chunk output for performance reasons and upgrade textual.
1 parent d3c2f0f commit 7bd133b

File tree

5 files changed

+150
-25
lines changed

5 files changed

+150
-25
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dependencies = [
1212
"nbformat>=5.10.4",
1313
"pyvips[binary]>=3.0.0",
1414
"textual-image[textual]>=0.8.1",
15-
"textual[syntax]>=6.6.0",
15+
"textual[syntax]>=6.11.0",
1616
]
1717

1818
[project.urls]
@@ -34,6 +34,7 @@ dev = [
3434
"pytest>=8.4.1",
3535
"pytest-asyncio>=1.0.0",
3636
"pytest-mock>=3.14.1",
37+
"requests>=2.32.5",
3738
"textual-dev>=1.7.0",
3839
"tqdm>=4.67.1",
3940
"tree-sitter-julia>=0.23.1",

src/netbook/_cell.py

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,31 @@ def watch_format(self, format) -> None:
4343
self.update(format.format(self.execution_count))
4444

4545

46+
class ChunkedStatic(textual.containers.Container):
47+
"""This widget splits a static text into a multiple chunks of `textual.widgets.Static` for performance."""
48+
49+
CHUNK_SIZE = 500
50+
51+
def __init__(self, text: str) -> None:
52+
super().__init__()
53+
54+
# splitlines - used by rich.text.Text.from_ansi splits on "\r" too.
55+
# Some libraries e.g. tqdm relies on "\r" to rerender on the same line. So we split on "\n"-only.
56+
lines = text.split("\n")
57+
start = 0
58+
while start < len(lines) and not lines[start].strip():
59+
start += 1
60+
end = len(lines) - 1
61+
while end >= start and not lines[end].strip():
62+
end -= 1
63+
decoder = rich.ansi.AnsiDecoder()
64+
self.decoded_lines = [decoder.decode_line(line) for line in lines[start : end + 1]]
65+
66+
def compose(self) -> textual.app.ComposeResult:
67+
for i in range(0, len(self.decoded_lines), self.CHUNK_SIZE):
68+
yield textual.widgets.Static(rich.text.Text("\n").join(self.decoded_lines[i : i + self.CHUNK_SIZE]))
69+
70+
4671
class Output(textual.containers.Container):
4772
"""Base class for code cell output."""
4873

@@ -60,24 +85,6 @@ def to_nbformat(self) -> nbformat.NotebookNode:
6085
def on_resize(self, event: textual.events.Resize) -> None:
6186
self.post_message(Output.Resized(self, event.size))
6287

63-
@staticmethod
64-
def ansi_to_rich(text: str, strip: bool = True) -> rich.text.Text:
65-
"""Convet ansi escaped sequences to rich text.
66-
67-
Work around some issues with rich.text.Text.from_ansi and optionally strip blank lines from the start and end."""
68-
69-
# splitlines - used by rich.text.Text.from_ansi splits on "\r" too.
70-
# E.g. tqdm relies on "\r" to rerender on the same line. So we split on "\n"-only.
71-
lines = text.split("\n")
72-
start = 0
73-
while strip and start < len(lines) and not lines[start].strip():
74-
start += 1
75-
end = len(lines) - 1
76-
while strip and end >= start and not lines[end].strip():
77-
end -= 1
78-
decoder = rich.ansi.AnsiDecoder()
79-
return rich.text.Text("\n").join(decoder.decode_line(line) for line in lines[start : end + 1])
80-
8188

8289
class Stream(Output):
8390
"""Stream output in notebook."""
@@ -94,7 +101,7 @@ def to_nbformat(self) -> nbformat.NotebookNode:
94101
return nbformat.v4.new_output(output_type="stream", name=self.stream_name, text=self.text)
95102

96103
def compose(self) -> tp.Iterable[textual.widgets.Widget]:
97-
yield textual.widgets.Static(self.ansi_to_rich(self.text))
104+
yield ChunkedStatic(self.text)
98105

99106

100107
class DisplayData(Output):
@@ -132,7 +139,7 @@ def compose(self) -> tp.Iterable[textual.widgets.Widget]:
132139
elif self.data.get("text/markdown"):
133140
yield textual.widgets.Markdown(self.data["text/markdown"])
134141
elif self.data.get("text/plain"):
135-
yield textual.widgets.Static(self.ansi_to_rich(self.data["text/plain"]))
142+
yield ChunkedStatic(self.data["text/plain"])
136143

137144

138145
class ExecuteResult(DisplayData):

src/netbook/_text_area.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ def action_cursor_up(self, select: bool = False) -> None:
135135
if not select and self.cursor_location == old_location:
136136
self.post_message(CellTextArea.CursorOutTop(self))
137137

138+
@property
139+
def is_container(self) -> bool:
140+
"""We add popups as children."""
141+
return True
142+
138143

139144
class CodeTextArea(CellTextArea):
140145
"""Extends text area with jupyter completions."""

src/netbook/netbook.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ Cell {
113113
background: midnightblue;
114114
}
115115

116+
ChunkedStatic {
117+
width: auto;
118+
}
119+
116120
Container.codeoutput.collapsed {
117121
height: 1;
118122
Button {

0 commit comments

Comments
 (0)