diff --git a/sandbox/basic.py b/sandbox/basic.py index aa3db8fa5..96bef6174 100644 --- a/sandbox/basic.py +++ b/sandbox/basic.py @@ -182,4 +182,4 @@ if __name__ == "__main__": from rich.style import Style - print(Style._add_cache) + print(Style._add.cache_info()) diff --git a/sandbox/table.py b/sandbox/table.py index bc980c88c..1a805795a 100644 --- a/sandbox/table.py +++ b/sandbox/table.py @@ -1,22 +1,59 @@ from textual.app import App, ComposeResult from textual.widgets import DataTable +from rich.syntax import Syntax +from rich.table import Table + +CODE = '''\ +def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]: + """Iterate and generate a tuple with a flag for first and last value.""" + iter_values = iter(values) + try: + previous_value = next(iter_values) + except StopIteration: + return + first = True + for value in iter_values: + yield first, False, previous_value + first = False + previous_value = value + yield first, True, previous_value''' + +test_table = Table(title="Star Wars Movies") + +test_table.add_column("Released", style="cyan", no_wrap=True) +test_table.add_column("Title", style="magenta") +test_table.add_column("Box Office", justify="right", style="green") + +test_table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690") +test_table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347") +test_table.add_row( + "Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889" +) +test_table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889") + class TableApp(App): def compose(self) -> ComposeResult: table = self.table = DataTable(id="data") + table.add_column("Foo", width=20) + table.add_column("Bar", width=60) + table.add_column("Baz", width=20) table.add_column("Foo", width=16) table.add_column("Bar", width=16) table.add_column("Baz", width=16) - table.add_column("Egg", width=16) - table.add_column("Foo", width=16) - table.add_column("Bar", width=16) - table.add_column("Baz", width=16) - table.add_column("Egg", width=16) - for n in range(100): - row = [f"row [b]{n}[/b] col [i]{c}[/i]" for c in range(8)] - table.add_row(*row) + for n in range(200): + height = 1 + row = [f"row [b]{n}[/b] col [i]{c}[/i]" for c in range(6)] + if n == 10: + row[1] = Syntax(CODE, "python", line_numbers=True, indent_guides=True) + height = 13 + + if n == 30: + row[1] = test_table + height = 13 + table.add_row(*row, height=height) yield table def on_mount(self): @@ -32,8 +69,10 @@ class TableApp(App): def action_exit(self) -> None: from rich.style import Style + self.exit(Style._add_cache.cache_info()) + app = TableApp() if __name__ == "__main__": print(app.run()) diff --git a/src/textual/_cache.py b/src/textual/_cache.py index 96800f715..8a1fbdd4b 100644 --- a/src/textual/_cache.py +++ b/src/textual/_cache.py @@ -1,10 +1,19 @@ -import sys -from collections import deque -from functools import wraps +""" + +A LRU (Least Recently Used) Cache container. + +Use when you want to cache slow operations and new keys are a good predictor +of subsequent keys. + +Note that stdlib's @lru_cache is implemented in C and faster! It's best to use +@lru_cache where you are caching things that are fairly quick and called many times. +Use LRUCache where you want increased flexibility and you are caching slow operations +where the overhead of the cache is a small fraction of the total processing time. + +""" + from threading import Lock from typing import ( - Callable, - Deque, Dict, Generic, List, @@ -14,11 +23,6 @@ from typing import ( overload, ) -if sys.version_info >= (3, 10): - from typing import ParamSpec -else: - from typing_extensions import ParamSpec - CacheKey = TypeVar("CacheKey") CacheValue = TypeVar("CacheValue") DefaultValue = TypeVar("DefaultValue") @@ -111,8 +115,8 @@ class LRUCache(Generic[CacheKey, CacheValue]): link = self.cache.get(key) if link is None: return default - if link is not self.root: - with self._lock: + with self._lock: + if link is not self.root: link[0][1] = link[1] # type: ignore[index] link[1][0] = link[0] # type: ignore[index] root = self.root @@ -121,12 +125,12 @@ class LRUCache(Generic[CacheKey, CacheValue]): root[0][1] = link # type: ignore[index] root[0] = link self.root = link - return link[3] # type: ignore[return-value] + return link[3] # type: ignore[return-value] def __getitem__(self, key: CacheKey) -> CacheValue: link = self.cache[key] - if link is not self.root: - with self._lock: + with self._lock: + if link is not self.root: link[0][1] = link[1] # type: ignore[index] link[1][0] = link[0] # type: ignore[index] root = self.root @@ -135,52 +139,7 @@ class LRUCache(Generic[CacheKey, CacheValue]): root[0][1] = link # type: ignore[index] root[0] = link self.root = link - return link[3] # type: ignore[return-value] + return link[3] # type: ignore[return-value] def __contains__(self, key: CacheKey) -> bool: return key in self.cache - - -P = ParamSpec("P") -T = TypeVar("T") - - -def fifo_cache(maxsize: int) -> Callable[[Callable[P, T]], Callable[P, T]]: - """A First In First Out cache. - - Args: - maxsize (int): Maximum size of the cache - - """ - - def decorator(func: Callable[P, T]) -> Callable[P, T]: - queue: Deque[object] = deque() - cache: Dict[object, T] = {} - - @wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - try: - return cache[args] - except KeyError: - assert not kwargs, "Will not work with keyword arguments!" - cache[args] = result = func(*args) - queue.append(args) - if len(queue) > maxsize: - del cache[queue.popleft()] - return result - - return wrapper - - return decorator - - -@fifo_cache(10) -def double(n: int) -> int: - return n * n - - -print(double(1)) -print(double(2)) -print(double(2)) -print(double(3)) -print(double(4)) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 64eb6d8d4..a1369c341 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -566,12 +566,9 @@ class Compositor: cls, chops: list[dict[int, list[Segment] | None]] ) -> list[list[Segment]]: """Combine chops in to lines.""" + from_iterable = chain.from_iterable segment_lines: list[list[Segment]] = [ - list( - chain.from_iterable( - line for line in bucket.values() if line is not None - ) - ) + list(from_iterable(line for line in bucket.values() if line is not None)) for bucket in chops ] return segment_lines diff --git a/src/textual/app.py b/src/textual/app.py index d6e8b3d7c..736e2e8f8 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -1135,9 +1135,6 @@ class App(Generic[ReturnType], DOMNode): async def action_toggle_class(self, selector: str, class_name: str) -> None: self.screen.query(selector).toggle_class(class_name) - # async def handle_styles_updated(self, message: messages.StylesUpdated) -> None: - # self.stylesheet.update(self, animate=True) - def handle_terminal_supports_synchronized_output( self, message: messages.TerminalSupportsSynchronizedOutput ) -> None: diff --git a/src/textual/widgets/_datatable.py b/src/textual/widgets/_datatable.py index 4fe3a896d..920b6d0ba 100644 --- a/src/textual/widgets/_datatable.py +++ b/src/textual/widgets/_datatable.py @@ -30,7 +30,7 @@ def default_cell_formatter(obj: object) -> RenderableType | None: if isinstance(obj, str): return Text.from_markup(obj) if not is_renderable(obj): - raise TypeError("Table cell contains {obj!r} which is not renderable") + raise TypeError(f"Table cell {obj!r} is not renderable") return cast(RenderableType, obj) @@ -166,7 +166,7 @@ class DataTable(ScrollView, Generic[CellType]): self._update_dimensions() self.refresh() - def add_row(self, *cells: CellType, height: int = 3) -> None: + def add_row(self, *cells: CellType, height: int = 1) -> None: row_index = self.row_count self.data[row_index] = list(cells) self.rows[row_index] = Row(row_index, height=height) @@ -187,22 +187,23 @@ class DataTable(ScrollView, Generic[CellType]): data = self.data.get(row_index) empty = Text() if data is None: - return [Text("!") for column in self.columns] + return [empty for _ in self.columns] else: return [default_cell_formatter(datum) or empty for datum in data] - def _render_cell(self, row_index: int, column: Column, style: Style) -> Lines: - - cell_key = (row_index, column.index, style) + def _render_cell( + self, row_index: int, column_index: int, style: Style, width: int + ) -> Lines: + cell_key = (row_index, column_index, style) if cell_key not in self._cell_render_cache: - style += Style.from_meta({"row": row_index, "column": column.index}) + style += Style.from_meta({"row": row_index, "column": column_index}) height = ( self.header_height if row_index == -1 else self.rows[row_index].height ) - cell = self.get_row_renderables(row_index)[column.index] + cell = self.get_row_renderables(row_index)[column_index] lines = self.app.console.render_lines( Padding(cell, (0, 1)), - self.app.console.options.update_dimensions(column.width, height), + self.app.console.options.update_dimensions(width, height), style=style, ) self._cell_render_cache[cell_key] = lines @@ -217,11 +218,13 @@ class DataTable(ScrollView, Generic[CellType]): if cache_key in self._row_render_cache: return self._row_render_cache[cache_key] + render_cell = self._render_cell + if self.fixed_columns: fixed_style = self.component_styles["datatable--fixed"].node.rich_style fixed_style += Style.from_meta({"fixed": True}) fixed_row = [ - self._render_cell(row_index, column, fixed_style)[line_no] + render_cell(row_index, column.index, fixed_style, column.width)[line_no] for column in self.columns[: self.fixed_columns] ] else: @@ -239,7 +242,7 @@ class DataTable(ScrollView, Generic[CellType]): row_style = base_style scrollable_row = [ - self._render_cell(row_index, column, row_style)[line_no] + render_cell(row_index, column.index, row_style, column.width)[line_no] for column in self.columns ] @@ -281,7 +284,9 @@ class DataTable(ScrollView, Generic[CellType]): elif remaining_width < 0: segments = Segment.adjust_line_length(segments, width, style=base_style) - self._line_cache[cache_key] = segments + simplified_segments = list(Segment.simplify(segments)) + + self._line_cache[cache_key] = simplified_segments return segments @timer("render_lines") @@ -298,11 +303,11 @@ class DataTable(ScrollView, Generic[CellType]): if self.show_header: fixed_top_row_count += self.get_row_height(-1) + render_line = self._render_line fixed_lines = [ - self._render_line(y, x1, x2, base_style) - for y in range(0, fixed_top_row_count) + render_line(y, x1, x2, base_style) for y in range(0, fixed_top_row_count) ] - lines = [self._render_line(y, x1, x2, base_style) for y in range(y1, y2)] + lines = [render_line(y, x1, x2, base_style) for y in range(y1, y2)] for line_index, y in enumerate(range(y1, y2)): if y - scroll_y < fixed_top_row_count: