Skip to content

Commit

Permalink
Merge branch 'master' into capture-bugfix
Browse files Browse the repository at this point in the history
  • Loading branch information
willmcgugan committed Jun 17, 2022
2 parents d110847 + 7dea1e7 commit 19e518f
Show file tree
Hide file tree
Showing 10 changed files with 321 additions and 84 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix crashes that can happen with `inspect` when docstrings contain some special control codes https://github.com/Textualize/rich/pull/2294
- Fix edges used in first row of tables when `show_header=False` https://github.com/Textualize/rich/pull/2330
- Fix interaction between `Capture` contexts and `Console(record=True)` https://github.com/Textualize/rich/pull/2343
- Fixed hash issue in Styles class https://github.com/Textualize/rich/pull/2346

## [12.4.4] - 2022-05-24

Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Expand Up @@ -12,6 +12,7 @@ The following people have contributed to the development of Rich:
- [Pete Davison](https://github.com/pd93)
- [James Estevez](https://github.com/jstvz)
- [Oleksis Fraga](https://github.com/oleksis)
- [Andy Gimblett](https://github.com/gimbo)
- [Michał Górny](https://github.com/mgorny)
- [Leron Gray](https://github.com/daddycocoaman)
- [Kenneth Hoste](https://github.com/boegel)
Expand Down
120 changes: 99 additions & 21 deletions rich/_lru_cache.py
@@ -1,38 +1,116 @@
from typing import Dict, Generic, TypeVar, TYPE_CHECKING
import sys
from threading import Lock
from typing import Dict, Generic, List, Optional, TypeVar, Union, overload

CacheKey = TypeVar("CacheKey")
CacheValue = TypeVar("CacheValue")
DefaultValue = TypeVar("DefaultValue")

if sys.version_info < (3, 9):
from typing_extensions import OrderedDict
else:
from collections import OrderedDict


class LRUCache(OrderedDict[CacheKey, CacheValue]):
class LRUCache(Generic[CacheKey, CacheValue]):
"""
A dictionary-like container that stores a given maximum items.
If an additional item is added when the LRUCache is full, the least
recently used key is discarded to make room for the new item.
The implementation is similar to functools.lru_cache, which uses a linked
list to keep track of the most recently used items.
Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference
to the previous entry, and NEXT is a reference to the next value.
"""

def __init__(self, cache_size: int) -> None:
self.cache_size = cache_size
def __init__(self, maxsize: int) -> None:
self.maxsize = maxsize
self.cache: Dict[CacheKey, List[object]] = {}
self.full = False
self.root: List[object] = []
self._lock = Lock()
super().__init__()

def __setitem__(self, key: CacheKey, value: CacheValue) -> None:
"""Store a new views, potentially discarding an old value."""
if key not in self:
if len(self) >= self.cache_size:
self.popitem(last=False)
super().__setitem__(key, value)
def __len__(self) -> int:
return len(self.cache)

def set(self, key: CacheKey, value: CacheValue) -> None:
"""Set a value.
Args:
key (CacheKey): Key.
value (CacheValue): Value.
"""
with self._lock:
link = self.cache.get(key)
if link is None:
root = self.root
if not root:
self.root[:] = [self.root, self.root, key, value]
else:
self.root = [root[0], root, key, value]
root[0][1] = self.root # type: ignore[index]
root[0] = self.root
self.cache[key] = self.root

if self.full or len(self.cache) > self.maxsize:
self.full = True
root = self.root
last = root[0]
last[0][1] = root # type: ignore[index]
root[0] = last[0] # type: ignore[index]
del self.cache[last[2]] # type: ignore[index]

__setitem__ = set

@overload
def get(self, key: CacheKey) -> Optional[CacheValue]:
...

@overload
def get(
self, key: CacheKey, default: DefaultValue
) -> Union[CacheValue, DefaultValue]:
...

def get(
self, key: CacheKey, default: Optional[DefaultValue] = None
) -> Union[CacheValue, Optional[DefaultValue]]:
"""Get a value from the cache, or return a default if the key is not present.
Args:
key (CacheKey): Key
default (Optional[DefaultValue], optional): Default to return if key is not present. Defaults to None.
Returns:
Union[CacheValue, Optional[DefaultValue]]: Either the value or a default.
"""
link = self.cache.get(key)
if link is None:
return default
if link is not self.root:
with self._lock:
link[0][1] = link[1] # type: ignore[index]
link[1][0] = link[0] # type: ignore[index]
root = self.root
link[0] = root[0]
link[1] = root
root[0][1] = link # type: ignore[index]
root[0] = link
self.root = link
return link[3] # type: ignore[return-value]

def __getitem__(self, key: CacheKey) -> CacheValue:
"""Gets the item, but also makes it most recent."""
value: CacheValue = super().__getitem__(key)
super().__delitem__(key)
super().__setitem__(key, value)
return value
link = self.cache[key]
if link is not self.root:
with self._lock:
link[0][1] = link[1] # type: ignore[index]
link[1][0] = link[0] # type: ignore[index]
root = self.root
link[0] = root[0]
link[1] = root
root[0][1] = link # type: ignore[index]
root[0] = link
self.root = link
return link[3] # type: ignore[return-value]

def __contains__(self, key: CacheKey) -> bool:
return key in self.cache
6 changes: 3 additions & 3 deletions rich/cells.py
@@ -1,6 +1,6 @@
import re
from functools import lru_cache
from typing import Dict, List
from typing import List

from ._cell_widths import CELL_WIDTHS
from ._lru_cache import LRUCache
Expand All @@ -9,7 +9,7 @@
_is_single_cell_widths = re.compile("^[\u0020-\u006f\u00a0\u02ff\u0370-\u0482]*$").match


def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int:
def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024 * 4)) -> int:
"""Get the number of cells required to display text.
Args:
Expand Down Expand Up @@ -80,7 +80,7 @@ def set_cell_size(text: str, total: int) -> str:
return text + " " * (total - size)
return text[:total]

if not total:
if total <= 0:
return ""
cell_size = cell_len(text)
if cell_size == total:
Expand Down
37 changes: 28 additions & 9 deletions rich/rule.py
Expand Up @@ -4,6 +4,7 @@
from .cells import cell_len, set_cell_size
from .console import Console, ConsoleOptions, RenderResult
from .jupyter import JupyterMixin
from .measure import Measurement
from .style import Style
from .text import Text

Expand Down Expand Up @@ -62,10 +63,7 @@ def __rich_console__(

chars_len = cell_len(characters)
if not self.title:
rule_text = Text(characters * ((width // chars_len) + 1), self.style)
rule_text.truncate(width)
rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text
yield self._rule_line(chars_len, width)
return

if isinstance(self.title, Text):
Expand All @@ -75,10 +73,16 @@ def __rich_console__(

title_text.plain = title_text.plain.replace("\n", " ")
title_text.expand_tabs()
rule_text = Text(end=self.end)

required_space = 4 if self.align == "center" else 2
truncate_width = max(0, width - required_space)
if not truncate_width:
yield self._rule_line(chars_len, width)
return

rule_text = Text(end=self.end)
if self.align == "center":
title_text.truncate(width - 4, overflow="ellipsis")
title_text.truncate(truncate_width, overflow="ellipsis")
side_width = (width - cell_len(title_text.plain)) // 2
left = Text(characters * (side_width // chars_len + 1))
left.truncate(side_width - 1)
Expand All @@ -89,27 +93,42 @@ def __rich_console__(
rule_text.append(title_text)
rule_text.append(" " + right.plain, self.style)
elif self.align == "left":
title_text.truncate(width - 2, overflow="ellipsis")
title_text.truncate(truncate_width, overflow="ellipsis")
rule_text.append(title_text)
rule_text.append(" ")
rule_text.append(characters * (width - rule_text.cell_len), self.style)
elif self.align == "right":
title_text.truncate(width - 2, overflow="ellipsis")
title_text.truncate(truncate_width, overflow="ellipsis")
rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
rule_text.append(" ")
rule_text.append(title_text)

rule_text.plain = set_cell_size(rule_text.plain, width)
yield rule_text

def _rule_line(self, chars_len: int, width: int) -> Text:
rule_text = Text(self.characters * ((width // chars_len) + 1), self.style)
rule_text.truncate(width)
rule_text.plain = set_cell_size(rule_text.plain, width)
return rule_text

def __rich_measure__(
self, console: Console, options: ConsoleOptions
) -> Measurement:
return Measurement(1, 1)


if __name__ == "__main__": # pragma: no cover
from rich.console import Console
import sys

from rich.console import Console

try:
text = sys.argv[1]
except IndexError:
text = "Hello, World"
console = Console()
console.print(Rule(title=text))

console = Console()
console.print(Rule("foo"), width=4)

0 comments on commit 19e518f

Please sign in to comment.