mirror of
https://github.com/Textualize/rich.git
synced 2025-08-04 18:18:22 +00:00
prompt fix
This commit is contained in:
parent
077acd5bea
commit
70d43d7c92
15 changed files with 141 additions and 91 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -5,6 +5,23 @@ 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).
|
||||
|
||||
## [4.0.0] - 2020-07-23
|
||||
|
||||
Major version bump for a breaking change to `Text.stylize signature`, which corrects a minor but irritating API wart. The style now comes first and the `start` and `end` offsets default to the entire text. This allows for `text.stylize_all(style)` to be replaced with `text.stylize(style)`. The `start` and `end` offsets now support negative indexing, so `text.stylize("bold", -1)` makes the last character bold.
|
||||
|
||||
### Added
|
||||
|
||||
- Added markup switch to RichHandler https://github.com/willmcgugan/rich/issues/171
|
||||
|
||||
### Changed
|
||||
|
||||
- Change signature of Text.stylize to accept style first
|
||||
- Remove Text.stylize_all which is no longer necessary
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed rendering of Confirm prompt https://github.com/willmcgugan/rich/issues/170
|
||||
|
||||
## [3.4.1] - 2020-07-22
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -61,6 +61,16 @@ If Rich detects that it is not writing to a terminal it will strip control codes
|
|||
Letting Rich auto-detect terminals is useful as it will write plain text when you pipe output to a file or other application.
|
||||
|
||||
|
||||
Environment variables
|
||||
---------------------
|
||||
|
||||
Rich respects some standard environment variables.
|
||||
|
||||
Settings the environment variable ``TERM`` to ``"dumb"`` or ``"unknown"`` will disable color/style and some features that require moving the cursor, such as progress bars.
|
||||
|
||||
If the environment variable ``NO_COLOR`` is set, Rich will disable all color in the output.
|
||||
|
||||
|
||||
Printing
|
||||
--------
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ Here's an example of how to set up a rich logger::
|
|||
log = logging.getLogger("rich")
|
||||
log.info("Hello, World!")
|
||||
|
||||
Rich logs won't process console markup by default, but you can enable markup per log statement with the ``extra`` argument as follows::
|
||||
Rich logs won't render :ref:`console_markup` in logging by default as most libraries won't be aware of the need to escape literal square brackets, but you can enable it by setting ``markup=True`` on the handler. Alternatively you can enable it per log message by supplying the ``extra`` argument as follows::
|
||||
|
||||
log.error("[bold red blink]Server is shutting down![/]", extra={"markup": True})
|
||||
|
||||
|
|
|
@ -69,4 +69,20 @@ This allows you to specify the text of the column only. If you want to set other
|
|||
title="Star Wars Movies"
|
||||
)
|
||||
|
||||
Grids
|
||||
~~~~~
|
||||
|
||||
The Table class can also make a great layout tool. If you disable headers and borders you can use it to position content within the terminal. The alternative constructor :meth:`~rich.table.Table.grid` can create such a table for you.
|
||||
|
||||
For instance, the following code displays two pieces of text aligned to both the left and right edges of the terminal on a single line::
|
||||
|
||||
|
||||
from rich import print
|
||||
from rich.table import Table
|
||||
|
||||
grid = Table.grid(expand=True)
|
||||
grid.add_column()
|
||||
grid.add_column(justify="right")
|
||||
grid.add_row("Raising shields", "[bold magenta]COMPLETED [green]:heavy_check_mark:")
|
||||
|
||||
print(grid)
|
||||
|
|
|
@ -22,7 +22,7 @@ else:
|
|||
def make_filename_text(filename):
|
||||
path = os.path.abspath(os.path.join(root_path, filename))
|
||||
text = Text(filename, style="bold blue" if os.path.isdir(path) else "default")
|
||||
text.stylize_all(f"link file://{path}")
|
||||
text.stylize(f"link file://{path}")
|
||||
text.highlight_regex(r"\..*?$", "bold")
|
||||
return text
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from rich.highlighter import Highlighter
|
|||
class RainbowHighlighter(Highlighter):
|
||||
def highlight(self, text):
|
||||
for index in range(len(text)):
|
||||
text.stylize(index, index + 1, str(randint(16, 255)))
|
||||
text.stylize(str(randint(16, 255)), index, index + 1)
|
||||
|
||||
|
||||
rainbow = RainbowHighlighter()
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "rich"
|
||||
homepage = "https://github.com/willmcgugan/rich"
|
||||
documentation = "https://rich.readthedocs.io/en/latest/"
|
||||
version = "3.4.1"
|
||||
version = "4.0.0"
|
||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||
authors = ["Will McGugan <willmcgugan@gmail.com>"]
|
||||
license = "MIT"
|
||||
|
|
|
@ -1,51 +1,45 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field, replace
|
||||
from functools import wraps
|
||||
from getpass import getpass
|
||||
import inspect
|
||||
|
||||
import os
|
||||
|
||||
import platform
|
||||
|
||||
import shutil
|
||||
import sys
|
||||
import threading
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass, field, replace
|
||||
from functools import wraps
|
||||
from getpass import getpass
|
||||
from typing import (
|
||||
IO,
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
cast,
|
||||
Dict,
|
||||
IO,
|
||||
Iterable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
TextIO,
|
||||
TYPE_CHECKING,
|
||||
NamedTuple,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from typing_extensions import Protocol, runtime_checkable, Literal
|
||||
|
||||
from typing_extensions import Literal, Protocol, runtime_checkable
|
||||
|
||||
from . import errors, themes
|
||||
from ._emoji_replace import _emoji_replace
|
||||
from .align import Align, AlignValues
|
||||
from .markup import render as render_markup
|
||||
from .measure import measure_renderables, Measurement
|
||||
from ._log_render import LogRender
|
||||
from . import errors
|
||||
from .align import Align, AlignValues
|
||||
from .color import ColorSystem
|
||||
from .control import Control
|
||||
from .highlighter import NullHighlighter, ReprHighlighter
|
||||
from .markup import render as render_markup
|
||||
from .measure import Measurement, measure_renderables
|
||||
from .pretty import Pretty
|
||||
from .scope import render_scope
|
||||
from .style import Style
|
||||
from .tabulate import tabulate_mapping
|
||||
from . import themes
|
||||
from .terminal_theme import TerminalTheme, DEFAULT_TERMINAL_THEME
|
||||
from .segment import Segment
|
||||
from .style import Style
|
||||
from .terminal_theme import DEFAULT_TERMINAL_THEME, TerminalTheme
|
||||
from .text import Text, TextType
|
||||
from .theme import Theme
|
||||
|
||||
|
|
|
@ -4,17 +4,22 @@ from logging import Handler, LogRecord
|
|||
from pathlib import Path
|
||||
from typing import ClassVar, List, Optional, Type
|
||||
|
||||
from . import get_console
|
||||
from rich._log_render import LogRender
|
||||
from rich.console import Console
|
||||
from rich.highlighter import Highlighter, ReprHighlighter
|
||||
from rich.text import Text
|
||||
|
||||
from . import get_console
|
||||
|
||||
|
||||
class RichHandler(Handler):
|
||||
"""A logging handler that renders output with Rich. The time / level / message and file are displayed in columns.
|
||||
The level is color coded, and the message is syntax highlighted.
|
||||
|
||||
The level is color coded, and the message is syntax highlighted.
|
||||
|
||||
Note:
|
||||
Be careful when enabling console markup in log messages if you have configured logging for libraries not
|
||||
under your control. If a dependency writes messages containing square brackets, it may not produce the intended output.
|
||||
|
||||
Args:
|
||||
level (int, optional): Log level. Defaults to logging.NOTSET.
|
||||
console (:class:`~rich.console.Console`, optional): Optional console instance to write logs.
|
||||
|
@ -22,6 +27,7 @@ class RichHandler(Handler):
|
|||
show_path (bool, optional): Show the path to the original log call. Defaults to True.
|
||||
enable_link_path (bool, optional): Enable terminal link of path column to file. Defaults to True.
|
||||
highlighter (Highlighter, optional): Highlighter to style log messages, or None to use ReprHighlighter. Defaults to None.
|
||||
markup (bool, optional): Enable console markup in log messages. Defaults to False.
|
||||
|
||||
"""
|
||||
|
||||
|
@ -45,12 +51,14 @@ class RichHandler(Handler):
|
|||
show_path: bool = True,
|
||||
enable_link_path: bool = True,
|
||||
highlighter: Highlighter = None,
|
||||
markup: bool = False,
|
||||
) -> None:
|
||||
super().__init__(level=level)
|
||||
self.console = console or get_console()
|
||||
self.highlighter = highlighter or self.HIGHLIGHTER_CLASS()
|
||||
self._log_render = LogRender(show_level=True, show_path=show_path)
|
||||
self.enable_link_path = enable_link_path
|
||||
self.markup = markup
|
||||
|
||||
def emit(self, record: LogRecord) -> None:
|
||||
"""Invoked by logging."""
|
||||
|
@ -63,7 +71,10 @@ class RichHandler(Handler):
|
|||
level = Text()
|
||||
level.append(record.levelname, log_style)
|
||||
|
||||
if getattr(record, "markup", False):
|
||||
use_markup = (
|
||||
getattr(record, "markup") if hasattr(record, "markup") else self.markup
|
||||
)
|
||||
if use_markup:
|
||||
message_text = Text.from_markup(message)
|
||||
else:
|
||||
message_text = Text(message)
|
||||
|
|
|
@ -337,9 +337,8 @@ class ImageItem(TextElement):
|
|||
) -> RenderResult:
|
||||
link_style = Style(link=self.link or self.destination or None)
|
||||
title = self.text or Text(self.destination.strip("/").rsplit("/", 1)[-1])
|
||||
|
||||
if self.hyperlinks:
|
||||
title.stylize_all(link_style)
|
||||
title.stylize(link_style)
|
||||
yield Text.assemble("🌆 ", title, " ", end="")
|
||||
|
||||
|
||||
|
|
|
@ -1,20 +1,9 @@
|
|||
from typing import (
|
||||
Any,
|
||||
Generic,
|
||||
IO,
|
||||
List,
|
||||
overload,
|
||||
Optional,
|
||||
TextIO,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
from typing import IO, Any, Generic, List, Optional, TextIO, TypeVar, Union, overload
|
||||
|
||||
from .__init__ import get_console
|
||||
from .console import Console, RenderableType
|
||||
from .console import Console
|
||||
from .text import Text, TextType
|
||||
|
||||
|
||||
PromptType = TypeVar("PromptType")
|
||||
DefaultType = TypeVar("DefaultType")
|
||||
|
||||
|
@ -34,7 +23,7 @@ class InvalidResponse(PromptError):
|
|||
def __init__(self, message: TextType) -> None:
|
||||
self.message = message
|
||||
|
||||
def __rich__(self) -> RenderableType:
|
||||
def __rich__(self) -> TextType:
|
||||
return self.message
|
||||
|
||||
|
||||
|
@ -151,6 +140,17 @@ class PromptBase(Generic[PromptType]):
|
|||
)
|
||||
return _prompt(default=default, stream=stream)
|
||||
|
||||
def render_default(self, default: DefaultType) -> Text:
|
||||
"""Turn the supplied default in to a Text instance.
|
||||
|
||||
Args:
|
||||
default (DefaultType): Default value.
|
||||
|
||||
Returns:
|
||||
Text: Text containing rendering of default value.
|
||||
"""
|
||||
return Text(f"({default})", "prompt.default")
|
||||
|
||||
def make_prompt(self, default: DefaultType) -> Text:
|
||||
"""Make prompt text.
|
||||
|
||||
|
@ -175,7 +175,8 @@ class PromptBase(Generic[PromptType]):
|
|||
and isinstance(default, (str, self.response_type))
|
||||
):
|
||||
prompt.append(" ")
|
||||
prompt.append(f"({default})", "prompt.default")
|
||||
_default = self.render_default(default)
|
||||
prompt.append(_default)
|
||||
|
||||
prompt.append(self.prompt_suffix)
|
||||
|
||||
|
@ -325,20 +326,28 @@ class Confirm(PromptBase[bool]):
|
|||
|
||||
response_type = bool
|
||||
validate_error_message = "[prompt.invalid]Please enter Y or N"
|
||||
choices = ["y", "N"]
|
||||
choices = ["y", "n"]
|
||||
|
||||
def render_default(self, default: DefaultType) -> Text:
|
||||
"""Render the default as (y) or (n) rather than True/False."""
|
||||
assert self.choices is not None
|
||||
yes, no = self.choices
|
||||
return Text(f"({yes})" if default else f"({no})", style="prompt.default")
|
||||
|
||||
def process_response(self, value: str) -> bool:
|
||||
"""Convert choices to a bool."""
|
||||
value = value.strip().lower()
|
||||
if value not in ["y", "n"]:
|
||||
if value not in self.choices:
|
||||
raise InvalidResponse(self.validate_error_message)
|
||||
return value == "y"
|
||||
assert self.choices is not None
|
||||
return value == self.choices[0]
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
|
||||
from rich import print
|
||||
|
||||
if Confirm.ask("Run [i]prompt[/i] tests?"):
|
||||
if Confirm.ask("Run [i]prompt[/i] tests?", default=True):
|
||||
while True:
|
||||
result = IntPrompt.ask(
|
||||
":rocket: Enter a number betwen [b]1[/b] and [b]10[/b]", default=5
|
||||
|
@ -363,4 +372,3 @@ if __name__ == "__main__": # pragma: no cover
|
|||
|
||||
else:
|
||||
print("[b]OK :loudly_crying_face:")
|
||||
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from typing import TYPE_CHECKING, Iterable, List, NamedTuple, Optional, Tuple, Union
|
||||
|
||||
from . import box, errors
|
||||
from ._loop import loop_first_last, loop_last
|
||||
|
|
42
rich/text.py
42
rich/text.py
|
@ -15,6 +15,7 @@ from typing import (
|
|||
)
|
||||
|
||||
from ._loop import loop_last
|
||||
from ._pick import pick_bool
|
||||
from ._wrap import divide_line
|
||||
from .align import AlignValues
|
||||
from .cells import cell_len, set_cell_size
|
||||
|
@ -22,7 +23,6 @@ from .containers import Lines
|
|||
from .control import strip_control_codes
|
||||
from .jupyter import JupyterMixin
|
||||
from .measure import Measurement
|
||||
from ._pick import pick_bool
|
||||
from .segment import Segment
|
||||
from .style import Style, StyleType
|
||||
|
||||
|
@ -230,7 +230,7 @@ class Text(JupyterMixin):
|
|||
Text: A text instance with a style applied to the entire string.
|
||||
"""
|
||||
styled_text = cls(text, justify=justify, overflow=overflow)
|
||||
styled_text.stylize_all(style)
|
||||
styled_text.stylize(style)
|
||||
return styled_text
|
||||
|
||||
@classmethod
|
||||
|
@ -321,31 +321,28 @@ class Text(JupyterMixin):
|
|||
copy_self._spans[:] = self._spans
|
||||
return copy_self
|
||||
|
||||
def stylize(self, start: int, end: int, style: Union[str, Style]) -> None:
|
||||
"""Apply a style to a portion of the text.
|
||||
|
||||
Args:
|
||||
start (int): Start offset.
|
||||
end (int): End offset.
|
||||
def stylize(
|
||||
self, style: Union[str, Style], start: int = 0, end: Optional[int] = None
|
||||
) -> None:
|
||||
"""Apply a style to the text, or a portion of the text.
|
||||
|
||||
Args:
|
||||
style (Union[str, Style]): Style instance or style definition to apply.
|
||||
start (int): Start offset (negative indexing is supported). Defaults to 0.
|
||||
end (Optional[int], optional): End offset (negative indexing is supported), or None for end of text. Defaults to None.
|
||||
|
||||
"""
|
||||
length = len(self)
|
||||
if end < 0 or start >= length:
|
||||
# span not in range
|
||||
if start < 0:
|
||||
start = length + start
|
||||
if end is None:
|
||||
end = length
|
||||
if end < 0:
|
||||
end = length + end
|
||||
if start >= length or end <= start:
|
||||
# Span not in text or not valid
|
||||
return
|
||||
self._spans.append(Span(max(0, start), min(length + 1, end), style))
|
||||
|
||||
def stylize_all(self, style: Union[str, Style]) -> None:
|
||||
"""Apply a style to all the text.
|
||||
|
||||
Args:
|
||||
start (int): Start offset.
|
||||
end (int): End offset.
|
||||
style (Union[str, Style]): Style instance or style definition to apply.
|
||||
|
||||
"""
|
||||
self._spans.append(Span(0, len(self), style))
|
||||
self._spans.append(Span(start, min(length, end), style))
|
||||
|
||||
def get_style_at_offset(self, console: "Console", offset: int) -> Style:
|
||||
"""Get the style of a character at give offset.
|
||||
|
@ -417,6 +414,7 @@ class Text(JupyterMixin):
|
|||
Args:
|
||||
words (Iterable[str]): Worlds to highlight.
|
||||
style (Union[str, Style]): Style to apply.
|
||||
case_sensitive (bool, optional): Enable case sensitive matchings. Defaults to True.
|
||||
|
||||
Returns:
|
||||
int: Number of words highlighted.
|
||||
|
|
|
@ -52,7 +52,7 @@ def test_prompt_confirm_no():
|
|||
console = Console(file=io.StringIO())
|
||||
answer = Confirm.ask("continue", console=console, stream=io.StringIO(INPUT),)
|
||||
assert answer is False
|
||||
expected = "continue [y/N]: Please enter Y or N\ncontinue [y/N]: Please enter Y or N\ncontinue [y/N]: "
|
||||
expected = "continue [y/n]: Please enter Y or N\ncontinue [y/n]: Please enter Y or N\ncontinue [y/n]: "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
@ -63,7 +63,7 @@ def test_prompt_confirm_yes():
|
|||
console = Console(file=io.StringIO())
|
||||
answer = Confirm.ask("continue", console=console, stream=io.StringIO(INPUT),)
|
||||
assert answer is True
|
||||
expected = "continue [y/N]: Please enter Y or N\ncontinue [y/N]: Please enter Y or N\ncontinue [y/N]: "
|
||||
expected = "continue [y/n]: Please enter Y or N\ncontinue [y/n]: Please enter Y or N\ncontinue [y/n]: "
|
||||
output = console.file.getvalue()
|
||||
print(repr(output))
|
||||
assert output == expected
|
||||
|
|
|
@ -114,12 +114,18 @@ def test_rstrip_end():
|
|||
|
||||
def test_stylize():
|
||||
test = Text("Hello, World!")
|
||||
test.stylize(7, 11, "bold")
|
||||
test.stylize("bold", 7, 11)
|
||||
assert test._spans == [Span(7, 11, "bold")]
|
||||
test.stylize(20, 25, "bold")
|
||||
test.stylize("bold", 20, 25)
|
||||
assert test._spans == [Span(7, 11, "bold")]
|
||||
|
||||
|
||||
def test_stylize_negative_index():
|
||||
test = Text("Hello, World!")
|
||||
test.stylize("bold", -6, -1)
|
||||
assert test._spans == [Span(7, 12, "bold")]
|
||||
|
||||
|
||||
def test_highlight_regex():
|
||||
test = Text("peek-a-boo")
|
||||
|
||||
|
@ -213,8 +219,8 @@ def test_set_length():
|
|||
assert test == Text("Hello ")
|
||||
|
||||
test = Text("Hello World")
|
||||
test.stylize(0, 5, "bold")
|
||||
test.stylize(7, 9, "italic")
|
||||
test.stylize("bold", 0, 5)
|
||||
test.stylize("italic", 7, 9)
|
||||
|
||||
test.set_length(3)
|
||||
expected = Text()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue