Skip to content

Commit 19e518f

Browse files
authored
Merge branch 'master' into capture-bugfix
2 parents d110847 + 7dea1e7 commit 19e518f

File tree

10 files changed

+321
-84
lines changed

10 files changed

+321
-84
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
- Fix crashes that can happen with `inspect` when docstrings contain some special control codes https://github.com/Textualize/rich/pull/2294
1616
- Fix edges used in first row of tables when `show_header=False` https://github.com/Textualize/rich/pull/2330
1717
- Fix interaction between `Capture` contexts and `Console(record=True)` https://github.com/Textualize/rich/pull/2343
18+
- Fixed hash issue in Styles class https://github.com/Textualize/rich/pull/2346
1819

1920
## [12.4.4] - 2022-05-24
2021

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The following people have contributed to the development of Rich:
1212
- [Pete Davison](https://github.com/pd93)
1313
- [James Estevez](https://github.com/jstvz)
1414
- [Oleksis Fraga](https://github.com/oleksis)
15+
- [Andy Gimblett](https://github.com/gimbo)
1516
- [Michał Górny](https://github.com/mgorny)
1617
- [Leron Gray](https://github.com/daddycocoaman)
1718
- [Kenneth Hoste](https://github.com/boegel)

rich/_lru_cache.py

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,116 @@
1-
from typing import Dict, Generic, TypeVar, TYPE_CHECKING
2-
import sys
1+
from threading import Lock
2+
from typing import Dict, Generic, List, Optional, TypeVar, Union, overload
33

44
CacheKey = TypeVar("CacheKey")
55
CacheValue = TypeVar("CacheValue")
6+
DefaultValue = TypeVar("DefaultValue")
67

7-
if sys.version_info < (3, 9):
8-
from typing_extensions import OrderedDict
9-
else:
10-
from collections import OrderedDict
118

12-
13-
class LRUCache(OrderedDict[CacheKey, CacheValue]):
9+
class LRUCache(Generic[CacheKey, CacheValue]):
1410
"""
1511
A dictionary-like container that stores a given maximum items.
1612
1713
If an additional item is added when the LRUCache is full, the least
1814
recently used key is discarded to make room for the new item.
1915
16+
The implementation is similar to functools.lru_cache, which uses a linked
17+
list to keep track of the most recently used items.
18+
19+
Each entry is stored as [PREV, NEXT, KEY, VALUE] where PREV is a reference
20+
to the previous entry, and NEXT is a reference to the next value.
21+
2022
"""
2123

22-
def __init__(self, cache_size: int) -> None:
23-
self.cache_size = cache_size
24+
def __init__(self, maxsize: int) -> None:
25+
self.maxsize = maxsize
26+
self.cache: Dict[CacheKey, List[object]] = {}
27+
self.full = False
28+
self.root: List[object] = []
29+
self._lock = Lock()
2430
super().__init__()
2531

26-
def __setitem__(self, key: CacheKey, value: CacheValue) -> None:
27-
"""Store a new views, potentially discarding an old value."""
28-
if key not in self:
29-
if len(self) >= self.cache_size:
30-
self.popitem(last=False)
31-
super().__setitem__(key, value)
32+
def __len__(self) -> int:
33+
return len(self.cache)
34+
35+
def set(self, key: CacheKey, value: CacheValue) -> None:
36+
"""Set a value.
37+
38+
Args:
39+
key (CacheKey): Key.
40+
value (CacheValue): Value.
41+
"""
42+
with self._lock:
43+
link = self.cache.get(key)
44+
if link is None:
45+
root = self.root
46+
if not root:
47+
self.root[:] = [self.root, self.root, key, value]
48+
else:
49+
self.root = [root[0], root, key, value]
50+
root[0][1] = self.root # type: ignore[index]
51+
root[0] = self.root
52+
self.cache[key] = self.root
53+
54+
if self.full or len(self.cache) > self.maxsize:
55+
self.full = True
56+
root = self.root
57+
last = root[0]
58+
last[0][1] = root # type: ignore[index]
59+
root[0] = last[0] # type: ignore[index]
60+
del self.cache[last[2]] # type: ignore[index]
61+
62+
__setitem__ = set
63+
64+
@overload
65+
def get(self, key: CacheKey) -> Optional[CacheValue]:
66+
...
67+
68+
@overload
69+
def get(
70+
self, key: CacheKey, default: DefaultValue
71+
) -> Union[CacheValue, DefaultValue]:
72+
...
73+
74+
def get(
75+
self, key: CacheKey, default: Optional[DefaultValue] = None
76+
) -> Union[CacheValue, Optional[DefaultValue]]:
77+
"""Get a value from the cache, or return a default if the key is not present.
78+
79+
Args:
80+
key (CacheKey): Key
81+
default (Optional[DefaultValue], optional): Default to return if key is not present. Defaults to None.
82+
83+
Returns:
84+
Union[CacheValue, Optional[DefaultValue]]: Either the value or a default.
85+
"""
86+
link = self.cache.get(key)
87+
if link is None:
88+
return default
89+
if link is not self.root:
90+
with self._lock:
91+
link[0][1] = link[1] # type: ignore[index]
92+
link[1][0] = link[0] # type: ignore[index]
93+
root = self.root
94+
link[0] = root[0]
95+
link[1] = root
96+
root[0][1] = link # type: ignore[index]
97+
root[0] = link
98+
self.root = link
99+
return link[3] # type: ignore[return-value]
32100

33101
def __getitem__(self, key: CacheKey) -> CacheValue:
34-
"""Gets the item, but also makes it most recent."""
35-
value: CacheValue = super().__getitem__(key)
36-
super().__delitem__(key)
37-
super().__setitem__(key, value)
38-
return value
102+
link = self.cache[key]
103+
if link is not self.root:
104+
with self._lock:
105+
link[0][1] = link[1] # type: ignore[index]
106+
link[1][0] = link[0] # type: ignore[index]
107+
root = self.root
108+
link[0] = root[0]
109+
link[1] = root
110+
root[0][1] = link # type: ignore[index]
111+
root[0] = link
112+
self.root = link
113+
return link[3] # type: ignore[return-value]
114+
115+
def __contains__(self, key: CacheKey) -> bool:
116+
return key in self.cache

rich/cells.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22
from functools import lru_cache
3-
from typing import Dict, List
3+
from typing import List
44

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

1111

12-
def cell_len(text: str, _cache: Dict[str, int] = LRUCache(1024 * 4)) -> int:
12+
def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024 * 4)) -> int:
1313
"""Get the number of cells required to display text.
1414
1515
Args:
@@ -80,7 +80,7 @@ def set_cell_size(text: str, total: int) -> str:
8080
return text + " " * (total - size)
8181
return text[:total]
8282

83-
if not total:
83+
if total <= 0:
8484
return ""
8585
cell_size = cell_len(text)
8686
if cell_size == total:

rich/rule.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from .cells import cell_len, set_cell_size
55
from .console import Console, ConsoleOptions, RenderResult
66
from .jupyter import JupyterMixin
7+
from .measure import Measurement
78
from .style import Style
89
from .text import Text
910

@@ -62,10 +63,7 @@ def __rich_console__(
6263

6364
chars_len = cell_len(characters)
6465
if not self.title:
65-
rule_text = Text(characters * ((width // chars_len) + 1), self.style)
66-
rule_text.truncate(width)
67-
rule_text.plain = set_cell_size(rule_text.plain, width)
68-
yield rule_text
66+
yield self._rule_line(chars_len, width)
6967
return
7068

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

7674
title_text.plain = title_text.plain.replace("\n", " ")
7775
title_text.expand_tabs()
78-
rule_text = Text(end=self.end)
7976

77+
required_space = 4 if self.align == "center" else 2
78+
truncate_width = max(0, width - required_space)
79+
if not truncate_width:
80+
yield self._rule_line(chars_len, width)
81+
return
82+
83+
rule_text = Text(end=self.end)
8084
if self.align == "center":
81-
title_text.truncate(width - 4, overflow="ellipsis")
85+
title_text.truncate(truncate_width, overflow="ellipsis")
8286
side_width = (width - cell_len(title_text.plain)) // 2
8387
left = Text(characters * (side_width // chars_len + 1))
8488
left.truncate(side_width - 1)
@@ -89,27 +93,42 @@ def __rich_console__(
8993
rule_text.append(title_text)
9094
rule_text.append(" " + right.plain, self.style)
9195
elif self.align == "left":
92-
title_text.truncate(width - 2, overflow="ellipsis")
96+
title_text.truncate(truncate_width, overflow="ellipsis")
9397
rule_text.append(title_text)
9498
rule_text.append(" ")
9599
rule_text.append(characters * (width - rule_text.cell_len), self.style)
96100
elif self.align == "right":
97-
title_text.truncate(width - 2, overflow="ellipsis")
101+
title_text.truncate(truncate_width, overflow="ellipsis")
98102
rule_text.append(characters * (width - title_text.cell_len - 1), self.style)
99103
rule_text.append(" ")
100104
rule_text.append(title_text)
101105

102106
rule_text.plain = set_cell_size(rule_text.plain, width)
103107
yield rule_text
104108

109+
def _rule_line(self, chars_len: int, width: int) -> Text:
110+
rule_text = Text(self.characters * ((width // chars_len) + 1), self.style)
111+
rule_text.truncate(width)
112+
rule_text.plain = set_cell_size(rule_text.plain, width)
113+
return rule_text
114+
115+
def __rich_measure__(
116+
self, console: Console, options: ConsoleOptions
117+
) -> Measurement:
118+
return Measurement(1, 1)
119+
105120

106121
if __name__ == "__main__": # pragma: no cover
107-
from rich.console import Console
108122
import sys
109123

124+
from rich.console import Console
125+
110126
try:
111127
text = sys.argv[1]
112128
except IndexError:
113129
text = "Hello, World"
114130
console = Console()
115131
console.print(Rule(title=text))
132+
133+
console = Console()
134+
console.print(Rule("foo"), width=4)

0 commit comments

Comments
 (0)