mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Safer locking
This commit is contained in:
@@ -182,4 +182,4 @@ if __name__ == "__main__":
|
||||
|
||||
from rich.style import Style
|
||||
|
||||
print(Style._add_cache)
|
||||
print(Style._add.cache_info())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user