diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 892dc791..3f95032c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,6 @@ repos: - id: check-ast - id: check-builtin-literals - id: check-case-conflict - - id: check-docstring-first - id: check-merge-conflict - id: check-json - id: check-toml diff --git a/pyproject.toml b/pyproject.toml index 42b60550..5b32ba3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ pygments = "^2.6.0" commonmark = "^0.9.0" colorama = "^0.4.0" ipywidgets = { version = "^7.5.1", optional = true } +cells = { path = "../cells/" } [tool.poetry.extras] diff --git a/rich/_wrap.py b/rich/_wrap.py index b537757a..7b738071 100644 --- a/rich/_wrap.py +++ b/rich/_wrap.py @@ -1,8 +1,10 @@ import re from typing import Iterable, List, Tuple -from .cells import cell_len, chop_cells +from cells.core import Cells + from ._loop import loop_last +from .cells import cell_len, chop_cells re_word = re.compile(r"\s*\S+\s*") @@ -17,13 +19,13 @@ def words(text: str) -> Iterable[Tuple[int, int, str]]: word_match = re_word.match(text, end) -def divide_line(text: str, width: int, fold: bool = True) -> List[int]: +def divide_line(text: str, width: int, cells: Cells, fold: bool = True) -> List[int]: divides: List[int] = [] append = divides.append line_position = 0 - _cell_len = cell_len + _cell_width = cells.measure for start, _end, word in words(text): - word_length = _cell_len(word.rstrip()) + word_length = _cell_width(word.rstrip()) if line_position + word_length > width: if word_length > width: if fold: @@ -31,19 +33,19 @@ def divide_line(text: str, width: int, fold: bool = True) -> List[int]: chop_cells(word, width, position=line_position) ): if last: - line_position = _cell_len(line) + line_position = _cell_width(line) else: start += len(line) append(start) else: if start: append(start) - line_position = _cell_len(word) + line_position = _cell_width(word) elif line_position and start: append(start) - line_position = _cell_len(word) + line_position = _cell_width(word) else: - line_position += _cell_len(word) + line_position += _cell_width(word) return divides diff --git a/rich/console.py b/rich/console.py index b6a66106..90ef4679 100644 --- a/rich/console.py +++ b/rich/console.py @@ -11,9 +11,9 @@ from getpass import getpass from html import escape from inspect import isclass from itertools import islice -from time import monotonic from threading import RLock -from types import FrameType, TracebackType, ModuleType +from time import monotonic +from types import FrameType, ModuleType, TracebackType from typing import ( IO, TYPE_CHECKING, @@ -32,6 +32,8 @@ from typing import ( cast, ) +from cells.core import Cells + if sys.version_info >= (3, 8): from typing import Literal, Protocol, runtime_checkable else: @@ -71,6 +73,25 @@ if TYPE_CHECKING: WINDOWS = platform.system() == "Windows" +DEFAULT_UNICODE_VERSION = "9.0.0" +UnicodeVersion = Literal[ + "4.1.0", + "5.0.0", + "5.1.0", + "5.2.0", + "6.0.0", + "6.1.0", + "6.2.0", + "6.3.0", + "7.0.0", + "8.0.0", + "9.0.0", + "10.0.0", + "11.0.0", + "12.0.0", + "12.1.0", + "13.0.0", +] HighlighterType = Callable[[Union[str, "Text"]], "Text"] JustifyMethod = Literal["default", "left", "center", "right", "full"] OverflowMethod = Literal["fold", "crop", "ellipsis", "ignore"] @@ -616,6 +637,10 @@ class Console: get_datetime (Callable[[], datetime], optional): Callable that gets the current time as a datetime.datetime object (used by Console.log), or None for datetime.now. get_time (Callable[[], time], optional): Callable that gets the current time in seconds, default uses time.monotonic. + unicode_version (UnicodeVersion, optional): Version of the Unicode database to use. + If ``None``, the default ``"9.0.0"`` will be used. + cells (Cells, optional): ``Cells`` instance to use for character cell width measurement. If ``None``, Rich will instantiate + ``Cell`` internally using the specified ``unicode_version``. """ _environ: Mapping[str, str] = os.environ @@ -652,6 +677,8 @@ class Console: safe_box: bool = True, get_datetime: Optional[Callable[[], datetime]] = None, get_time: Optional[Callable[[], float]] = None, + unicode_version: Optional[UnicodeVersion] = DEFAULT_UNICODE_VERSION, + cells: Optional[Cells] = None, _environ: Optional[Mapping[str, str]] = None, ): # Copy of os.environ allows us to replace it for testing @@ -713,6 +740,7 @@ class Console: self.safe_box = safe_box self.get_datetime = get_datetime or datetime.now self.get_time = get_time or monotonic + self.cells = cells or Cells(unicode_version or DEFAULT_UNICODE_VERSION) self.style = style self.no_color = ( no_color if no_color is not None else "NO_COLOR" in self._environ diff --git a/rich/containers.py b/rich/containers.py index e29cf368..ac493014 100644 --- a/rich/containers.py +++ b/rich/containers.py @@ -1,13 +1,13 @@ from itertools import zip_longest from typing import ( - Iterator, + TYPE_CHECKING, Iterable, + Iterator, List, Optional, + TypeVar, Union, overload, - TypeVar, - TYPE_CHECKING, ) if TYPE_CHECKING: @@ -21,7 +21,6 @@ if TYPE_CHECKING: ) from .text import Text -from .cells import cell_len from .measure import Measurement T = TypeVar("T") @@ -126,6 +125,7 @@ class Lines: """ from .text import Text + _cell_width = console.cells.measure if justify == "left": for line in self._lines: line.truncate(width, overflow=overflow, pad=True) @@ -133,19 +133,19 @@ class Lines: for line in self._lines: line.rstrip() line.truncate(width, overflow=overflow) - line.pad_left((width - cell_len(line.plain)) // 2) - line.pad_right(width - cell_len(line.plain)) + line.pad_left((width - _cell_width(line.plain)) // 2) + line.pad_right(width - _cell_width(line.plain)) elif justify == "right": for line in self._lines: line.rstrip() line.truncate(width, overflow=overflow) - line.pad_left(width - cell_len(line.plain)) + line.pad_left(width - _cell_width(line.plain)) elif justify == "full": for line_index, line in enumerate(self._lines): if line_index == len(self._lines) - 1: break words = line.split(" ") - words_size = sum(cell_len(word.plain) for word in words) + words_size = sum(_cell_width(word.plain) for word in words) num_spaces = len(words) - 1 spaces = [1 for _ in range(num_spaces)] index = 0 diff --git a/rich/pretty.py b/rich/pretty.py index 10cc2925..70b07fd6 100644 --- a/rich/pretty.py +++ b/rich/pretty.py @@ -1,28 +1,31 @@ import builtins +import dataclasses import os -from rich.repr import RichReprResult +import re import sys from array import array -from collections import Counter, defaultdict, deque, UserDict, UserList -import dataclasses +from collections import Counter, UserDict, UserList, defaultdict, deque from dataclasses import dataclass, fields, is_dataclass from inspect import isclass from itertools import islice -import re +from types import MappingProxyType from typing import ( - DefaultDict, TYPE_CHECKING, Any, Callable, + DefaultDict, Dict, Iterable, List, Optional, Set, - Union, Tuple, + Union, ) -from types import MappingProxyType + +from cells.core import Cells + +from rich.repr import RichReprResult try: import attr as _attr_module @@ -30,12 +33,10 @@ except ImportError: # pragma: no cover _attr_module = None # type: ignore -from .highlighter import ReprHighlighter from . import get_console from ._loop import loop_last from ._pick import pick_bool from .abc import RichRenderable -from .cells import cell_len from .highlighter import ReprHighlighter from .jupyter import JupyterMixin, JupyterRenderable from .measure import Measurement @@ -278,8 +279,11 @@ class Pretty(JupyterMixin): max_length=self.max_length, max_string=self.max_string, ) + _cell_width = console.cells.measure text_width = ( - max(cell_len(line) for line in pretty_str.splitlines()) if pretty_str else 0 + max(_cell_width(line) for line in pretty_str.splitlines()) + if pretty_str + else 0 ) return Measurement(text_width, text_width) @@ -339,6 +343,10 @@ class Node: children: Optional[List["Node"]] = None key_separator = ": " separator: str = ", " + cells: Optional[Cells] = None + + def __post_init__(self): + self.cells = self.cells or Cells() def iter_tokens(self) -> Iterable[str]: """Generate tokens for this node.""" @@ -373,8 +381,9 @@ class Node: bool: True if the node can be rendered within max length, otherwise False. """ total_length = start_length + _cell_width = self.cells.measure for token in self.iter_tokens(): - total_length += cell_len(token) + total_length += _cell_width(token) if total_length > max_length: return False return True @@ -401,7 +410,7 @@ class Node: while line_no < len(lines): line = lines[line_no] if line.expandable and not line.expanded: - if expand_all or not line.check_length(max_width): + if expand_all or not line.check_length(max_width, self.cells): lines[line_no : line_no + 1] = line.expand(indent_size) line_no += 1 @@ -427,10 +436,11 @@ class _Line: """Check if the line may be expanded.""" return bool(self.node is not None and self.node.children) - def check_length(self, max_length: int) -> bool: + def check_length(self, max_length: int, cells: Cells) -> bool: """Check this line fits within a given number of cells.""" + _cell_width = cells.measure start_length = ( - len(self.whitespace) + cell_len(self.text) + cell_len(self.suffix) + len(self.whitespace) + _cell_width(self.text) + _cell_width(self.suffix) ) assert self.node is not None return self.node.check_length(start_length, max_length) diff --git a/rich/text.py b/rich/text.py index d9ac2c0d..1672468a 100644 --- a/rich/text.py +++ b/rich/text.py @@ -2,7 +2,6 @@ import re from functools import partial, reduce from math import gcd from operator import itemgetter -from rich.emoji import EmojiVariant from typing import ( TYPE_CHECKING, Any, @@ -16,6 +15,8 @@ from typing import ( Union, ) +from rich.emoji import EmojiVariant + from ._loop import loop_last from ._pick import pick_bool from ._wrap import divide_line @@ -1148,6 +1149,7 @@ class Text(JupyterMixin): no_wrap = pick_bool(no_wrap, self.no_wrap, False) or overflow == "ignore" + cells = console.cells lines = Lines() for line in self.split(allow_blank=True): if "\t" in line: @@ -1155,7 +1157,9 @@ class Text(JupyterMixin): if no_wrap: new_lines = Lines([line]) else: - offsets = divide_line(str(line), width, fold=wrap_overflow == "fold") + offsets = divide_line( + str(line), width, cells, fold=wrap_overflow == "fold" + ) new_lines = line.divide(offsets) for line in new_lines: line.rstrip_end(width)