mirror of https://github.com/Textualize/rich.git
Merge pull request #368 from willmcgugan/safe-box-encoding
Safe box encoding
This commit is contained in:
commit
b0a68d3341
|
@ -8,14 +8,15 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
python-version: [3.6, 3.7, 3.8]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
architecture: x64
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -5,15 +5,22 @@ 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).
|
||||
|
||||
## [8.1.0] - unreleased
|
||||
## [9.0.0] - unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Progress download column now displays decimal units
|
||||
|
||||
### Added
|
||||
|
||||
- Added legacy_windows to ConsoleOptions
|
||||
- Added ascii_only to ConsoleOptions
|
||||
- Addded box.SQUARE_DOUBLE_HEAD
|
||||
- Added highlighting of EUI-48 and EUI-64 (MAC addresses)
|
||||
|
||||
### Changed
|
||||
|
||||
- Change the render prefix to correspond to the decimal units in progress
|
||||
- Dropped box.get_safe_box function in favor of Box.substitute
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -146,7 +146,7 @@ You can also set overflow to "ignore" which allows text to run on to the next li
|
|||
Soft Wrapping
|
||||
-------------
|
||||
|
||||
Rich word wraps text you print by inserting line breaks. You can disable this behavior by setting ``soft_wrap=True`` when calling :meth:`~rich.console.Console.print`. With *soft wrapping* enabled text any text that doesn't fit will run on to the following line(s), just like the builtin ``print``.
|
||||
Rich word wraps text you print by inserting line breaks. You can disable this behavior by setting ``soft_wrap=True`` when calling :meth:`~rich.console.Console.print`. With *soft wrapping* enabled any text that doesn't fit will run on to the following line(s), just like the builtin ``print``.
|
||||
|
||||
|
||||
Cropping
|
||||
|
|
|
@ -17,6 +17,7 @@ classifiers = [
|
|||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
]
|
||||
include = ["rich/py.typed"]
|
||||
|
||||
|
|
26
rich/bar.py
26
rich/bar.py
|
@ -1,7 +1,7 @@
|
|||
from functools import lru_cache
|
||||
import math
|
||||
from functools import lru_cache
|
||||
from time import monotonic
|
||||
from typing import Iterable, Optional, List
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from .color import Color, blend_rgb
|
||||
from .color_triplet import ColorTriplet
|
||||
|
@ -11,7 +11,6 @@ from .measure import Measurement
|
|||
from .segment import Segment
|
||||
from .style import Style, StyleType
|
||||
|
||||
|
||||
# Number of characters before 'pulse' animation repeats
|
||||
PULSE_SIZE = 20
|
||||
|
||||
|
@ -71,14 +70,14 @@ class Bar(JupyterMixin):
|
|||
fore_style: Style,
|
||||
back_style: Style,
|
||||
color_system: str,
|
||||
legacy_windows: bool,
|
||||
ascii: bool = False,
|
||||
) -> List[Segment]:
|
||||
"""Get a list of segments to render a pulse animation.
|
||||
|
||||
Returns:
|
||||
List[Segment]: A list of segments, one segment per character.
|
||||
"""
|
||||
bar = "─" if legacy_windows else "━"
|
||||
bar = "-" if ascii else "━"
|
||||
segments: List[Segment] = []
|
||||
|
||||
if color_system != "truecolor":
|
||||
|
@ -122,7 +121,9 @@ class Bar(JupyterMixin):
|
|||
self.completed = completed
|
||||
self.total = total if total is not None else self.total
|
||||
|
||||
def _render_pulse(self, console: Console, width: int) -> Iterable[Segment]:
|
||||
def _render_pulse(
|
||||
self, console: Console, width: int, ascii: bool = False
|
||||
) -> Iterable[Segment]:
|
||||
"""Renders the pulse animation.
|
||||
|
||||
Args:
|
||||
|
@ -139,7 +140,7 @@ class Bar(JupyterMixin):
|
|||
back_style = console.get_style(self.style, default="black")
|
||||
|
||||
pulse_segments = self._get_pulse_segments(
|
||||
fore_style, back_style, console.color_system, console.legacy_windows
|
||||
fore_style, back_style, console.color_system, ascii=ascii
|
||||
)
|
||||
segment_count = len(pulse_segments)
|
||||
current_time = (
|
||||
|
@ -155,15 +156,16 @@ class Bar(JupyterMixin):
|
|||
) -> RenderResult:
|
||||
|
||||
width = min(self.width or options.max_width, options.max_width)
|
||||
ascii = options.legacy_windows or options.ascii_only
|
||||
if self.pulse:
|
||||
yield from self._render_pulse(console, width)
|
||||
yield from self._render_pulse(console, width, ascii=ascii)
|
||||
return
|
||||
|
||||
completed = min(self.total, max(0, self.completed))
|
||||
legacy_windows = console.legacy_windows
|
||||
bar = "─" if legacy_windows else "━"
|
||||
half_bar_right = " " if legacy_windows else "╸"
|
||||
half_bar_left = " " if legacy_windows else "╺"
|
||||
|
||||
bar = "-" if ascii else "━"
|
||||
half_bar_right = " " if ascii else "╸"
|
||||
half_bar_left = " " if ascii else "╺"
|
||||
complete_halves = (
|
||||
int(width * 2 * completed / self.total) if self.total else width * 2
|
||||
)
|
||||
|
|
82
rich/box.py
82
rich/box.py
|
@ -1,8 +1,12 @@
|
|||
from typing import Iterable, List, Optional, overload
|
||||
from typing import TYPE_CHECKING, Iterable, List
|
||||
|
||||
from typing_extensions import Literal
|
||||
|
||||
from ._loop import loop_last
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from rich.console import ConsoleOptions
|
||||
|
||||
|
||||
class Box:
|
||||
"""Defines characters to render boxes.
|
||||
|
@ -16,11 +20,14 @@ class Box:
|
|||
│ ││ foot
|
||||
└─┴┘ bottom
|
||||
|
||||
|
||||
Args:
|
||||
box (str): Characters making up box.
|
||||
ascii (bool, optional): True if this box uses ascii characters only. Default is False.
|
||||
"""
|
||||
|
||||
def __init__(self, box: str) -> None:
|
||||
def __init__(self, box: str, *, ascii: bool = False) -> None:
|
||||
self._box = box
|
||||
self.ascii = ascii
|
||||
line1, line2, line3, line4, line5, line6, line7, line8 = box.splitlines()
|
||||
# top
|
||||
self.top_left, self.top, self.top_divider, self.top_right = iter(line1)
|
||||
|
@ -58,6 +65,24 @@ class Box:
|
|||
def __str__(self) -> str:
|
||||
return self._box
|
||||
|
||||
def substitute(self, options: "ConsoleOptions", safe: bool = True) -> "Box":
|
||||
"""Substitute this box for another if it won't render due to platform issues.
|
||||
|
||||
Args:
|
||||
options (ConsoleOptions): Console options used in rendering.
|
||||
safe (bool, optional): Substitute this for another Box if there are known problems
|
||||
displaying on the platform (currently only relevant on Windows). Default is True.
|
||||
|
||||
Returns:
|
||||
Box: A different Box or the same Box.
|
||||
"""
|
||||
box = self
|
||||
if options.legacy_windows and safe:
|
||||
box = LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)
|
||||
if options.ascii_only:
|
||||
box = ASCII
|
||||
return box
|
||||
|
||||
def get_top(self, widths: Iterable[int]) -> str:
|
||||
"""Get the top of a simple box.
|
||||
|
||||
|
@ -158,7 +183,8 @@ ASCII: Box = Box(
|
|||
|-+|
|
||||
| ||
|
||||
+--+
|
||||
"""
|
||||
""",
|
||||
ascii=True,
|
||||
)
|
||||
|
||||
ASCII2: Box = Box(
|
||||
|
@ -171,7 +197,8 @@ ASCII2: Box = Box(
|
|||
+-++
|
||||
| ||
|
||||
+-++
|
||||
"""
|
||||
""",
|
||||
ascii=True,
|
||||
)
|
||||
|
||||
ASCII_DOUBLE_HEAD: Box = Box(
|
||||
|
@ -184,7 +211,8 @@ ASCII_DOUBLE_HEAD: Box = Box(
|
|||
+-++
|
||||
| ||
|
||||
+-++
|
||||
"""
|
||||
""",
|
||||
ascii=True,
|
||||
)
|
||||
|
||||
SQUARE: Box = Box(
|
||||
|
@ -200,6 +228,18 @@ SQUARE: Box = Box(
|
|||
"""
|
||||
)
|
||||
|
||||
SQUARE_DOUBLE_HEAD: Box = Box(
|
||||
"""\
|
||||
┌─┬┐
|
||||
│ ││
|
||||
╞═╪╡
|
||||
│ ││
|
||||
├─┼┤
|
||||
├─┼┤
|
||||
│ ││
|
||||
└─┴┘
|
||||
"""
|
||||
)
|
||||
|
||||
MINIMAL: Box = Box(
|
||||
"""\
|
||||
|
@ -385,40 +425,15 @@ LEGACY_WINDOWS_SUBSTITUTIONS = {
|
|||
}
|
||||
|
||||
|
||||
@overload
|
||||
def get_safe_box(box: None, legacy_windows: bool) -> None:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def get_safe_box(box: Box, legacy_windows: bool) -> Box:
|
||||
...
|
||||
|
||||
|
||||
def get_safe_box(box: Optional[Box], legacy_windows: bool) -> Optional[Box]:
|
||||
"""Substitute Box constants that don't render on windows legacy.
|
||||
|
||||
Args:
|
||||
box (Optional[Box]): A Box instance.
|
||||
legacy_windows (bool): Enable legacy Windows.
|
||||
|
||||
Returns:
|
||||
Optional[Box]: A Box instance (potentially a new instance).
|
||||
"""
|
||||
if legacy_windows and box is not None:
|
||||
return LEGACY_WINDOWS_SUBSTITUTIONS.get(box, box)
|
||||
else:
|
||||
return box
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
from rich.columns import Columns
|
||||
from rich.panel import Panel
|
||||
|
||||
from . import box
|
||||
from .console import Console
|
||||
from .table import Table
|
||||
from .text import Text
|
||||
from . import box
|
||||
|
||||
console = Console(record=True)
|
||||
|
||||
|
@ -427,6 +442,7 @@ if __name__ == "__main__": # pragma: no cover
|
|||
"ASCII2",
|
||||
"ASCII_DOUBLE_HEAD",
|
||||
"SQUARE",
|
||||
"SQUARE_DOUBLE_HEAD",
|
||||
"MINIMAL",
|
||||
"MINIMAL_HEAVY_HEAD",
|
||||
"MINIMAL_DOUBLE_HEAD",
|
||||
|
|
|
@ -81,6 +81,8 @@ _TERM_COLORS = {"256color": ColorSystem.EIGHT_BIT, "16color": ColorSystem.STANDA
|
|||
class ConsoleOptions:
|
||||
"""Options for __rich_console__ method."""
|
||||
|
||||
legacy_windows: bool
|
||||
"""legacy_windows: flag for legacy windows."""
|
||||
min_width: int
|
||||
"""Minimum width of renderable."""
|
||||
max_width: int
|
||||
|
@ -96,6 +98,11 @@ class ConsoleOptions:
|
|||
no_wrap: Optional[bool] = False
|
||||
""""Disable wrapping for text."""
|
||||
|
||||
@property
|
||||
def ascii_only(self) -> bool:
|
||||
"""Check if renderables should use ascii only."""
|
||||
return not self.encoding.startswith("utf")
|
||||
|
||||
def update(
|
||||
self,
|
||||
width: int = None,
|
||||
|
@ -341,7 +348,7 @@ class Console:
|
|||
force_terminal (Optional[bool], optional): Enable/disable terminal control codes, or None to auto-detect terminal. Defaults to None.
|
||||
force_jupyter (Optional[bool], optional): Enable/disable Jupyter rendering, or None to auto-detect Jupyter. Defaults to None.
|
||||
theme (Theme, optional): An optional style theme object, or ``None`` for default theme.
|
||||
file (IO, optional): A file object where the console should write to. Defaults to stdoutput.
|
||||
file (IO, optional): A file object where the console should write to. Defaults to stdout.
|
||||
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,
|
||||
|
@ -568,7 +575,7 @@ class Console:
|
|||
Returns:
|
||||
str: A standard encoding string.
|
||||
"""
|
||||
return getattr(self.file, "encoding", "utf-8")
|
||||
return (getattr(self.file, "encoding", "utf-8") or "utf-8").lower()
|
||||
|
||||
@property
|
||||
def is_terminal(self) -> bool:
|
||||
|
@ -599,6 +606,7 @@ class Console:
|
|||
def options(self) -> ConsoleOptions:
|
||||
"""Get default console options."""
|
||||
return ConsoleOptions(
|
||||
legacy_windows=self.legacy_windows,
|
||||
min_width=1,
|
||||
max_width=self.width,
|
||||
encoding=self.encoding,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from .box import get_safe_box, Box, SQUARE, ROUNDED
|
||||
from .box import Box, ROUNDED
|
||||
|
||||
from .align import AlignValues
|
||||
from .jupyter import JupyterMixin
|
||||
|
@ -113,7 +113,7 @@ class Panel(JupyterMixin):
|
|||
width = options.max_width if self.width is None else self.width
|
||||
|
||||
safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box # type: ignore
|
||||
box = get_safe_box(self.box, console.legacy_windows) if safe_box else self.box
|
||||
box = self.box.substitute(options, safe=safe_box)
|
||||
|
||||
title_text = self._title
|
||||
if title_text is not None:
|
||||
|
|
|
@ -299,9 +299,7 @@ class DownloadColumn(ProgressColumn):
|
|||
completed = int(task.completed)
|
||||
total = int(task.total)
|
||||
unit, suffix = filesize.pick_unit_and_suffix(
|
||||
total,
|
||||
["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
|
||||
1000,
|
||||
total, ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], 1000
|
||||
)
|
||||
completed_ratio = completed / unit
|
||||
total_ratio = total / unit
|
||||
|
|
|
@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Iterable, List, NamedTuple, Optional, Tuple, U
|
|||
|
||||
from . import box, errors
|
||||
from ._loop import loop_first_last, loop_last
|
||||
from ._pick import pick_bool
|
||||
from ._ratio import ratio_distribute, ratio_reduce
|
||||
from .jupyter import JupyterMixin
|
||||
from .measure import Measurement
|
||||
|
@ -609,9 +610,12 @@ class Table(JupyterMixin):
|
|||
)
|
||||
)
|
||||
)
|
||||
safe_box: bool = console.safe_box if self.safe_box is None else self.safe_box # type: ignore
|
||||
_box = (
|
||||
box.get_safe_box(self.box, console.legacy_windows) if safe_box else self.box
|
||||
self.box.substitute(
|
||||
options, safe=pick_bool(self.safe_box, console.safe_box)
|
||||
)
|
||||
if self.box
|
||||
else None
|
||||
)
|
||||
|
||||
# _box = self.box
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from rich.box import get_safe_box, ASCII, DOUBLE, ROUNDED, HEAVY, SQUARE
|
||||
from rich.console import ConsoleOptions
|
||||
from rich.box import ASCII, DOUBLE, ROUNDED, HEAVY, SQUARE
|
||||
|
||||
|
||||
def test_str():
|
||||
|
@ -35,8 +36,18 @@ def test_get_bottom():
|
|||
assert bottom == "┗━┻━━┻━━━┛"
|
||||
|
||||
|
||||
def test_get_safe_box():
|
||||
assert get_safe_box(HEAVY, True) == SQUARE
|
||||
assert get_safe_box(HEAVY, False) == HEAVY
|
||||
assert get_safe_box(None, True) is None
|
||||
assert get_safe_box(None, False) is None
|
||||
def test_box_substitute():
|
||||
options = ConsoleOptions(
|
||||
legacy_windows=True,
|
||||
min_width=1,
|
||||
max_width=100,
|
||||
is_terminal=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert HEAVY.substitute(options) == SQUARE
|
||||
|
||||
options.legacy_windows = False
|
||||
assert HEAVY.substitute(options) == HEAVY
|
||||
|
||||
options.encoding = "ascii"
|
||||
assert HEAVY.substitute(options) == ASCII
|
||||
|
|
|
@ -43,7 +43,11 @@ def test_truecolor_terminal():
|
|||
|
||||
def test_console_options_update():
|
||||
options = ConsoleOptions(
|
||||
min_width=10, max_width=20, is_terminal=False, encoding="utf-8"
|
||||
legacy_windows=False,
|
||||
min_width=10,
|
||||
max_width=20,
|
||||
is_terminal=False,
|
||||
encoding="utf-8",
|
||||
)
|
||||
options1 = options.update(width=15)
|
||||
assert options1.min_width == 15 and options1.max_width == 15
|
||||
|
|
|
@ -103,12 +103,8 @@ def test_inspect_empty_dict():
|
|||
"│ in the keyword argument list. For │\n"
|
||||
"│ example: dict(one=1, two=2) │\n"
|
||||
"│ │\n"
|
||||
"│ 30 attribute(s) not shown. Use │\n"
|
||||
"│ inspect(<OBJECT>, all=True) to see all │\n"
|
||||
"│ attributes. │\n"
|
||||
"╰────────────────────────────────────────────────╯\n"
|
||||
)
|
||||
assert expected == render({})
|
||||
assert render({}).startswith(expected)
|
||||
|
||||
|
||||
def test_inspect_builtin_function():
|
||||
|
|
Loading…
Reference in New Issue