prompt fix

This commit is contained in:
Will McGugan 2020-07-23 16:49:07 +01:00
parent 077acd5bea
commit 70d43d7c92
15 changed files with 141 additions and 91 deletions

View file

@ -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

View file

@ -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
--------

View file

@ -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})

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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="")

View file

@ -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:")

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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()