Merge pull request #22 from willmcgugan/terminal-width

Terminal width
This commit is contained in:
Will McGugan 2020-03-17 17:13:27 +00:00 committed by GitHub
commit 3f6625365a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 789 additions and 60 deletions

1
.gitignore vendored
View file

@ -4,6 +4,7 @@
mypy_report
docs/build
docs/source/_build
tools/*.txt
# Byte-compiled / optimized / DLL files
__pycache__/

View file

@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.8.0] - 2020-03-17
### Added
- CJK support
- Console level highlight flag
- Added encoding argument to Syntax.from_path
### Changed
- Dropped support for Windows command prompt (try https://www.microsoft.com/en-gb/p/windows-terminal-preview/)
- Added task_id to Progress.track
## [0.7.2] - 2020-03-15
### Fixed

View file

@ -2,7 +2,7 @@
name = "rich"
homepage = "https://github.com/willmcgugan/rich"
documentation = "https://rich.readthedocs.io/en/latest/"
version = "0.7.2"
version = "0.8.0-alpha.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
authors = ["Will McGugan <willmcgugan@gmail.com>"]
license = "MIT"
@ -25,7 +25,6 @@ python = "^3.6"
pprintpp = "^0.4.0"
typing-extensions = "^3.7.4"
dataclasses = {version="^0.7", python = "~3.6"}
colorama = "^0.4.3"
pygments = "^2.5.0"
commonmark = "^0.9.0"

View file

@ -1,5 +1,3 @@
from colorama import init
from typing import Any, IO, Optional, TYPE_CHECKING
if TYPE_CHECKING:
@ -22,7 +20,5 @@ def print(
return write_console.print(*objects, sep=sep, end=end)
init()
if __name__ == "__main__": # pragma: no cover
print("Hello, **World**")

407
rich/_cell_widths.py Normal file
View file

@ -0,0 +1,407 @@
# Auto generated by make_terminal_widths.py
CELL_WIDTHS = [
(0, 0, 0),
(1, 31, -1),
(127, 159, -1),
(768, 879, 0),
(1155, 1161, 0),
(1425, 1469, 0),
(1471, 1471, 0),
(1473, 1474, 0),
(1476, 1477, 0),
(1479, 1479, 0),
(1552, 1562, 0),
(1611, 1631, 0),
(1648, 1648, 0),
(1750, 1756, 0),
(1759, 1764, 0),
(1767, 1768, 0),
(1770, 1773, 0),
(1809, 1809, 0),
(1840, 1866, 0),
(1958, 1968, 0),
(2027, 2035, 0),
(2070, 2073, 0),
(2075, 2083, 0),
(2085, 2087, 0),
(2089, 2093, 0),
(2137, 2139, 0),
(2260, 2273, 0),
(2275, 2306, 0),
(2362, 2362, 0),
(2364, 2364, 0),
(2369, 2376, 0),
(2381, 2381, 0),
(2385, 2391, 0),
(2402, 2403, 0),
(2433, 2433, 0),
(2492, 2492, 0),
(2497, 2500, 0),
(2509, 2509, 0),
(2530, 2531, 0),
(2561, 2562, 0),
(2620, 2620, 0),
(2625, 2626, 0),
(2631, 2632, 0),
(2635, 2637, 0),
(2641, 2641, 0),
(2672, 2673, 0),
(2677, 2677, 0),
(2689, 2690, 0),
(2748, 2748, 0),
(2753, 2757, 0),
(2759, 2760, 0),
(2765, 2765, 0),
(2786, 2787, 0),
(2817, 2817, 0),
(2876, 2876, 0),
(2879, 2879, 0),
(2881, 2884, 0),
(2893, 2893, 0),
(2902, 2902, 0),
(2914, 2915, 0),
(2946, 2946, 0),
(3008, 3008, 0),
(3021, 3021, 0),
(3072, 3072, 0),
(3134, 3136, 0),
(3142, 3144, 0),
(3146, 3149, 0),
(3157, 3158, 0),
(3170, 3171, 0),
(3201, 3201, 0),
(3260, 3260, 0),
(3263, 3263, 0),
(3270, 3270, 0),
(3276, 3277, 0),
(3298, 3299, 0),
(3329, 3329, 0),
(3393, 3396, 0),
(3405, 3405, 0),
(3426, 3427, 0),
(3530, 3530, 0),
(3538, 3540, 0),
(3542, 3542, 0),
(3633, 3633, 0),
(3636, 3642, 0),
(3655, 3662, 0),
(3761, 3761, 0),
(3764, 3769, 0),
(3771, 3772, 0),
(3784, 3789, 0),
(3864, 3865, 0),
(3893, 3893, 0),
(3895, 3895, 0),
(3897, 3897, 0),
(3953, 3966, 0),
(3968, 3972, 0),
(3974, 3975, 0),
(3981, 3991, 0),
(3993, 4028, 0),
(4038, 4038, 0),
(4141, 4144, 0),
(4146, 4151, 0),
(4153, 4154, 0),
(4157, 4158, 0),
(4184, 4185, 0),
(4190, 4192, 0),
(4209, 4212, 0),
(4226, 4226, 0),
(4229, 4230, 0),
(4237, 4237, 0),
(4253, 4253, 0),
(4352, 4447, 2),
(4957, 4959, 0),
(5906, 5908, 0),
(5938, 5940, 0),
(5970, 5971, 0),
(6002, 6003, 0),
(6068, 6069, 0),
(6071, 6077, 0),
(6086, 6086, 0),
(6089, 6099, 0),
(6109, 6109, 0),
(6155, 6157, 0),
(6277, 6278, 0),
(6313, 6313, 0),
(6432, 6434, 0),
(6439, 6440, 0),
(6450, 6450, 0),
(6457, 6459, 0),
(6679, 6680, 0),
(6683, 6683, 0),
(6742, 6742, 0),
(6744, 6750, 0),
(6752, 6752, 0),
(6754, 6754, 0),
(6757, 6764, 0),
(6771, 6780, 0),
(6783, 6783, 0),
(6832, 6846, 0),
(6912, 6915, 0),
(6964, 6964, 0),
(6966, 6970, 0),
(6972, 6972, 0),
(6978, 6978, 0),
(7019, 7027, 0),
(7040, 7041, 0),
(7074, 7077, 0),
(7080, 7081, 0),
(7083, 7085, 0),
(7142, 7142, 0),
(7144, 7145, 0),
(7149, 7149, 0),
(7151, 7153, 0),
(7212, 7219, 0),
(7222, 7223, 0),
(7376, 7378, 0),
(7380, 7392, 0),
(7394, 7400, 0),
(7405, 7405, 0),
(7412, 7412, 0),
(7416, 7417, 0),
(7616, 7669, 0),
(7675, 7679, 0),
(8203, 8207, 0),
(8232, 8238, 0),
(8288, 8291, 0),
(8400, 8432, 0),
(8986, 8987, 2),
(9001, 9002, 2),
(9193, 9196, 2),
(9200, 9200, 2),
(9203, 9203, 2),
(9725, 9726, 2),
(9748, 9749, 2),
(9800, 9811, 2),
(9855, 9855, 2),
(9875, 9875, 2),
(9889, 9889, 2),
(9898, 9899, 2),
(9917, 9918, 2),
(9924, 9925, 2),
(9934, 9934, 2),
(9940, 9940, 2),
(9962, 9962, 2),
(9970, 9971, 2),
(9973, 9973, 2),
(9978, 9978, 2),
(9981, 9981, 2),
(9989, 9989, 2),
(9994, 9995, 2),
(10024, 10024, 2),
(10060, 10060, 2),
(10062, 10062, 2),
(10067, 10069, 2),
(10071, 10071, 2),
(10133, 10135, 2),
(10160, 10160, 2),
(10175, 10175, 2),
(11035, 11036, 2),
(11088, 11088, 2),
(11093, 11093, 2),
(11503, 11505, 0),
(11647, 11647, 0),
(11744, 11775, 0),
(11904, 11929, 2),
(11931, 12019, 2),
(12032, 12245, 2),
(12272, 12283, 2),
(12288, 12329, 2),
(12330, 12333, 0),
(12334, 12350, 2),
(12353, 12438, 2),
(12441, 12442, 0),
(12443, 12543, 2),
(12549, 12591, 2),
(12593, 12686, 2),
(12688, 12730, 2),
(12736, 12771, 2),
(12784, 12830, 2),
(12832, 12871, 2),
(12880, 19903, 2),
(19968, 42124, 2),
(42128, 42182, 2),
(42607, 42610, 0),
(42612, 42621, 0),
(42654, 42655, 0),
(42736, 42737, 0),
(43010, 43010, 0),
(43014, 43014, 0),
(43019, 43019, 0),
(43045, 43046, 0),
(43204, 43205, 0),
(43232, 43249, 0),
(43302, 43309, 0),
(43335, 43345, 0),
(43360, 43388, 2),
(43392, 43394, 0),
(43443, 43443, 0),
(43446, 43449, 0),
(43452, 43452, 0),
(43493, 43493, 0),
(43561, 43566, 0),
(43569, 43570, 0),
(43573, 43574, 0),
(43587, 43587, 0),
(43596, 43596, 0),
(43644, 43644, 0),
(43696, 43696, 0),
(43698, 43700, 0),
(43703, 43704, 0),
(43710, 43711, 0),
(43713, 43713, 0),
(43756, 43757, 0),
(43766, 43766, 0),
(44005, 44005, 0),
(44008, 44008, 0),
(44013, 44013, 0),
(44032, 55203, 2),
(63744, 64255, 2),
(64286, 64286, 0),
(65024, 65039, 0),
(65040, 65049, 2),
(65056, 65071, 0),
(65072, 65106, 2),
(65108, 65126, 2),
(65128, 65131, 2),
(65281, 65376, 2),
(65504, 65510, 2),
(66045, 66045, 0),
(66272, 66272, 0),
(66422, 66426, 0),
(68097, 68099, 0),
(68101, 68102, 0),
(68108, 68111, 0),
(68152, 68154, 0),
(68159, 68159, 0),
(68325, 68326, 0),
(69633, 69633, 0),
(69688, 69702, 0),
(69759, 69761, 0),
(69811, 69814, 0),
(69817, 69818, 0),
(69888, 69890, 0),
(69927, 69931, 0),
(69933, 69940, 0),
(70003, 70003, 0),
(70016, 70017, 0),
(70070, 70078, 0),
(70090, 70092, 0),
(70191, 70193, 0),
(70196, 70196, 0),
(70198, 70199, 0),
(70206, 70206, 0),
(70367, 70367, 0),
(70371, 70378, 0),
(70400, 70401, 0),
(70460, 70460, 0),
(70464, 70464, 0),
(70502, 70508, 0),
(70512, 70516, 0),
(70712, 70719, 0),
(70722, 70724, 0),
(70726, 70726, 0),
(70835, 70840, 0),
(70842, 70842, 0),
(70847, 70848, 0),
(70850, 70851, 0),
(71090, 71093, 0),
(71100, 71101, 0),
(71103, 71104, 0),
(71132, 71133, 0),
(71219, 71226, 0),
(71229, 71229, 0),
(71231, 71232, 0),
(71339, 71339, 0),
(71341, 71341, 0),
(71344, 71349, 0),
(71351, 71351, 0),
(71453, 71455, 0),
(71458, 71461, 0),
(71463, 71467, 0),
(72752, 72758, 0),
(72760, 72765, 0),
(72767, 72767, 0),
(72850, 72871, 0),
(72874, 72880, 0),
(72882, 72883, 0),
(72885, 72886, 0),
(92912, 92916, 0),
(92976, 92982, 0),
(94095, 94098, 0),
(94176, 94179, 2),
(94208, 100343, 2),
(100352, 101106, 2),
(110592, 110878, 2),
(110928, 110930, 2),
(110948, 110951, 2),
(110960, 111355, 2),
(113821, 113822, 0),
(119143, 119145, 0),
(119163, 119170, 0),
(119173, 119179, 0),
(119210, 119213, 0),
(119362, 119364, 0),
(121344, 121398, 0),
(121403, 121452, 0),
(121461, 121461, 0),
(121476, 121476, 0),
(121499, 121503, 0),
(121505, 121519, 0),
(122880, 122886, 0),
(122888, 122904, 0),
(122907, 122913, 0),
(122915, 122916, 0),
(122918, 122922, 0),
(125136, 125142, 0),
(125252, 125258, 0),
(126980, 126980, 2),
(127183, 127183, 2),
(127374, 127374, 2),
(127377, 127386, 2),
(127488, 127490, 2),
(127504, 127547, 2),
(127552, 127560, 2),
(127568, 127569, 2),
(127584, 127589, 2),
(127744, 127776, 2),
(127789, 127797, 2),
(127799, 127868, 2),
(127870, 127891, 2),
(127904, 127946, 2),
(127951, 127955, 2),
(127968, 127984, 2),
(127988, 127988, 2),
(127992, 128062, 2),
(128064, 128064, 2),
(128066, 128252, 2),
(128255, 128317, 2),
(128331, 128334, 2),
(128336, 128359, 2),
(128378, 128378, 2),
(128405, 128406, 2),
(128420, 128420, 2),
(128507, 128591, 2),
(128640, 128709, 2),
(128716, 128716, 2),
(128720, 128722, 2),
(128725, 128725, 2),
(128747, 128748, 2),
(128756, 128762, 2),
(128992, 129003, 2),
(129293, 129393, 2),
(129395, 129398, 2),
(129402, 129442, 2),
(129445, 129450, 2),
(129454, 129482, 2),
(129485, 129535, 2),
(129648, 129651, 2),
(129656, 129658, 2),
(129664, 129666, 2),
(129680, 129685, 2),
(131072, 196605, 2),
(196608, 262141, 2),
(917760, 917999, 0),
]

34
rich/_lru_cache.py Normal file
View file

@ -0,0 +1,34 @@
from collections import OrderedDict
from typing import Generic, TypeVar
CacheKey = TypeVar("CacheKey")
CacheValue = TypeVar("CacheValue")
class LRUCache(Generic[CacheKey, CacheValue], OrderedDict):
"""
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.
"""
def __init__(self, cache_size: int) -> None:
self.cache_size = cache_size
super(LRUCache, self).__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)
OrderedDict.__setitem__(self, key, value)
def __getitem__(self, key: CacheKey) -> CacheValue:
"""Gets the item, but also makes it most recent."""
value: CacheValue = OrderedDict.__getitem__(self, key)
OrderedDict.__delitem__(self, key)
OrderedDict.__setitem__(self, key, value)
return value

View file

@ -1,6 +1,9 @@
import re
from typing import Iterable, List, Tuple
from .cells import cell_len, chop_cells
from ._tools import iter_last
re_word = re.compile(r"\s*\S+\s*")
@ -18,14 +21,18 @@ def divide_line(text: str, width: int) -> List[int]:
divides: List[int] = []
append = divides.append
line_position = 0
for start, end, word in words(text):
if line_position + len(word.rstrip()) > width:
for start, _end, word in words(text):
if line_position + cell_len(word.rstrip()) > width:
if line_position and start:
append(start)
line_position = len(word)
line_position = cell_len(word)
else:
divides.extend(range(start or width, end + 1, width))
line_position = len(word) % width
for last, line in iter_last(chop_cells(text, width)):
if last:
line_position = cell_len(line)
else:
line_position += len(word)
start += len(line)
append(start)
else:
line_position += cell_len(word)
return divides

View file

@ -79,7 +79,6 @@ class Bar:
remaining_bars -= 1
if remaining_bars:
yield Segment(bar * remaining_bars, style)
yield Segment("\r")
def __measure__(self, console: Console, max_width: int) -> Measurement:
if self.width is not None:
@ -97,6 +96,7 @@ if __name__ == "__main__":
for n in range(0, 101, 1):
bar.update_progress(n)
console.print(bar)
console.file.write("\r")
time.sleep(0.05)
console.show_cursor(True)
console.print()

113
rich/cells.py Normal file
View file

@ -0,0 +1,113 @@
from functools import lru_cache
from itertools import takewhile
from typing import List, Tuple
from ._cell_widths import CELL_WIDTHS
from ._lru_cache import LRUCache
def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024)) -> int:
"""Get the number of cells required to display text.
Args:
text (str): Text to display.
Returns:
int: Number of cells required to display the text.
"""
cached_result = _cache.get(text, None)
if cached_result is not None:
return cached_result
_get_size = get_character_cell_size
total_size = sum(_get_size(character) for character in text)
if len(text) < 256:
_cache[text] = total_size
return total_size
@lru_cache(maxsize=5000)
def get_character_cell_size(character: str) -> int:
"""Get the cell size of a character.
Args:
character (str): A single character.
Returns:
int: Number of cells (0, 1 or 2) occupied by that character.
"""
codepoint = ord(character)
if 127 > codepoint > 31:
# Shortcut for ascii
return 1
_table = CELL_WIDTHS
lower_bound = 0
upper_bound = len(_table) - 1
index = (lower_bound + upper_bound) // 2
while True:
start, end, width = _table[index]
if codepoint < start:
upper_bound = index - 1
elif codepoint > end:
lower_bound = index + 1
else:
return 0 if width == -1 else width
if upper_bound < lower_bound:
break
index = (lower_bound + upper_bound) // 2
return 1
def set_cell_size(text: str, total: int) -> str:
"""Set the length of a string to fit within given number of cells."""
cell_size = cell_len(text)
if cell_size == total:
return text
if cell_size < total:
return text + " " * (total - cell_size)
_get_character_cell_size = get_character_cell_size
character_sizes = [_get_character_cell_size(character) for character in text]
excess = cell_size - total
pop = character_sizes.pop
while excess > 0:
excess -= pop()
text = text[: len(character_sizes)]
if excess == -1:
text += " "
return text
def chop_cells(text: str, max_size: int) -> List[str]:
"""Break text in to equal (cell) length strings."""
_get_character_cell_size = get_character_cell_size
characters = [
(character, _get_character_cell_size(character)) for character in text
][::-1]
total_size = 0
lines: List[List[str]] = [[]]
append = lines[-1].append
pop = characters.pop
while characters:
character, size = pop()
if total_size + size > max_size:
lines.append([character])
append = lines[-1].append
total_size = size
else:
total_size += size
append(character)
return ["".join(line) for line in lines]
if __name__ == "__main__":
print(get_character_cell_size("😽"))
for line in chop_cells("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", 8):
print(line)
for n in range(80, 1, -1):
print(set_cell_size("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", n) + "|")
print("x" * n)

11
rich/cjk.py Normal file
View file

@ -0,0 +1,11 @@
from rich.console import Console
from rich.panel import Panel
console = Console(width=16)
console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False))
console.print(Panel(""":pile_of_poo:""", expand=False))
console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 5))
print("x" * 15)

View file

@ -235,8 +235,9 @@ class Console:
height (int, optional): The height of the terminal. Leave as default to auto-detect height.
record (bool, optional): Boolean to enable recording of terminal output,
required to call :meth:`export_html` and :meth:`export_text`. Defaults to False.
emoji (Optional[bool], optional): Enable emoji code. Defaults to True.
markup (bool, optional): Boolean to enable :ref:`console_markup`. Defaults to True.
emoji (bool, optional): Enable emoji code. Defaults to True.
highlight (bool, optional): Enable automatic highlighting. Defaults to True.
log_time (bool, optional): Boolean to enable logging of time by :meth:`log` methods. Defaults to True.
log_path (bool, optional): Boolean to enable the logging of the caller by :meth:`log`. Defaults to True.
log_time_format (str, optional): Log time format if ``log_time`` is enabled. Defaults to "[%X] ".
@ -256,6 +257,7 @@ class Console:
record: bool = False,
markup: bool = True,
emoji: bool = True,
highlight: bool = True,
log_time: bool = True,
log_path: bool = True,
log_time_format: str = "[%X] ",
@ -271,6 +273,7 @@ class Console:
self.record = record
self._markup = markup
self._emoji = emoji
self._highlight = highlight
if color_system is None:
self._color_system = None
@ -319,9 +322,7 @@ class Console:
"""Detect color system from env vars."""
if not self.is_terminal:
return None
if WINDOWS:
return ColorSystem.WINDOWS
if os.environ.get("COLORTERM", "").strip().lower() == "truecolor":
if os.environ.get("COLORTERM", "").strip().lower() in ("truecolor", "24bit"):
return ColorSystem.TRUECOLOR
# 256 can be considered standard nowadays
return ColorSystem.EIGHT_BIT
@ -410,8 +411,6 @@ class Console:
return ConsoleDimensions(self._width, self._height)
width, height = shutil.get_terminal_size()
# Fixes Issue with Windows console (https://github.com/willmcgugan/rich/issues/7)
width -= 1
return ConsoleDimensions(
width if self._width is None else self._width,
height if self._height is None else self._height,
@ -445,10 +444,8 @@ class Console:
Args:
show (bool, optional): Set visibility of the cursor.
"""
if WINDOWS:
return
self._buffer.append(Segment("\033[?25h" if show else "\033[?25l"))
self._check_buffer()
self.file.write("\033[?25h" if show else "\033[?25l")
def _render(
self,
@ -642,17 +639,17 @@ class Console:
end: str,
emoji: bool = None,
markup: bool = None,
highlight: bool = True,
highlight: bool = None,
) -> List[ConsoleRenderable]:
"""Combined a number of renderables and text in to one renderable.
Args:
renderables (Iterable[Union[str, ConsoleRenderable]]): [description]
renderables (Iterable[Union[str, ConsoleRenderable]]): Anyting that Rich can render.
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n".
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
highlight (bool, optional): Perform highlighting. Defaults to True.
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default.
Returns:
List[ConsoleRenderable]: A list of things to render.
@ -664,7 +661,7 @@ class Console:
append_text = text.append
_highlighter: HighlighterType
if highlight:
if highlight or (highlight is None and self._highlight):
_highlighter = self.highlighter
else:
_highlighter = _null_highlighter
@ -723,7 +720,7 @@ class Console:
style: Union[str, Style] = None,
emoji: bool = None,
markup: bool = None,
highlight: bool = True,
highlight: bool = None,
) -> None:
r"""Print to the console.
@ -732,9 +729,9 @@ class Console:
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n".
style (Union[str, Style], optional): A style to apply to output. Defaults to None.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
highlight (bool, optional): Perform highlighting. Defaults to True.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
"""
if not objects:
self.line()
@ -783,7 +780,7 @@ class Console:
end="\n",
emoji: bool = None,
markup: bool = None,
highlight: bool = True,
highlight: bool = None,
log_locals: bool = False,
_stack_offset=1,
) -> None:
@ -793,9 +790,9 @@ class Console:
objects (positional args): Objects to log to the terminal.
sep (str, optional): String to write between print data. Defaults to " ".
end (str, optional): String to write at end of print data. Defaults to "\n".
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default.
highlight (bool, optional): Perform highlighting. Defaults to True.
emoji (Optional[bool], optional): Enable emoji code, or ``None`` to use console default. Defaults to None.
markup (Optional[bool], optional): Enable markup, or ``None`` to use console default. Defaults to None.
highlight (Optional[bool], optional): Enable automatic highlighting, or ``None`` to use console default. Defaults to None.
log_locals (bool, optional): Boolean to enable logging of locals where ``log()``
was called. Defaults to False.
_stack_offset (int, optional): Offset of caller from end of call stack. Defaults to 1.

View file

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
from collections import deque
from collections.abc import Sized
from contextlib import contextmanager
from dataclasses import dataclass, replace, field
from datetime import timedelta
@ -37,24 +38,41 @@ ProgressType = TypeVar("ProgressType")
def track(
sequence: Sequence[ProgressType], description="Working..."
sequence: Union[Sequence[ProgressType], Iterable[ProgressType]],
description="Working...",
total: int = None,
auto_refresh=True,
) -> Iterable[ProgressType]:
"""Track progress of processing a sequence.
Args:
sequence (Sequence[ProgressType]): A sequence (must support "len") you wish to iterate over.
description (str, optional): [description]. Defaults to "Working".
sequence (Iterable[ProgressType]): A sequence (must support "len") you wish to iterate over.
description (str, optional): Description of task show next to progress bar. Defaults to "Working".
total: (int, optional): Total number of steps. Default is len(sequence).
auto_refresh (bool, optional): Automatic refresh, disable to force a refresh after each iteration. Default is True.
Returns:
Iterable[ProgressType]: An iterable of the values in the sequence.
"""
progress = Progress(auto_refresh=False)
task_id = progress.add_task(description, total=len(sequence))
progress = Progress(auto_refresh=auto_refresh)
if total is None:
if isinstance(sequence, Sized):
task_total = len(sequence)
else:
raise ValueError(
f"unable to get size of {sequence!r}, please specify 'total'"
)
else:
task_total = total
task_id = progress.add_task(description, total=task_total)
with progress:
for completed, value in enumerate(sequence, 1):
yield value
progress.update(task_id, completed=completed)
if not auto_refresh:
progress.refresh()
@ -117,7 +135,7 @@ class TimeRemainingColumn(ProgressColumn):
"""Show time remaining."""
remaining = task.time_remaining
if remaining is None:
return Text("?", style="progress.remaining")
return Text("-:--:--", style="progress.remaining")
remaining_delta = timedelta(seconds=int(remaining))
return Text(str(remaining_delta), style="progress.remaining")
@ -278,6 +296,7 @@ class Progress:
self._lock = RLock()
self._refresh_thread: Optional[_RefreshThread] = None
self._refresh_count = 0
self._enter_count = 0
@property
def tasks_ids(self) -> List[TaskID]:
@ -313,29 +332,56 @@ class Progress:
self.console.show_cursor(True)
def __enter__(self) -> "Progress":
with self._lock:
if self._enter_count:
self._enter_count += 1
return self
self.start()
self._enter_count += 1
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
with self._lock:
self._enter_count -= 1
if not self._enter_count:
self.stop()
def track(
self, sequence: Sequence[ProgressType], description="Working..."
self,
sequence: Sequence[ProgressType],
total: int = None,
task_id: Optional[TaskID] = None,
description="Working...",
) -> Iterable[ProgressType]:
"""[summary]
Args:
sequence (Sequence[ProgressType]): [description]
total: (int, optional): Total number of steps. Default is len(sequence).
task_id: (TaskID): Task to track. Default is new task.
description: (str, optional): Description of task, if new task is created.
Returns:
Iterable[ProgressType]: [description]
"""
task_id = self.add_task(description, total=len(sequence))
if total is None:
if isinstance(sequence, Sized):
task_total = len(sequence)
else:
raise ValueError(
f"unable to get size of {sequence!r}, please specify 'total'"
)
else:
task_total = total
if task_id is None:
task_id = self.add_task(description, total=task_total)
else:
self.update(task_id, total=task_total)
with self:
for completed, value in enumerate(sequence, 1):
yield value
self.update(task_id, completed=completed)
self.refresh()
def start_task(self, task_id: TaskID) -> None:
"""Start a task.
@ -412,6 +458,15 @@ class Progress:
if refresh:
self.refresh()
def advance(self, task_id: TaskID, advance: float = 1) -> None:
"""Advance task by a number of steps.
Args:
task_id (TaskID): ID of task.
advance (float): Number of steps to advance. Default is 1.
"""
self.update(task_id, advance=advance)
def refresh(self) -> None:
"""Refresh (render) the progress information."""
with self._lock:

View file

@ -1,5 +1,6 @@
from typing import NamedTuple, Optional
from .cells import cell_len, set_cell_size
from .style import Style
from itertools import zip_longest
@ -104,7 +105,7 @@ class Segment(NamedTuple):
Returns:
List[Segment]: A line of segments with the desired length.
"""
line_length = sum(len(text) for text, _style in line)
line_length = sum(cell_len(text) for text, _style in line)
new_line: List[Segment]
if line_length < length:
@ -117,13 +118,14 @@ class Segment(NamedTuple):
append = new_line.append
line_length = 0
for segment in line:
segment_length = len(segment.text)
segment_length = cell_len(segment.text)
if line_length + segment_length < length:
append(segment)
line_length += segment_length
else:
text, style = segment
append(cls(text[: length - line_length], style))
text = set_cell_size(text, length - line_length)
append(cls(text, style))
break
else:
new_line = line[:]
@ -139,7 +141,7 @@ class Segment(NamedTuple):
Returns:
int: The length of the line.
"""
return sum(len(text) for text, _ in line)
return sum(cell_len(text) for text, _ in line)
@classmethod
def get_shape(cls, lines: List[List["Segment"]]) -> Tuple[int, int]:

View file

@ -73,6 +73,7 @@ class Syntax:
def from_path(
cls,
path: str,
encoding: str = "utf-8",
theme: Union[str, PygmentsStyle] = DEFAULT_THEME,
dedent: bool = True,
line_numbers: bool = False,
@ -86,6 +87,7 @@ class Syntax:
Args:
path (str): Path to file to highlight.
encoding (str): Encoding of file.
lexer_name (str): Lexer to use (see https://pygments.org/docs/lexers/)
theme (str, optional): Color theme, aka Pygments style (see https://pygments.org/docs/styles/#getting-a-list-of-available-styles). Defaults to "emacs".
dedent (bool, optional): Enable stripping of initial whitespace. Defaults to True.
@ -99,7 +101,7 @@ class Syntax:
Returns:
[Syntax]: A Syntax object that may be printed to the console
"""
with open(path, "rt") as code_file:
with open(path, "rt", encoding=encoding) as code_file:
code = code_file.read()
try:
lexer = guess_lexer_for_filename(path, code)
@ -242,7 +244,7 @@ class Syntax:
padding = _Segment(" " * numbers_column_width, background_style)
new_line = _Segment("\n")
line_pointer = "->" if WINDOWS else ""
line_pointer = ""
for line_no, line in enumerate(lines, self.start_line + line_offset):
wrapped_lines = console.render_lines(

View file

@ -461,8 +461,8 @@ class Table:
if __name__ == "__main__":
from .console import Console
c = Console(width=80)
table = Table()
c = Console()
table = Table(expand=True)
table.add_column(no_wrap=True)
table.add_column()
table.add_row(

View file

@ -23,6 +23,7 @@ if TYPE_CHECKING: # pragma: no cover
RenderableType,
)
from .cells import cell_len
from .containers import Lines
from .style import Style
from .segment import Segment
@ -322,9 +323,9 @@ class Text:
def __measure__(self, console: "Console", max_width: int) -> Measurement:
text = self.text
if not text.strip():
return Measurement(len(text), len(text))
max_text_width = max(len(line) for line in text.splitlines())
min_text_width = max(len(word) for word in text.split())
return Measurement(cell_len(text), cell_len(text))
max_text_width = max(cell_len(line) for line in text.splitlines())
min_text_width = max(cell_len(word) for word in text.split())
return Measurement(min_text_width, max_text_width)
def _render_line(

View file

@ -281,7 +281,7 @@ if __name__ == "__main__": # pragma: no cover
console = Console()
import sys
def bar(a):
def bar(a): # 这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑
print(1 / a)
def foo(a):

View file

@ -0,0 +1,91 @@
import subprocess
from typing import List, Tuple
import sys
from rich.progress import Progress
from wcwidth import wcwidth
progress = Progress()
def make_widths_table():
table: List[Tuple[int, int, int]] = []
append = table.append
make_table_task = progress.add_task("Calculating table...")
widths = (
(codepoint, wcwidth(chr(codepoint)))
for codepoint in range(0, sys.maxunicode + 1)
)
widths = [(codepoint, width) for codepoint, width in widths if width != 1]
iter_widths = iter(widths)
endpoint, group_cell_size = next(iter_widths)
start_codepoint = end_codepoint = endpoint
for codepoint, cell_size in progress.track(
iter_widths, task_id=make_table_task, total=len(widths) - 1
):
if cell_size != group_cell_size or codepoint != end_codepoint + 1:
append((start_codepoint, end_codepoint, group_cell_size))
start_codepoint = end_codepoint = codepoint
group_cell_size = cell_size
else:
end_codepoint = codepoint
append((start_codepoint, end_codepoint, group_cell_size))
return table
def get_cell_size(table: List[Tuple[int, int, int]], character: str) -> int:
codepoint = ord(character)
lower_bound = 0
upper_bound = len(table) - 1
index = (lower_bound + upper_bound) // 2
while True:
start, end, width = table[index]
if codepoint < start:
upper_bound = index - 1
elif codepoint > end:
lower_bound = index + 1
else:
return width
if upper_bound < lower_bound:
break
index = (lower_bound + upper_bound) // 2
return 1
def test(widths_table):
for codepoint in progress.track(
range(0, sys.maxunicode + 1), description="Testing..."
):
character = chr(codepoint)
width1 = get_cell_size(widths_table, character)
width2 = wcwidth(character)
if width1 != width2:
print(f"{width1} != {width2}")
break
def run():
with progress:
widths_table = make_widths_table()
test(widths_table)
table_file = f"""# Auto generated by make_terminal_widths.py
CELL_WIDTHS = {widths_table!r}
"""
with open("../rich/_cell_widths.py", "wt") as fh:
fh.write(table_file)
subprocess.run("black ../rich/_cell_widths.py", shell=True)
if __name__ == "__main__":
run()