From 433ee13234cb7d4e3a0e1a7afac3326ab8cca0ce Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Mon, 2 Mar 2020 10:26:55 +0000 Subject: [PATCH] theme improvements and docs --- CHANGELOG.md | 3 +++ docs/source/style.rst | 29 ++++++++++++++++++++------- rich/console.py | 21 ++++++++++++++------ rich/theme.py | 46 +++++++++++++++++++++++++++++++++++++------ 4 files changed, 80 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f2662a..4e0f3d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added tab_size to Console and Text - Added protocol.is_renderable for runtime check +- Added emoji switch to Console +- Added inherit boolean to Theme +- Made Console thread safe, with a thread local buffer ### Changed diff --git a/docs/source/style.rst b/docs/source/style.rst index c010b1b6..2162d186 100644 --- a/docs/source/style.rst +++ b/docs/source/style.rst @@ -63,20 +63,35 @@ It is slightly quicker to construct a Style class like this, since a style defin You can parse a style definition explicitly with the :meth:`~rich.style.Style.parse` method. -Custom Styles -------------- +Style Themes +------------ -While it is possible to explicitly specify styles each time you use them, it is discouraged. Rich allows you to predefine style names which you can use in place on an explicit style. For instance, you may want to define styles for info, warning, danger etc. +If you re-use styles it can be a maintenance headache if you ever want to modify an attribute or color -- you would have to change every line where the style is used. Rich provides a :class:`rich.theme.Theme` class which you can use to pre-define styles, so if you ever need to modify a style you only need change one file. -You can define a mapping of names to styles in the :class:`~rich.console.Console` constructor. These styles will be merged in to the default styles, potentially overwriting them. Here's an example:: +Style themes can also make your code more semantic, for instance a style called ``"warning"`` better expresses intent that ``"italic magenta underline"``. + +To use a style theme, construct a :class:`rich.theme.Theme` instance and pass it to the :class:`~rich.console.Console` constructor. Here's an example:: from rich.console import Console - styles = { + from rich.theme import Theme + custom_theme = Theme({ "info" : Style.parse("dim cyan"), "warning": Style.parse("magenta"), "danger": Style.parse("bold red") - } - console = Console(styles=styles) + }) + console = Console(theme=custom_theme) console.print("This is information", style="info") console.print("Something terrible happened!", style="danger") +You can also use these custom styles via markup. For example:: + + console.print("[warning]The pod bay doors are locked[/warning]") + +If you prefer you can write your styles in an external config file rather than in Python. Here's an example of the format:: + + [styles] + info = dim cyan + warning = magenta + danger bold red + +You can read these files with the :method:`~rich.theme.Theme.read` method. diff --git a/rich/console.py b/rich/console.py index d691a846..b28f7729 100644 --- a/rich/console.py +++ b/rich/console.py @@ -12,6 +12,7 @@ import platform import re import shutil import sys +import threading from typing import ( Any, Callable, @@ -286,7 +287,7 @@ class Console: else: self._color_system = COLOR_SYSTEMS[color_system] - self.buffer: List[Segment] = [] + self._thread_locals = threading.local() self._buffer_index = 0 self._record_buffer: List[Segment] = [] @@ -302,6 +303,14 @@ class Console: def __repr__(self) -> str: return f"" + @property + def _buffer(self) -> List[Segment]: + """Get a thread local buffer.""" + buffer = getattr(self._thread_locals, "buffer", None) + if buffer is None: + buffer = self._thread_locals.buffer = [] + return buffer + def _detect_color_system(self,) -> Optional[ColorSystem]: """Detect color system from env vars.""" if not self.is_terminal: @@ -423,7 +432,7 @@ class Console: assert count >= 0, "count must be >= 0" if count: - self.buffer.append(Segment("\n" * count)) + self._buffer.append(Segment("\n" * count)) self._check_buffer() def _render( @@ -771,7 +780,7 @@ class Console: ) render_options = self.options - extend = self.buffer.extend + extend = self._buffer.extend render = self.render with self.style(style): for renderable in renderables: @@ -838,7 +847,7 @@ class Console: renderables.append(tabulate_mapping(locals_map, title="Locals")) with self: - self.buffer.extend( + self._buffer.extend( self.render( self._log_render(self, renderables, path=path, line_no=line_no), self.options, @@ -856,10 +865,10 @@ class Console: output: List[str] = [] append = output.append color_system = self._color_system - buffer = self.buffer[:] + buffer = self._buffer[:] if self.record: self._record_buffer.extend(buffer) - del self.buffer[:] + del self._buffer[:] for line in Segment.split_and_crop_lines(buffer, self.width, pad=False): for text, style in line: if style: diff --git a/rich/theme.py b/rich/theme.py index 17b27f53..57a0977f 100644 --- a/rich/theme.py +++ b/rich/theme.py @@ -1,12 +1,25 @@ import configparser from typing import Dict, IO +from .default_styles import DEFAULT_STYLES from .style import Style class Theme: - def __init__(self, styles: Dict[str, Style] = None): - self.styles = styles or {} + """An container for style information, used by :class:`~rich.console.Console`. + + Args: + styles (Dict[str, Style], optional): A mapping of style names on to styles. Defaults to None for empty styles. + inherit (bool, optional): Switch to inherit default styles. Defaults to True. + """ + + def __init__(self, styles: Dict[str, Style] = None, inherit: bool = True): + if inherit: + self.styles = DEFAULT_STYLES + else: + self.styles = {} + if styles is not None: + self.styles.update(styles) @property def config(self) -> str: @@ -19,14 +32,35 @@ class Theme: return config @classmethod - def from_file(cls, config_file: IO[str], source: str = None) -> "Theme": + def from_file( + cls, config_file: IO[str], source: str = None, inherit: bool = True + ) -> "Theme": + """Load a theme from a text mode file. + + Args: + config_file (IO[str]): An open conf file. + source (str, optional): The filename of the open file. Defaults to None. + inherit (bool, optional): Switch to inherit default styles. Defaults to True. + + Returns: + Theme: A New theme instance. + """ config = configparser.ConfigParser() config.read_file(config_file, source=source) styles = {name: Style.parse(value) for name, value in config.items("styles")} - theme = Theme(styles) + theme = Theme(styles, inherit=inherit) return theme @classmethod - def read(cls, path: str) -> "Theme": + def read(cls, path: str, inherit: bool = True) -> "Theme": + """Read a theme from a path. + + Args: + path (str): Path to a config file readable by Python configparser module. + inherit (bool, optional): Switch to inherit default styles. Defaults to True. + + Returns: + Theme: A new theme instance. + """ with open(path, "rt") as config_file: - return cls.from_file(config_file, source=path) + return cls.from_file(config_file, source=path, inherit=inherit)