Safer locking

This commit is contained in:
Will McGugan
2022-06-21 09:31:59 +01:00
parent 5b56bc116a
commit a15e57ec74
6 changed files with 90 additions and 93 deletions

View File

@@ -182,4 +182,4 @@ if __name__ == "__main__":
from rich.style import Style
print(Style._add_cache)
print(Style._add.cache_info())

View File

@@ -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())

View File

@@ -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))

View File

@@ -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

View File

@@ -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:

View File

@@ -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: