From f7c1bb269e4ef422e6151d4a20c37051f9e74dc2 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Thu, 5 May 2022 18:17:05 +0300 Subject: [PATCH] Refactor palette (#1378) * Refactor palette * Modifiers / change static strings to colors * Colors... * Error-based tests * Styling linting Co-authored-by: Jakub Roztocil --- httpie/context.py | 57 +++++----- httpie/core.py | 4 +- httpie/output/formatters/colors.py | 138 ++++++++++++------------ httpie/output/ui/man_pages.py | 6 +- httpie/output/ui/palette.py | 168 ++++++++++++++++++++--------- httpie/output/ui/rich_help.py | 18 ++-- httpie/output/ui/rich_palette.py | 78 ++++++++++---- httpie/output/ui/rich_progress.py | 27 +++-- httpie/output/ui/rich_utils.py | 7 +- httpie/sessions.py | 4 +- 10 files changed, 312 insertions(+), 195 deletions(-) diff --git a/httpie/context.py b/httpie/context.py index da4d1925..086cfa4f 100644 --- a/httpie/context.py +++ b/httpie/context.py @@ -18,20 +18,28 @@ from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError from .encoding import UTF8 from .utils import repr_dict -from httpie.output.ui import rich_palette as palette +from .output.ui.palette import GenericColor if TYPE_CHECKING: from rich.console import Console -class Levels(str, Enum): +class LogLevel(str, Enum): + INFO = 'info' WARNING = 'warning' ERROR = 'error' -DISPLAY_THRESHOLDS = { - Levels.WARNING: 2, - Levels.ERROR: float('inf'), # Never hide errors. +LOG_LEVEL_COLORS = { + LogLevel.INFO: GenericColor.PINK, + LogLevel.WARNING: GenericColor.ORANGE, + LogLevel.ERROR: GenericColor.RED, +} + +LOG_LEVEL_DISPLAY_THRESHOLDS = { + LogLevel.INFO: 1, + LogLevel.WARNING: 2, + LogLevel.ERROR: float('inf'), # Never hide errors. } @@ -159,16 +167,22 @@ class Environment: self.stdout = original_stdout self.stderr = original_stderr - def log_error(self, msg: str, level: Levels = Levels.ERROR) -> None: - if self.stdout_isatty and self.quiet >= DISPLAY_THRESHOLDS[level]: + def log_error(self, msg: str, level: LogLevel = LogLevel.ERROR) -> None: + if self.stdout_isatty and self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[level]: stderr = self.stderr # Not directly /dev/null, since stderr might be mocked else: stderr = self._orig_stderr - - stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n') + rich_console = self._make_rich_console(file=stderr, force_terminal=stderr.isatty()) + rich_console.print( + f'\n{self.program_name}: {level}: {msg}\n\n', + style=LOG_LEVEL_COLORS[level], + markup=False, + highlight=False, + soft_wrap=True + ) def apply_warnings_filter(self) -> None: - if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]: + if self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[LogLevel.WARNING]: warnings.simplefilter("ignore") def _make_rich_console( @@ -177,32 +191,17 @@ class Environment: force_terminal: bool ) -> 'Console': from rich.console import Console - from rich.theme import Theme - from rich.style import Style - - style = getattr(self.args, 'style', palette.AUTO_STYLE) - theme = {} - if style in palette.STYLE_SHADES: - shade = palette.STYLE_SHADES[style] - theme.update({ - color: Style( - color=palette.get_color( - color, - shade, - palette=palette.RICH_THEME_PALETTE - ), - bold=True - ) - for color in palette.RICH_THEME_PALETTE - }) + from httpie.output.ui.rich_palette import _make_rich_color_theme + style = getattr(self.args, 'style', None) + theme = _make_rich_color_theme(style) # Rich infers the rest of the knowledge (e.g encoding) # dynamically by looking at the file/stderr. return Console( file=file, force_terminal=force_terminal, no_color=(self.colors == 0), - theme=Theme(theme) + theme=theme ) # Rich recommends separating the actual console (stdout) from diff --git a/httpie/core.py b/httpie/core.py index 71ecfa08..2259c4ad 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -13,7 +13,7 @@ from . import __version__ as httpie_version from .cli.constants import OUT_REQ_BODY from .cli.nested_json import HTTPieSyntaxError from .client import collect_messages -from .context import Environment, Levels +from .context import Environment, LogLevel from .downloads import Downloader from .models import ( RequestsMessageKind, @@ -223,7 +223,7 @@ def program(args: argparse.Namespace, env: Environment) -> ExitStatus: if args.check_status or downloader: exit_status = http_status_to_exit_status(http_status=message.status_code, follow=args.follow) if exit_status != ExitStatus.SUCCESS and (not env.stdout_isatty or args.quiet == 1): - env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=Levels.WARNING) + env.log_error(f'HTTP {message.raw.status} {message.raw.reason}', level=LogLevel.WARNING) write_message( requests_message=message, env=env, diff --git a/httpie/output/formatters/colors.py b/httpie/output/formatters/colors.py index 4bafe732..0d9bd12e 100644 --- a/httpie/output/formatters/colors.py +++ b/httpie/output/formatters/colors.py @@ -17,13 +17,15 @@ from pygments.util import ClassNotFound from ..lexers.json import EnhancedJsonLexer from ..lexers.metadata import MetadataLexer -from ..ui.palette import AUTO_STYLE, SHADE_NAMES, get_color +from ..ui.palette import AUTO_STYLE, SHADE_TO_PIE_STYLE, PieColor, ColorString, get_color from ...context import Environment from ...plugins import FormatterPlugin DEFAULT_STYLE = AUTO_STYLE SOLARIZED_STYLE = 'solarized' # Bundled here +PYGMENTS_BOLD = ColorString('bold') +PYGMENTS_ITALIC = ColorString('italic') BUNDLED_STYLES = { SOLARIZED_STYLE, @@ -253,11 +255,11 @@ class Solarized256Style(pygments.style.Style): pygments.token.Comment.Preproc: GREEN, pygments.token.Comment.Special: GREEN, pygments.token.Generic.Deleted: CYAN, - pygments.token.Generic.Emph: 'italic', + pygments.token.Generic.Emph: PYGMENTS_ITALIC, pygments.token.Generic.Error: RED, pygments.token.Generic.Heading: ORANGE, pygments.token.Generic.Inserted: GREEN, - pygments.token.Generic.Strong: 'bold', + pygments.token.Generic.Strong: PYGMENTS_BOLD, pygments.token.Generic.Subheading: ORANGE, pygments.token.Token: BASE1, pygments.token.Token.Other: ORANGE, @@ -266,86 +268,86 @@ class Solarized256Style(pygments.style.Style): PIE_HEADER_STYLE = { # HTTP line / Headers / Etc. - pygments.token.Name.Namespace: 'bold primary', - pygments.token.Keyword.Reserved: 'bold grey', - pygments.token.Operator: 'bold grey', - pygments.token.Number: 'bold grey', - pygments.token.Name.Function.Magic: 'bold green', - pygments.token.Name.Exception: 'bold green', - pygments.token.Name.Attribute: 'blue', - pygments.token.String: 'primary', + pygments.token.Name.Namespace: PYGMENTS_BOLD | PieColor.PRIMARY, + pygments.token.Keyword.Reserved: PYGMENTS_BOLD | PieColor.GREY, + pygments.token.Operator: PYGMENTS_BOLD | PieColor.GREY, + pygments.token.Number: PYGMENTS_BOLD | PieColor.GREY, + pygments.token.Name.Function.Magic: PYGMENTS_BOLD | PieColor.GREEN, + pygments.token.Name.Exception: PYGMENTS_BOLD | PieColor.GREEN, + pygments.token.Name.Attribute: PieColor.BLUE, + pygments.token.String: PieColor.PRIMARY, # HTTP Methods - pygments.token.Name.Function: 'bold grey', - pygments.token.Name.Function.HTTP.GET: 'bold green', - pygments.token.Name.Function.HTTP.HEAD: 'bold green', - pygments.token.Name.Function.HTTP.POST: 'bold yellow', - pygments.token.Name.Function.HTTP.PUT: 'bold orange', - pygments.token.Name.Function.HTTP.PATCH: 'bold orange', - pygments.token.Name.Function.HTTP.DELETE: 'bold red', + pygments.token.Name.Function: PYGMENTS_BOLD | PieColor.GREY, + pygments.token.Name.Function.HTTP.GET: PYGMENTS_BOLD | PieColor.GREEN, + pygments.token.Name.Function.HTTP.HEAD: PYGMENTS_BOLD | PieColor.GREEN, + pygments.token.Name.Function.HTTP.POST: PYGMENTS_BOLD | PieColor.YELLOW, + pygments.token.Name.Function.HTTP.PUT: PYGMENTS_BOLD | PieColor.ORANGE, + pygments.token.Name.Function.HTTP.PATCH: PYGMENTS_BOLD | PieColor.ORANGE, + pygments.token.Name.Function.HTTP.DELETE: PYGMENTS_BOLD | PieColor.RED, # HTTP status codes - pygments.token.Number.HTTP.INFO: 'bold aqua', - pygments.token.Number.HTTP.OK: 'bold green', - pygments.token.Number.HTTP.REDIRECT: 'bold yellow', - pygments.token.Number.HTTP.CLIENT_ERR: 'bold orange', - pygments.token.Number.HTTP.SERVER_ERR: 'bold red', + pygments.token.Number.HTTP.INFO: PYGMENTS_BOLD | PieColor.AQUA, + pygments.token.Number.HTTP.OK: PYGMENTS_BOLD | PieColor.GREEN, + pygments.token.Number.HTTP.REDIRECT: PYGMENTS_BOLD | PieColor.YELLOW, + pygments.token.Number.HTTP.CLIENT_ERR: PYGMENTS_BOLD | PieColor.ORANGE, + pygments.token.Number.HTTP.SERVER_ERR: PYGMENTS_BOLD | PieColor.RED, # Metadata - pygments.token.Name.Decorator: 'grey', - pygments.token.Number.SPEED.FAST: 'bold green', - pygments.token.Number.SPEED.AVG: 'bold yellow', - pygments.token.Number.SPEED.SLOW: 'bold orange', - pygments.token.Number.SPEED.VERY_SLOW: 'bold red', + pygments.token.Name.Decorator: PieColor.GREY, + pygments.token.Number.SPEED.FAST: PYGMENTS_BOLD | PieColor.GREEN, + pygments.token.Number.SPEED.AVG: PYGMENTS_BOLD | PieColor.YELLOW, + pygments.token.Number.SPEED.SLOW: PYGMENTS_BOLD | PieColor.ORANGE, + pygments.token.Number.SPEED.VERY_SLOW: PYGMENTS_BOLD | PieColor.RED, } PIE_BODY_STYLE = { # {}[]: - pygments.token.Punctuation: 'grey', + pygments.token.Punctuation: PieColor.GREY, # Keys - pygments.token.Name.Tag: 'pink', + pygments.token.Name.Tag: PieColor.PINK, # Values - pygments.token.Literal.String: 'green', - pygments.token.Literal.String.Double: 'green', - pygments.token.Literal.Number: 'aqua', - pygments.token.Keyword: 'orange', + pygments.token.Literal.String: PieColor.GREEN, + pygments.token.Literal.String.Double: PieColor.GREEN, + pygments.token.Literal.Number: PieColor.AQUA, + pygments.token.Keyword: PieColor.ORANGE, # Other stuff - pygments.token.Text: 'primary', - pygments.token.Name.Attribute: 'primary', - pygments.token.Name.Builtin: 'blue', - pygments.token.Name.Builtin.Pseudo: 'blue', - pygments.token.Name.Class: 'blue', - pygments.token.Name.Constant: 'orange', - pygments.token.Name.Decorator: 'blue', - pygments.token.Name.Entity: 'orange', - pygments.token.Name.Exception: 'yellow', - pygments.token.Name.Function: 'blue', - pygments.token.Name.Variable: 'blue', - pygments.token.String: 'aqua', - pygments.token.String.Backtick: 'secondary', - pygments.token.String.Char: 'aqua', - pygments.token.String.Doc: 'aqua', - pygments.token.String.Escape: 'red', - pygments.token.String.Heredoc: 'aqua', - pygments.token.String.Regex: 'red', - pygments.token.Number: 'aqua', - pygments.token.Operator: 'primary', - pygments.token.Operator.Word: 'green', - pygments.token.Comment: 'secondary', - pygments.token.Comment.Preproc: 'green', - pygments.token.Comment.Special: 'green', - pygments.token.Generic.Deleted: 'aqua', - pygments.token.Generic.Emph: 'italic', - pygments.token.Generic.Error: 'red', - pygments.token.Generic.Heading: 'orange', - pygments.token.Generic.Inserted: 'green', - pygments.token.Generic.Strong: 'bold', - pygments.token.Generic.Subheading: 'orange', - pygments.token.Token: 'primary', - pygments.token.Token.Other: 'orange', + pygments.token.Text: PieColor.PRIMARY, + pygments.token.Name.Attribute: PieColor.PRIMARY, + pygments.token.Name.Builtin: PieColor.BLUE, + pygments.token.Name.Builtin.Pseudo: PieColor.BLUE, + pygments.token.Name.Class: PieColor.BLUE, + pygments.token.Name.Constant: PieColor.ORANGE, + pygments.token.Name.Decorator: PieColor.BLUE, + pygments.token.Name.Entity: PieColor.ORANGE, + pygments.token.Name.Exception: PieColor.YELLOW, + pygments.token.Name.Function: PieColor.BLUE, + pygments.token.Name.Variable: PieColor.BLUE, + pygments.token.String: PieColor.AQUA, + pygments.token.String.Backtick: PieColor.SECONDARY, + pygments.token.String.Char: PieColor.AQUA, + pygments.token.String.Doc: PieColor.AQUA, + pygments.token.String.Escape: PieColor.RED, + pygments.token.String.Heredoc: PieColor.AQUA, + pygments.token.String.Regex: PieColor.RED, + pygments.token.Number: PieColor.AQUA, + pygments.token.Operator: PieColor.PRIMARY, + pygments.token.Operator.Word: PieColor.GREEN, + pygments.token.Comment: PieColor.SECONDARY, + pygments.token.Comment.Preproc: PieColor.GREEN, + pygments.token.Comment.Special: PieColor.GREEN, + pygments.token.Generic.Deleted: PieColor.AQUA, + pygments.token.Generic.Emph: PYGMENTS_ITALIC, + pygments.token.Generic.Error: PieColor.RED, + pygments.token.Generic.Heading: PieColor.ORANGE, + pygments.token.Generic.Inserted: PieColor.GREEN, + pygments.token.Generic.Strong: PYGMENTS_BOLD, + pygments.token.Generic.Subheading: PieColor.ORANGE, + pygments.token.Token: PieColor.PRIMARY, + pygments.token.Token.Other: PieColor.ORANGE, } @@ -369,7 +371,7 @@ def make_style(name, raw_styles, shade): def make_styles(): styles = {} - for shade, name in SHADE_NAMES.items(): + for shade, name in SHADE_TO_PIE_STYLE.items(): styles[name] = [ make_style(name, style_map, shade) for style_name, style_map in [ diff --git a/httpie/output/ui/man_pages.py b/httpie/output/ui/man_pages.py index 5871e21d..5ab0f2d7 100644 --- a/httpie/output/ui/man_pages.py +++ b/httpie/output/ui/man_pages.py @@ -18,7 +18,7 @@ def is_available(program: str) -> bool: [MAN_COMMAND, program], shell=False, stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL + stderr=subprocess.DEVNULL, ) return process.returncode == 0 @@ -27,7 +27,5 @@ def display_for(env: Environment, program: str) -> None: """Display the man page for the given command (http/https).""" subprocess.run( - [MAN_COMMAND, program], - stdout=env.stdout, - stderr=env.stderr + [MAN_COMMAND, program], stdout=env.stdout, stderr=env.stderr ) diff --git a/httpie/output/ui/palette.py b/httpie/output/ui/palette.py index 87b962e4..2b10a0b7 100644 --- a/httpie/output/ui/palette.py +++ b/httpie/output/ui/palette.py @@ -1,16 +1,97 @@ -from typing import Dict, Optional +from enum import Enum, auto +from typing import Optional + + +PYGMENTS_BRIGHT_BLACK = 'ansibrightblack' AUTO_STYLE = 'auto' # Follows terminal ANSI color styles -STYLE_PIE = 'pie' -STYLE_PIE_DARK = 'pie-dark' -STYLE_PIE_LIGHT = 'pie-light' +class Styles(Enum): + PIE = auto() + ANSI = auto() + + +class PieStyle(str, Enum): + UNIVERSAL = 'pie' + DARK = 'pie-dark' + LIGHT = 'pie-light' + + +PIE_STYLE_TO_SHADE = { + PieStyle.DARK: '500', + PieStyle.UNIVERSAL: '600', + PieStyle.LIGHT: '700', +} +SHADE_TO_PIE_STYLE = { + shade: style for style, shade in PIE_STYLE_TO_SHADE.items() +} + + +class ColorString(str): + def __or__(self, other: str) -> 'ColorString': + """Combine a style with a property. + + E.g: PieColor.BLUE | BOLD | ITALIC + """ + return ColorString(self + ' ' + other) + + +class PieColor(ColorString, Enum): + """Styles that are available only in Pie themes.""" + + PRIMARY = 'primary' + SECONDARY = 'secondary' + + WHITE = 'white' + BLACK = 'black' + GREY = 'grey' + AQUA = 'aqua' + PURPLE = 'purple' + ORANGE = 'orange' + RED = 'red' + BLUE = 'blue' + PINK = 'pink' + GREEN = 'green' + YELLOW = 'yellow' + + +class GenericColor(Enum): + """Generic colors that are safe to use everywhere.""" + + # + + WHITE = {Styles.PIE: PieColor.WHITE, Styles.ANSI: 'white'} + BLACK = {Styles.PIE: PieColor.BLACK, Styles.ANSI: 'black'} + GREEN = {Styles.PIE: PieColor.GREEN, Styles.ANSI: 'green'} + ORANGE = {Styles.PIE: PieColor.ORANGE, Styles.ANSI: 'yellow'} + YELLOW = {Styles.PIE: PieColor.YELLOW, Styles.ANSI: 'bright_yellow'} + BLUE = {Styles.PIE: PieColor.BLUE, Styles.ANSI: 'blue'} + PINK = {Styles.PIE: PieColor.PINK, Styles.ANSI: 'bright_magenta'} + PURPLE = {Styles.PIE: PieColor.PURPLE, Styles.ANSI: 'magenta'} + RED = {Styles.PIE: PieColor.RED, Styles.ANSI: 'red'} + AQUA = {Styles.PIE: PieColor.AQUA, Styles.ANSI: 'cyan'} + GREY = {Styles.PIE: PieColor.GREY, Styles.ANSI: 'bright_black'} + + def apply_style( + self, style: Styles, *, style_name: Optional[str] = None + ) -> str: + """Apply the given style to a particular value.""" + exposed_color = self.value[style] + if style is Styles.PIE: + assert style_name is not None + shade = PIE_STYLE_TO_SHADE[PieStyle(style_name)] + return get_color(exposed_color, shade) + else: + return exposed_color + + +# noinspection PyDictCreation COLOR_PALETTE = { # Copy the brand palette - 'white': '#F5F5F0', - 'black': '#1C1818', - 'grey': { + PieColor.WHITE: '#F5F5F0', + PieColor.BLACK: '#1C1818', + PieColor.GREY: { '50': '#F5F5F0', '100': '#EDEDEB', '200': '#D1D1CF', @@ -23,7 +104,7 @@ COLOR_PALETTE = { '900': '#1C1818', 'DEFAULT': '#7D7D7D', }, - 'aqua': { + PieColor.AQUA: { '50': '#E8F0F5', '100': '#D6E3ED', '200': '#C4D9E5', @@ -36,7 +117,7 @@ COLOR_PALETTE = { '900': '#455966', 'DEFAULT': '#8CB4CD', }, - 'purple': { + PieColor.PURPLE: { '50': '#F0E0FC', '100': '#E3C7FA', '200': '#D9ADF7', @@ -49,7 +130,7 @@ COLOR_PALETTE = { '900': '#5C2982', 'DEFAULT': '#B464F0', }, - 'orange': { + PieColor.ORANGE: { '50': '#FFEDDB', '100': '#FFDEBF', '200': '#FFCFA3', @@ -62,7 +143,7 @@ COLOR_PALETTE = { '900': '#C75E0A', 'DEFAULT': '#FFA24E', }, - 'red': { + PieColor.RED: { '50': '#FFE0DE', '100': '#FFC7C4', '200': '#FFB0AB', @@ -75,7 +156,7 @@ COLOR_PALETTE = { '900': '#910A00', 'DEFAULT': '#FF665B', }, - 'blue': { + PieColor.BLUE: { '50': '#DBE3FA', '100': '#BFCFF5', '200': '#A1B8F2', @@ -88,7 +169,7 @@ COLOR_PALETTE = { '900': '#2B478F', 'DEFAULT': '#4B78E6', }, - 'pink': { + PieColor.PINK: { '50': '#FFEBFF', '100': '#FCDBFC', '200': '#FCCCFC', @@ -101,7 +182,7 @@ COLOR_PALETTE = { '900': '#8C3D8A', 'DEFAULT': '#FA9BFA', }, - 'green': { + PieColor.GREEN: { '50': '#E3F7E8', '100': '#CCF2D6', '200': '#B5EDC4', @@ -114,7 +195,7 @@ COLOR_PALETTE = { '900': '#307842', 'DEFAULT': '#73DC8C', }, - 'yellow': { + PieColor.YELLOW: { '50': '#F7F7DB', '100': '#F2F2BF', '200': '#EDEDA6', @@ -128,47 +209,38 @@ COLOR_PALETTE = { 'DEFAULT': '#DBDE52', }, } - -# Grey is the same no matter shade for the colors -COLOR_PALETTE['grey'] = { - shade: COLOR_PALETTE['grey']['500'] for shade in COLOR_PALETTE['grey'].keys() -} - -COLOR_PALETTE['primary'] = { - '700': COLOR_PALETTE['black'], - '600': 'ansibrightblack', - '500': COLOR_PALETTE['white'], -} - -COLOR_PALETTE['secondary'] = {'700': '#37523C', '600': '#6c6969', '500': '#6c6969'} +COLOR_PALETTE.update( + { + # Terminal-specific palette customizations. + PieColor.GREY: { + # Grey is the same no matter shade for the colors + shade: COLOR_PALETTE[PieColor.GREY]['500'] + for shade in COLOR_PALETTE[PieColor.GREY].keys() + }, + PieColor.PRIMARY: { + '700': COLOR_PALETTE[PieColor.BLACK], + '600': PYGMENTS_BRIGHT_BLACK, + '500': COLOR_PALETTE[PieColor.WHITE], + }, + PieColor.SECONDARY: { + '700': '#37523C', + '600': '#6c6969', + '500': '#6c6969', + }, + } +) -SHADE_NAMES = { - '500': STYLE_PIE_DARK, - '600': STYLE_PIE, - '700': STYLE_PIE_LIGHT -} - -STYLE_SHADES = { - style: shade - for shade, style in SHADE_NAMES.items() -} - -SHADES = [ - '50', - *map(str, range(100, 1000, 100)) -] +def boldify(color: PieColor) -> str: + return f'bold {color}' +# noinspection PyDefaultArgument def get_color( - color: str, - shade: str, - *, - palette: Dict[str, Dict[str, str]] = COLOR_PALETTE + color: PieColor, shade: str, *, palette=COLOR_PALETTE ) -> Optional[str]: if color not in palette: return None - color_code = palette[color] if isinstance(color_code, dict) and shade in color_code: return color_code[shade] diff --git a/httpie/output/ui/rich_help.py b/httpie/output/ui/rich_help.py index 7faa337a..0782b747 100644 --- a/httpie/output/ui/rich_help.py +++ b/httpie/output/ui/rich_help.py @@ -10,16 +10,18 @@ from rich.text import Text from httpie.cli.constants import SEPARATOR_GROUP_ALL_ITEMS from httpie.cli.options import Argument, ParserSpec, Qualifiers +from httpie.output.ui.palette import GenericColor SEPARATORS = '|'.join(map(re.escape, SEPARATOR_GROUP_ALL_ITEMS)) -STYLE_METAVAR = 'yellow' -STYLE_SWITCH = 'green' -STYLE_PROGRAM_NAME = 'bold green' -STYLE_USAGE_OPTIONAL = 'grey46' -STYLE_USAGE_REGULAR = 'white' -STYLE_USAGE_ERROR = 'red' -STYLE_USAGE_MISSING = 'yellow' +STYLE_METAVAR = GenericColor.YELLOW +STYLE_SWITCH = GenericColor.GREEN +STYLE_PROGRAM_NAME = GenericColor.GREEN # .boldify() +STYLE_USAGE_OPTIONAL = GenericColor.GREY +STYLE_USAGE_REGULAR = GenericColor.WHITE +STYLE_USAGE_ERROR = GenericColor.RED +STYLE_USAGE_MISSING = GenericColor.YELLOW +STYLE_BOLD = 'bold' MAX_CHOICE_CHARS = 80 @@ -77,7 +79,7 @@ def to_usage( # shown first shown_arguments.sort(key=lambda argument: argument.aliases, reverse=True) - text = Text(program_name or spec.program, style='bold') + text = Text(program_name or spec.program, style=STYLE_BOLD) for argument in shown_arguments: text.append(' ') diff --git a/httpie/output/ui/rich_palette.py b/httpie/output/ui/rich_palette.py index c3b6b424..3d140210 100644 --- a/httpie/output/ui/rich_palette.py +++ b/httpie/output/ui/rich_palette.py @@ -1,23 +1,65 @@ -from httpie.output.ui.palette import * # noqa +from collections import ChainMap +from typing import TYPE_CHECKING, Any, Optional + +if TYPE_CHECKING: + from rich.theme import Theme + +from httpie.output.ui.palette import GenericColor, PieStyle, Styles # noqa # Rich-specific color code declarations -# https://github.com/Textualize/rich/blob/fcd684dd3a482977cab620e71ccaebb94bf13ac9/rich/default_styles.py#L5 +# CUSTOM_STYLES = { - 'progress.description': 'white', - 'progress.data.speed': 'green', - 'progress.percentage': 'aqua', - 'progress.download': 'aqua', - 'progress.remaining': 'orange', - 'bar.complete': 'purple', - 'bar.finished': 'green', - 'bar.pulse': 'purple', - 'option': 'pink' + 'progress.description': GenericColor.WHITE, + 'progress.data.speed': GenericColor.GREEN, + 'progress.percentage': GenericColor.AQUA, + 'progress.download': GenericColor.AQUA, + 'progress.remaining': GenericColor.ORANGE, + 'bar.complete': GenericColor.PURPLE, + 'bar.finished': GenericColor.GREEN, + 'bar.pulse': GenericColor.PURPLE, + 'option': GenericColor.PINK, } -RICH_THEME_PALETTE = COLOR_PALETTE.copy() # noqa -RICH_THEME_PALETTE.update( - { - custom_style: RICH_THEME_PALETTE[color] - for custom_style, color in CUSTOM_STYLES.items() - } -) + +class _GenericColorCaster(dict): + """ + Translate GenericColor to a regular string on the attribute access + phase. + """ + + def _translate(self, key: Any) -> Any: + if isinstance(key, GenericColor): + return key.name.lower() + else: + return key + + def __getitem__(self, key: Any) -> Any: + return super().__getitem__(self._translate(key)) + + def get(self, key: Any) -> Any: + return super().get(self._translate(key)) + + +def _make_rich_color_theme(style_name: Optional[str]) -> 'Theme': + from rich.style import Style + from rich.theme import Theme + + try: + PieStyle(style_name) + except ValueError: + style = Styles.ANSI + else: + style = Styles.PIE + + theme = Theme() + for color, color_set in ChainMap( + GenericColor.__members__, CUSTOM_STYLES + ).items(): + theme.styles[color.lower()] = Style( + color=color_set.apply_style(style, style_name=style_name), + bold=style is Styles.PIE, + ) + + # E.g translate GenericColor.BLUE into blue on key access + theme.styles = _GenericColorCaster(theme.styles) + return theme diff --git a/httpie/output/ui/rich_progress.py b/httpie/output/ui/rich_progress.py index 84109c9f..d2cfd38c 100644 --- a/httpie/output/ui/rich_progress.py +++ b/httpie/output/ui/rich_progress.py @@ -28,10 +28,7 @@ class BaseDisplay: return self.env.rich_error_console def _print_summary( - self, - is_finished: bool, - observed_steps: int, - time_spent: float + self, is_finished: bool, observed_steps: int, time_spent: float ): from rich import filesize @@ -50,7 +47,9 @@ class BaseDisplay: else: total_time = f'{minutes:02d}:{seconds:0.5f}' - self.console.print(f'[progress.description]{verb}. {total_size} in {total_time} ({avg_speed}/s)') + self.console.print( + f'[progress.description]{verb}. {total_size} in {total_time} ({avg_speed}/s)' + ) class DummyDisplay(BaseDisplay): @@ -65,7 +64,9 @@ class StatusDisplay(BaseDisplay): self, *, total: Optional[float], at: float, description: str ) -> None: self.observed = at - self.description = f'[progress.description]{description}[/progress.description]' + self.description = ( + f'[progress.description]{description}[/progress.description]' + ) self.status = self.console.status(self.description, spinner='line') self.status.start() @@ -75,8 +76,12 @@ class StatusDisplay(BaseDisplay): self.observed += steps - observed_amount, observed_unit = filesize.decimal(self.observed).split() - self.status.update(status=f'{self.description} [progress.download]{observed_amount}/? {observed_unit}[/progress.download]') + observed_amount, observed_unit = filesize.decimal( + self.observed + ).split() + self.status.update( + status=f'{self.description} [progress.download]{observed_amount}/? {observed_unit}[/progress.download]' + ) def stop(self, time_spent: float) -> None: self.status.stop() @@ -85,7 +90,7 @@ class StatusDisplay(BaseDisplay): self._print_summary( is_finished=True, observed_steps=self.observed, - time_spent=time_spent + time_spent=time_spent, ) @@ -114,7 +119,7 @@ class ProgressDisplay(BaseDisplay): TimeRemainingColumn(), TransferSpeedColumn(), console=self.console, - transient=True + transient=True, ) self.progress_bar.start() self.transfer_task = self.progress_bar.add_task( @@ -132,5 +137,5 @@ class ProgressDisplay(BaseDisplay): self._print_summary( is_finished=task.finished, observed_steps=task.completed, - time_spent=time_spent + time_spent=time_spent, ) diff --git a/httpie/output/ui/rich_utils.py b/httpie/output/ui/rich_utils.py index 82567ba5..4cdd2695 100644 --- a/httpie/output/ui/rich_utils.py +++ b/httpie/output/ui/rich_utils.py @@ -11,11 +11,8 @@ def render_as_string(renderable: RenderableType) -> str: """Render any `rich` object in a fake console and return a *style-less* version of it as a string.""" - with open(os.devnull, "w") as null_stream: - fake_console = Console( - file=null_stream, - record=True - ) + with open(os.devnull, 'w') as null_stream: + fake_console = Console(file=null_stream, record=True) fake_console.print(renderable) return fake_console.export_text() diff --git a/httpie/sessions.py b/httpie/sessions.py index ca40b575..99dcdba9 100644 --- a/httpie/sessions.py +++ b/httpie/sessions.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, Optional, Union from requests.auth import AuthBase from requests.cookies import RequestsCookieJar, remove_cookie_by_name -from .context import Environment, Levels +from .context import Environment, LogLevel from .cookies import HTTPieCookiePolicy from .cli.dicts import HTTPHeadersDict from .config import BaseConfigDict, DEFAULT_CONFIG_DIR @@ -313,7 +313,7 @@ class Session(BaseConfigDict): self.env.log_error( warning, - level=Levels.WARNING + level=LogLevel.WARNING ) # We don't want to spam multiple warnings on each usage,