From 1db22c9d8848574e4e025dc7609dbee8eeb91c2f Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Tue, 19 May 2020 13:38:13 +0100 Subject: [PATCH] allow attribute abbreviations --- CHANGELOG.md | 12 +++++++++++- docs/source/console.rst | 11 ++++++++++- docs/source/logging.rst | 2 +- docs/source/markup.rst | 3 +++ docs/source/reference.rst | 1 + docs/source/reference/align.rst | 7 +++++++ docs/source/style.rst | 19 +++++++++--------- docs/source/text.rst | 7 ++++++- pyproject.toml | 2 +- rich/bar.py | 4 +++- rich/default_styles.py | 4 ---- rich/markup.py | 9 ++++++--- rich/measure.py | 2 ++ rich/progress.py | 6 +++++- rich/style.py | 34 ++++++++++++++++++++------------- rich/table.py | 5 ++++- tests/test_bar.py | 6 ++++++ 17 files changed, 97 insertions(+), 37 deletions(-) create mode 100644 docs/source/reference/align.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index b50c1e4c..0e45274f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,17 @@ 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). -## [1.1.6] - Unreleased +## [1.1.7] - 2020-05-19 + +### Changed + +- Some style attributes may be abbreviated (b for bold, i for italic etc). Previously abbreviations worked in console markup but only one at a time, i.e. "[b]Hello[/]" but not "[b i]Hello[/]" -- now they work everywhere. + +### Fixed + +- Fixed zero division if total is 0 in progress bar + +## [1.1.6] - 2020-05-17 ### Added diff --git a/docs/source/console.rst b/docs/source/console.rst index b76e0b52..112b7ba7 100644 --- a/docs/source/console.rst +++ b/docs/source/console.rst @@ -52,7 +52,7 @@ Justify / Alignment Both print and log support a ``justify`` argument which if set must be one of "left", "right", "center", or "full". If "left", any text printed (or logged) will be left aligned, if "right" text will be aligned to the right of the terminal, if "center" the text will be centered, and if "full" the text will be lined up with both the left and right edges of the terminal (like printed text in a book). -The default for ``justify`` is ``None`` which will generally look the same as ``"left"`` but with a subtle different. Left justify will pad the right of the text with spaces, while a None justify will not. You will only notice the difference if you set a background color with the ``style`` argument. The following example demonstrates the difference:: +The default for ``justify`` is ``None`` which will generally look the same as ``"left"`` but with a subtle difference. Left justify will pad the right of the text with spaces, while a None justify will not. You will only notice the difference if you set a background color with the ``style`` argument. The following example demonstrates the difference:: from rich.console import Console @@ -76,6 +76,15 @@ This produces the following output: +Input +----- + +The console class has an :meth:`~rich.console.Console.input` which works in the same way as Python's builtin ``input()`` method, but can use anything that Rich can print as a prompt. For example, here's a colorful prompt with an emoji:: + + from rich.console import Console + console = Console() + console.input("What is [i]your[/i] [bold red]name[/]? :smiley: ") + Exporting --------- diff --git a/docs/source/logging.rst b/docs/source/logging.rst index 4203fe04..77ad82bd 100644 --- a/docs/source/logging.rst +++ b/docs/source/logging.rst @@ -10,7 +10,7 @@ Here's an example of how to set up a rich logger:: FORMAT = "%(message)s" logging.basicConfig( - level="NOTSET", format=FORMAT, datefmt="[%X] ", handlers=[RichHandler()] + level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] ) log = logging.getLogger("rich") diff --git a/docs/source/markup.rst b/docs/source/markup.rst index d90c529a..c8fe981e 100644 --- a/docs/source/markup.rst +++ b/docs/source/markup.rst @@ -46,6 +46,9 @@ Occasionally you may want to print something that Rich would interpret as markup The function :func:`~rich.markup.escape` will handle escape of text for you. +.. warning:: + Be careful when using f-strings with console markup. You will need to escape any variables if they could contain square brackets. + Rendering Markup ---------------- diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 95c708b2..c57eda75 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -4,6 +4,7 @@ Reference .. toctree:: :maxdepth: 3 + reference/align.rst reference/bar.rst reference/color.rst reference/console.rst diff --git a/docs/source/reference/align.rst b/docs/source/reference/align.rst new file mode 100644 index 00000000..54b05255 --- /dev/null +++ b/docs/source/reference/align.rst @@ -0,0 +1,7 @@ +rich.align +========== + +.. automodule:: rich.align + :members: + + diff --git a/docs/source/style.rst b/docs/source/style.rst index 3e58c967..52433e85 100644 --- a/docs/source/style.rst +++ b/docs/source/style.rst @@ -30,20 +30,21 @@ The hex and rgb forms allow you to select from the full *truecolor* set of 16.7 .. note:: Some terminals only support 256 colors. Rich will attempt to pick the closest color it can if your color isn't available. - By itself, a color will change the *foreground* color. To specify a *background* color precede the color with the word "on". For example, the following prints text in red on a white background:: console.print("DANGER!", style="red on white") +You can also set a color with the word ``"default"`` which will reset the color to a default managed by your terminal software. This works for back grounds as well, so the style of ``"default on default"`` is what your terminal starts with. + You can set a style attribute by adding one or more of the following words: -* ``"bold"`` For bold text. -* ``"blink"`` For text that flashes (use this one sparingly). -* ``"conceal"`` For *concealed* text (not supported by many terminals). -* ``"italic"`` For italic text. -* ``"reverse"`` For text with foreground and background colors reversed. -* ``"strike"`` For text with a line through it. -* ``"underline"`` For underlined text. +* ``"bold"`` or ``"b"`` for bold text. +* ``"blink"`` for text that flashes (use this one sparingly). +* ``"conceal"`` for *concealed* text (not supported by many terminals). +* ``"italic"`` or ``"i"`` for italic text. +* ``"reverse"`` or ``"r"`` for text with foreground and background colors reversed. +* ``"strike"`` or ``"s"`` for text with a line through it. +* ``"underline"`` or ``"u"`` for underlined text. Style attributes and colors may be used in combination with each other. For example:: @@ -74,7 +75,7 @@ Ultimately the style definition is parsed and an instance of a :class:`~rich.sty from rich.style import Style console.print("Danger, Will Robinson!", style=Style(color="red", blink=True, bold=True) -It is slightly quicker to construct a Style class like this, since a style definition takes a little time to parse -- but only on the first call, as Rich will cache any style definitions it parses. +It is slightly quicker to construct a Style class like this, since a style definition takes a little time to parse -- but only on the first call, as Rich will cache parsed style definitions. You can parse a style definition explicitly with the :meth:`~rich.style.Style.parse` method. diff --git a/docs/source/text.rst b/docs/source/text.rst index 4a7b2b3c..eb28a607 100644 --- a/docs/source/text.rst +++ b/docs/source/text.rst @@ -21,4 +21,9 @@ Alternatively, you can construct styled text by calling :meth:`~rich.text.Text.a text.append(" World!") console.print(text) -You can also apply a style to given words in the text with :meth:`~rich.text.Text.highlight_words` or for ultimate control call :meth:`~rich.text.Text.highlight_regex` to highlight text matching a *regular expression*. +Since building Text instances from parts is a common requirement, Rich offers :meth:`~rich.text.Text.assemble` which will combine strings or pairs of string and Style, and return an Text instance. The follow example is equivalent to the code above:: + + text = Text.assemble(("Hello", "bold magenta"), " World!") + console.print(text) + +You can apply a style to given words in the text with :meth:`~rich.text.Text.highlight_words` or for ultimate control call :meth:`~rich.text.Text.highlight_regex` to highlight text matching a *regular expression*. diff --git a/pyproject.toml b/pyproject.toml index 1bb8ee27..afa7538a 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 = "1.1.6" +version = "1.1.7" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" authors = ["Will McGugan "] license = "MIT" diff --git a/rich/bar.py b/rich/bar.py index 14a0ce4c..8eb2898d 100644 --- a/rich/bar.py +++ b/rich/bar.py @@ -62,7 +62,9 @@ class Bar: bar = "▓" if legacy_windows else "━" half_bar_right = "░" if legacy_windows else "╸" half_bar_left = " " if legacy_windows else "╺" - complete_halves = int(width * 2 * completed / self.total) + complete_halves = ( + int(width * 2 * completed / self.total) if self.total else width * 2 + ) bar_count = complete_halves // 2 half_bar_count = complete_halves % 2 style = console.get_style(self.style) diff --git a/rich/default_styles.py b/rich/default_styles.py index 632679b3..943134e7 100644 --- a/rich/default_styles.py +++ b/rich/default_styles.py @@ -22,17 +22,13 @@ DEFAULT_STYLES: Dict[str, Style] = { "bold": Style(bold=True), "strong": Style(bold=True), "code": Style(reverse=True, bold=True), - "b": Style(bold=True), "italic": Style(italic=True), "emphasize": Style(italic=True), - "i": Style(italic=True), "underline": Style(underline=True), - "u": Style(underline=True), "blink": Style(blink=True), "blink2": Style(blink2=True), "reverse": Style(reverse=True), "strike": Style(strike=True), - "s": Style(strike=True), "black": Style(color="black"), "red": Style(color="red"), "green": Style(color="green"), diff --git a/rich/markup.py b/rich/markup.py index 78129ccb..5760dd39 100644 --- a/rich/markup.py +++ b/rich/markup.py @@ -26,7 +26,7 @@ class Tag(NamedTuple): return ( f"[{self.name}]" if self.parameters is None - else f"[{self.name} {self.parameters}]" + else f"[{self.name}={self.parameters}]" ) @@ -61,7 +61,7 @@ def _parse(markup: str) -> Iterable[Tuple[int, Optional[str], Optional[Tag]]]: if equals: yield start, None, Tag(text, parameters) else: - yield start, None, Tag(normalize(tag_text.strip()), None) + yield start, None, Tag(tag_text.strip(), None) else: yield start, (escape_open and "[") or (escape_close and "]"), None # type: ignore position = end @@ -84,6 +84,7 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te """ text = Text(style=style) append = text.append + normalize = Style.normalize style_stack: List[Tuple[int, Tag]] = [] pop = style_stack.pop @@ -108,6 +109,7 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te if tag.name.startswith("/"): # Closing tag style_name = tag.name[1:].strip() if style_name: # explicit close + style_name = normalize(style_name) try: start, open_tag = pop_style(style_name) except KeyError: @@ -124,7 +126,8 @@ def render(markup: str, style: Union[str, Style] = "", emoji: bool = True) -> Te append_span(_Span(start, len(text), str(open_tag))) else: # Opening tag - style_stack.append((len(text), tag)) + normalized_tag = Tag(normalize(tag.name), tag.parameters) + style_stack.append((len(text), normalized_tag)) text_length = len(text) while style_stack: diff --git a/rich/measure.py b/rich/measure.py index 59ac93d7..0c548fc3 100644 --- a/rich/measure.py +++ b/rich/measure.py @@ -57,6 +57,8 @@ class Measurement(NamedTuple): if isinstance(renderable, str): renderable = console.render_str(renderable) + + renderable = getattr(renderable, "__rich__", renderable) if is_renderable(renderable): get_console_width = getattr(renderable, "__measure__", None) if get_console_width is not None: diff --git a/rich/progress.py b/rich/progress.py index d9fe0ec9..eb9864a1 100644 --- a/rich/progress.py +++ b/rich/progress.py @@ -153,7 +153,11 @@ 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=max(0, task.total), + completed=max(0, task.completed), + width=max(1, self.bar_width), + ) class TimeRemainingColumn(ProgressColumn): diff --git a/rich/style.py b/rich/style.py index 7fba6fdb..928332cc 100644 --- a/rich/style.py +++ b/rich/style.py @@ -215,15 +215,22 @@ class Style: `Style`: A Style instance. """ style_attributes = { - "dim", - "bold", - "italic", - "underline", - "blink", - "blink2", - "reverse", - "conceal", - "strike", + "dim": "dim", + "d": "dim", + "bold": "bold", + "b": "bold", + "italic": "italic", + "i": "italic", + "underline": "underline", + "u": "underline", + "blink": "blink", + "blink2": "blink2", + "reverse": "reverse", + "r": "reverse", + "conceal": "conceal", + "c": "conceal", + "strike": "strike", + "s": "strike" } color: Optional[str] = None bgcolor: Optional[str] = None @@ -247,11 +254,12 @@ class Style: elif word == "not": word = next(words, "") - if word not in style_attributes: + attribute = style_attributes.get(word) + if attribute is None: raise errors.StyleSyntaxError( f"expected style attribute after 'not', found {original_word!r}" - ) - attributes[word] = False + ) + attributes[attribute] = False elif word == "link": word = next(words, "") @@ -260,7 +268,7 @@ class Style: link = word elif word in style_attributes: - attributes[word] = True + attributes[style_attributes[word]] = True else: try: diff --git a/rich/table.py b/rich/table.py index 55296301..1b6ef54d 100644 --- a/rich/table.py +++ b/rich/table.py @@ -260,6 +260,9 @@ class Table: def add_row(self, *renderables: Optional["RenderableType"]) -> None: """Add a row of renderables. + Args: + *renderables (None or renderable): Each cell in a row must be None (for a blank cell) or a renderable object (including str). + Raises: errors.NotRenderableError: If you add something that can't be rendered. """ @@ -289,7 +292,7 @@ class Table: add_cell(column, renderable) else: raise errors.NotRenderableError( - f"unable to render {renderable!r}; str or object with a __console__ method is required" + f"unable to render {type(renderable).__name__}; a string or other renderable object is required" ) self._row_count += 1 diff --git a/tests/test_bar.py b/tests/test_bar.py index 7b1f55f2..5c6c0699 100644 --- a/tests/test_bar.py +++ b/tests/test_bar.py @@ -45,6 +45,12 @@ def test_measure(): assert measurement.maximum == 120 +def test_zero_total(): + # Shouldn't throw zero division error + bar = Bar(total=0) + render(bar) + + if __name__ == "__main__": bar = Bar(completed=11, width=50) bar_render = render(bar)