From 82a11b9960489143225f161a3d7f34bea4937635 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 15 Mar 2020 15:45:45 +0000 Subject: [PATCH 01/14] replace cwwidth --- .gitignore | 1 + rich/progress.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index df23e0ae..acbe050d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ mypy_report docs/build docs/source/_build +tools/*.txt # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/rich/progress.py b/rich/progress.py index 5d0c9069..1fd95db7 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -412,6 +412,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: From d968a7732fb6d2ccf505d6c2543f61fb9593b71f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 15 Mar 2020 15:46:03 +0000 Subject: [PATCH 02/14] cjk test --- rich/cjk.py | 17 ++++++++++ tools/make_terminal_widths.py | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 rich/cjk.py create mode 100644 tools/make_terminal_widths.py diff --git a/rich/cjk.py b/rich/cjk.py new file mode 100644 index 00000000..bd4a4686 --- /dev/null +++ b/rich/cjk.py @@ -0,0 +1,17 @@ +from rich.console import Console +from rich.panel import Panel + +console = Console(width=15) + +# console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False,)) + +# console.print(Panel("""Hello, World!""", width=8, expand=False)) + +console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 20)) +print("x" * 15) + +# console = Console(width=15) +# from rich.text import Text + +# console.print("[u][b]Where[/b] there is a [i]Will[/i], there is a Way.[/u]") +# print("x" * 15) diff --git a/tools/make_terminal_widths.py b/tools/make_terminal_widths.py new file mode 100644 index 00000000..6270b0e5 --- /dev/null +++ b/tools/make_terminal_widths.py @@ -0,0 +1,64 @@ +from functools import partial +from typing import List +import os.path +from urllib.request import urlopen + +from rich.progress import Progress + + +def download(url: str) -> str: + """Copy data from a url to a local file.""" + + # This will break if the response doesn't contain content length + filename = url.rsplit("/")[-1] + if os.path.exists(filename): + print(f"{filename} exists") + return filename + progress = Progress() + task = progress.add_task(filename) + with progress: + response = urlopen(url) + progress.update(task, total=int(response.info()["Content-length"])) + with open(filename, "wb") as dest_file: + for data in iter(partial(response.read, 32768), b""): + dest_file.write(data) + progress.advance(task, len(data)) + return filename + + +def get_data(): + east_asian_filename = download( + "http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt" + ) + download( + "http://www.unicode.org/Public/UNIDATA/extracted/DerivedGeneralCategory.txt" + ) + print(parse_east_asian(east_asian_filename)) + + +def parse_east_asian(filename: str) -> List[int]: + codepoints: List[int] = [] + for line in open(filename, "rt"): + if line.startswith("#") or not line.strip(): + continue + print(line) + first_field = line.split()[0] + if ";" not in first_field: + continue + codepoint_range, details = first_field.split(";", 1) + if ".." in codepoint_range: + start, end = codepoint_range.split("..") + codepoints.extend(range(int(start, 16), int(end, 16) + 1)) + else: + codepoints.append(int(codepoint_range, 16)) + + return codepoints + + +def run(): + get_data() + + +if __name__ == "__main__": + run() + From 33d0c9796f1994affc3f2b1d232d2364b14b9f49 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:18:22 +0000 Subject: [PATCH 03/14] cjk support --- CHANGELOG.md | 10 + rich/_cell_widths.py | 407 ++++++++++++++++++++++++++++++++++ rich/_wrap.py | 19 +- rich/bar.py | 5 +- rich/cells.py | 99 +++++++++ rich/cjk.py | 14 +- rich/console.py | 2 - rich/progress.py | 77 +++++-- rich/segment.py | 10 +- rich/table.py | 4 +- rich/text.py | 7 +- rich/traceback.py | 2 +- tools/make_terminal_widths.py | 119 ++++++---- 13 files changed, 687 insertions(+), 88 deletions(-) create mode 100644 rich/_cell_widths.py create mode 100644 rich/cells.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a29b310..50e0b27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ 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] - Unreleased + +### Added + +- CJK support + +### Changed + +- Added task_id to Progress.track + ## [0.7.2] - 2020-03-15 ### Fixed diff --git a/rich/_cell_widths.py b/rich/_cell_widths.py new file mode 100644 index 00000000..e1080d8f --- /dev/null +++ b/rich/_cell_widths.py @@ -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), +] diff --git a/rich/_wrap.py b/rich/_wrap.py index f36edc26..bbfccda4 100644 --- a/rich/_wrap.py +++ b/rich/_wrap.py @@ -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: + start += len(line) + append(start) else: - line_position += len(word) + line_position += cell_len(word) return divides diff --git a/rich/bar.py b/rich/bar.py index b0636e1b..140c1ff5 100644 --- a/rich/bar.py +++ b/rich/bar.py @@ -26,6 +26,7 @@ class Bar: style: StyleType = "bar.back", complete_style: StyleType = "bar.complete", finished_style: StyleType = "bar.finished", + carriage_return: bool = True, ): self.total = total self.completed = completed @@ -33,6 +34,7 @@ class Bar: self.style = style self.complete_style = complete_style self.finished_style = finished_style + self.carriage_return = carriage_return def __repr__(self) -> str: return f"" @@ -79,7 +81,8 @@ class Bar: remaining_bars -= 1 if remaining_bars: yield Segment(bar * remaining_bars, style) - yield Segment("\r") + if self.carriage_return: + yield Segment("\r") def __measure__(self, console: Console, max_width: int) -> Measurement: if self.width is not None: diff --git a/rich/cells.py b/rich/cells.py new file mode 100644 index 00000000..081e4e41 --- /dev/null +++ b/rich/cells.py @@ -0,0 +1,99 @@ +from functools import lru_cache +from itertools import takewhile +from typing import List, Tuple + +from ._cell_widths import CELL_WIDTHS + + +def cell_len(text: str) -> 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. + """ + _get_size = get_character_cell_size + return sum(_get_size(character) for character in text) + + +@lru_cache(maxsize=5000) +def get_character_cell_size(character: str, _table=CELL_WIDTHS) -> 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. + """ + assert len(character) == 1, "'character' should have a length of 1" + + 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 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]: + _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]] = [[]] + + pop = characters.pop + while characters: + character, size = characters.pop() + if total_size + size > max_size: + lines.append([character]) + total_size = size + else: + total_size += size + lines[-1].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) + diff --git a/rich/cjk.py b/rich/cjk.py index bd4a4686..35468ca0 100644 --- a/rich/cjk.py +++ b/rich/cjk.py @@ -1,17 +1,11 @@ from rich.console import Console from rich.panel import Panel -console = Console(width=15) +console = Console(width=16) -# console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False,)) +console.print(Panel("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", expand=False)) -# console.print(Panel("""Hello, World!""", width=8, expand=False)) +console.print(Panel(""":pile_of_poo:""", expand=False)) -console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 20)) +console.print(Panel(""":pile_of_poo::vampire::thumbs_up: """ * 5)) print("x" * 15) - -# console = Console(width=15) -# from rich.text import Text - -# console.print("[u][b]Where[/b] there is a [i]Will[/i], there is a Way.[/u]") -# print("x" * 15) diff --git a/rich/console.py b/rich/console.py index bcbac906..aeb228dd 100644 --- a/rich/console.py +++ b/rich/console.py @@ -410,8 +410,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, diff --git a/rich/progress.py b/rich/progress.py index 1fd95db7..b49b0f9b 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -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,25 +38,42 @@ 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) - progress.refresh() + if not auto_refresh: + progress.refresh() class ProgressColumn(ABC): @@ -104,7 +122,12 @@ class BarColumn(ProgressColumn): def render(self, task: "Task") -> Bar: """Gets a progress bar widget for a task.""" - return Bar(total=task.total, completed=task.completed, width=self.bar_width) + return Bar( + total=task.total, + completed=task.completed, + width=self.bar_width, + carriage_return=False, + ) class TimeRemainingColumn(ProgressColumn): @@ -278,6 +301,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 +337,56 @@ class Progress: self.console.show_cursor(True) def __enter__(self) -> "Progress": - self.start() - return self + 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: - self.stop() + 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. diff --git a/rich/segment.py b/rich/segment.py index a36390a8..0a687f44 100644 --- a/rich/segment.py +++ b/rich/segment.py @@ -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]: diff --git a/rich/table.py b/rich/table.py index 49469af1..2c0967c2 100644 --- a/rich/table.py +++ b/rich/table.py @@ -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( diff --git a/rich/text.py b/rich/text.py index 6ac2d925..8a8954ab 100644 --- a/rich/text.py +++ b/rich/text.py @@ -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( diff --git a/rich/traceback.py b/rich/traceback.py index 6921934d..dc328b0b 100644 --- a/rich/traceback.py +++ b/rich/traceback.py @@ -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): diff --git a/tools/make_terminal_widths.py b/tools/make_terminal_widths.py index 6270b0e5..86e84b03 100644 --- a/tools/make_terminal_widths.py +++ b/tools/make_terminal_widths.py @@ -1,62 +1,89 @@ -from functools import partial -from typing import List -import os.path -from urllib.request import urlopen +import subprocess +from typing import List, Tuple +import sys from rich.progress import Progress - -def download(url: str) -> str: - """Copy data from a url to a local file.""" - - # This will break if the response doesn't contain content length - filename = url.rsplit("/")[-1] - if os.path.exists(filename): - print(f"{filename} exists") - return filename - progress = Progress() - task = progress.add_task(filename) - with progress: - response = urlopen(url) - progress.update(task, total=int(response.info()["Content-length"])) - with open(filename, "wb") as dest_file: - for data in iter(partial(response.read, 32768), b""): - dest_file.write(data) - progress.advance(task, len(data)) - return filename +from wcwidth import wcwidth -def get_data(): - east_asian_filename = download( - "http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt" +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) ) - download( - "http://www.unicode.org/Public/UNIDATA/extracted/DerivedGeneralCategory.txt" - ) - print(parse_east_asian(east_asian_filename)) + widths = [(codepoint, width) for codepoint, width in widths if width != 1] + iter_widths = iter(widths) -def parse_east_asian(filename: str) -> List[int]: - codepoints: List[int] = [] - for line in open(filename, "rt"): - if line.startswith("#") or not line.strip(): - continue - print(line) - first_field = line.split()[0] - if ";" not in first_field: - continue - codepoint_range, details = first_field.split(";", 1) - if ".." in codepoint_range: - start, end = codepoint_range.split("..") - codepoints.extend(range(int(start, 16), int(end, 16) + 1)) + 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: - codepoints.append(int(codepoint_range, 16)) + end_codepoint = codepoint + append((start_codepoint, end_codepoint, group_cell_size)) + return table - return codepoints + +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(): - get_data() + 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__": From 8d9ef6c9af5bcd761de395a88e5f6c2845df474d Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:20:19 +0000 Subject: [PATCH 04/14] reneable test --- rich/cells.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rich/cells.py b/rich/cells.py index 081e4e41..68238e91 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -93,7 +93,7 @@ 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) + for n in range(80, 1, -1): + print(set_cell_size("""这是对亚洲语言支持的测试。面对模棱两可的想法,拒绝猜测的诱惑。""", n) + "|") + print("x" * n) From f9db65217572c8f3134d529ee5723746078a3da8 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:26:48 +0000 Subject: [PATCH 05/14] version bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0ec1110c..448218b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 "] license = "MIT" From 08821053743c55e49ce790d41973261e6091da34 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:33:27 +0000 Subject: [PATCH 06/14] Shortcut for ascii chars --- rich/cells.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rich/cells.py b/rich/cells.py index 68238e91..2e650694 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -31,6 +31,11 @@ def get_character_cell_size(character: str, _table=CELL_WIDTHS) -> int: assert len(character) == 1, "'character' should have a length of 1" codepoint = ord(character) + + if 127 > codepoint > 31: + # Shortcut for ascii + return 1 + lower_bound = 0 upper_bound = len(_table) - 1 index = (lower_bound + upper_bound) // 2 From 836ce01aecc78824c805ba5e438000c12472ee35 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:34:43 +0000 Subject: [PATCH 07/14] simplify signature --- rich/cells.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rich/cells.py b/rich/cells.py index 2e650694..44d59ae1 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -19,7 +19,7 @@ def cell_len(text: str) -> int: @lru_cache(maxsize=5000) -def get_character_cell_size(character: str, _table=CELL_WIDTHS) -> int: +def get_character_cell_size(character: str) -> int: """Get the cell size of a character. Args: @@ -36,6 +36,7 @@ def get_character_cell_size(character: str, _table=CELL_WIDTHS) -> int: # Shortcut for ascii return 1 + _table = CELL_WIDTHS lower_bound = 0 upper_bound = len(_table) - 1 index = (lower_bound + upper_bound) // 2 From e7dc2553eef9ec26d81427ab05c7aa5699346daf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:37:12 +0000 Subject: [PATCH 08/14] optimization --- rich/cells.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rich/cells.py b/rich/cells.py index 44d59ae1..a208298b 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -75,6 +75,7 @@ def set_cell_size(text: str, total: int) -> str: 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 @@ -84,7 +85,7 @@ def chop_cells(text: str, max_size: int) -> List[str]: pop = characters.pop while characters: - character, size = characters.pop() + character, size = pop() if total_size + size > max_size: lines.append([character]) total_size = size From 8c8ee763937ad41e7d833d7fbaf5aaae22eb9df4 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 16 Mar 2020 22:40:17 +0000 Subject: [PATCH 09/14] optimization --- rich/cells.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rich/cells.py b/rich/cells.py index a208298b..d7ea1e61 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -28,10 +28,8 @@ def get_character_cell_size(character: str) -> int: Returns: int: Number of cells (0, 1 or 2) occupied by that character. """ - assert len(character) == 1, "'character' should have a length of 1" codepoint = ord(character) - if 127 > codepoint > 31: # Shortcut for ascii return 1 @@ -82,16 +80,18 @@ def chop_cells(text: str, max_size: int) -> List[str]: ][::-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 - lines[-1].append(character) + append(character) return ["".join(line) for line in lines] From f12c2be6e9514a59bbbeba0a249084a971d07e83 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Mar 2020 10:38:30 +0000 Subject: [PATCH 10/14] simplify bar --- rich/bar.py | 5 +---- rich/progress.py | 7 +------ 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/rich/bar.py b/rich/bar.py index 140c1ff5..28ae54e8 100644 --- a/rich/bar.py +++ b/rich/bar.py @@ -26,7 +26,6 @@ class Bar: style: StyleType = "bar.back", complete_style: StyleType = "bar.complete", finished_style: StyleType = "bar.finished", - carriage_return: bool = True, ): self.total = total self.completed = completed @@ -34,7 +33,6 @@ class Bar: self.style = style self.complete_style = complete_style self.finished_style = finished_style - self.carriage_return = carriage_return def __repr__(self) -> str: return f"" @@ -81,8 +79,6 @@ class Bar: remaining_bars -= 1 if remaining_bars: yield Segment(bar * remaining_bars, style) - if self.carriage_return: - yield Segment("\r") def __measure__(self, console: Console, max_width: int) -> Measurement: if self.width is not None: @@ -100,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() diff --git a/rich/progress.py b/rich/progress.py index b49b0f9b..e70d4881 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -122,12 +122,7 @@ class BarColumn(ProgressColumn): def render(self, task: "Task") -> Bar: """Gets a progress bar widget for a task.""" - return Bar( - total=task.total, - completed=task.completed, - width=self.bar_width, - carriage_return=False, - ) + return Bar(total=task.total, completed=task.completed, width=self.bar_width) class TimeRemainingColumn(ProgressColumn): From 2753519e996921b0997581366d0bf5f3ef3f4348 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Mar 2020 16:38:45 +0000 Subject: [PATCH 11/14] added highlight switch, added cache to cell_len --- CHANGELOG.md | 1 + rich/cells.py | 11 +++++++++-- rich/console.py | 35 ++++++++++++++++++----------------- rich/progress.py | 2 +- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50e0b27f..d5c5741e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - CJK support +- Console level highlight flag ### Changed diff --git a/rich/cells.py b/rich/cells.py index d7ea1e61..96897648 100644 --- a/rich/cells.py +++ b/rich/cells.py @@ -3,9 +3,10 @@ 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) -> int: +def cell_len(text: str, _cache: LRUCache[str, int] = LRUCache(1024)) -> int: """Get the number of cells required to display text. Args: @@ -14,8 +15,14 @@ def cell_len(text: str) -> int: 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 - return sum(_get_size(character) for character in text) + 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) diff --git a/rich/console.py b/rich/console.py index aeb228dd..8d3f2e1c 100644 --- a/rich/console.py +++ b/rich/console.py @@ -234,9 +234,10 @@ class Console: width (int, optional): The width of the terminal. Leave as default to auto-detect width. 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. + required to call :meth:`export_html` and :meth:`export_text`. Defaults to False. 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 @@ -443,10 +446,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, @@ -640,17 +641,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. @@ -662,7 +663,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 @@ -721,7 +722,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. @@ -730,9 +731,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() @@ -781,7 +782,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: @@ -791,9 +792,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. diff --git a/rich/progress.py b/rich/progress.py index e70d4881..c9388936 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -135,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") From 3527a653a649b6773ab82b9dce9a5855661e5522 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Mar 2020 16:39:07 +0000 Subject: [PATCH 12/14] lru cache --- rich/_lru_cache.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 rich/_lru_cache.py diff --git a/rich/_lru_cache.py b/rich/_lru_cache.py new file mode 100644 index 00000000..e10c2ece --- /dev/null +++ b/rich/_lru_cache.py @@ -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 From 6a21d3b2bc86ded6ad5ec0cdd93eaa61e25316af Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Mar 2020 17:07:00 +0000 Subject: [PATCH 13/14] dropped windows terminal support. --- CHANGELOG.md | 2 ++ pyproject.toml | 1 - rich/__init__.py | 4 ---- rich/console.py | 4 +--- rich/syntax.py | 6 ++++-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c5741e..41305dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 diff --git a/pyproject.toml b/pyproject.toml index 448218b6..09ec5d2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/rich/__init__.py b/rich/__init__.py index 6f750b22..2c7ac3db 100644 --- a/rich/__init__.py +++ b/rich/__init__.py @@ -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**") diff --git a/rich/console.py b/rich/console.py index 8d3f2e1c..a4a91271 100644 --- a/rich/console.py +++ b/rich/console.py @@ -322,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 diff --git a/rich/syntax.py b/rich/syntax.py index 2c97f0c2..ee815fd0 100644 --- a/rich/syntax.py +++ b/rich/syntax.py @@ -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( From 9585a66fbd63b733b2e7d89fe1e3e1281ae69a04 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 17 Mar 2020 17:12:08 +0000 Subject: [PATCH 14/14] version bump --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41305dbc..d6894ffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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] - Unreleased +## [0.8.0] - 2020-03-17 ### Added