1
0
mirror of https://github.com/httpie/cli.git synced 2024-11-21 17:16:30 +02:00

Refactor palette (#1378)

* Refactor palette

* Modifiers / change static strings to colors

* Colors...

* Error-based tests

* Styling linting

Co-authored-by: Jakub Roztocil <jakub@roztocil.co>
This commit is contained in:
Batuhan Taskaya 2022-05-05 18:17:05 +03:00 committed by GitHub
parent 0f9fd76852
commit f7c1bb269e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 312 additions and 195 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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."""
# <https://rich.readthedocs.io/en/stable/appendix/colors.html>
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]

View File

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

View File

@ -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
# <https://github.com/Textualize/rich/blob/fcd684dd3a482977cab620e71ccaebb94bf13ac9/rich/default_styles.py>
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

View File

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

View File

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

View File

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