mirror of
https://github.com/httpie/cli.git
synced 2024-11-25 17:23:25 +01:00
ff6f1887b0
* Refactor tests to use a text-based standard output. (#1318) * Implement new style `--help` (#1316) * Implement man page generation (#1317) * Implement rich progress bars. (#1324) * Man page deployment & isolation. (#1325) * Remove all unsorted usages in the CLI docs * Implement isolated mode for man page generation * Add a CI job for autogenerated files * Distribute man pages through PyPI * Pin the date for man pages. (#1326) * Hide suppressed arguments from --help/man pages (#1329) * Change download spinner to line (#1328) * Regenerate autogenerated files when pushed against to master. (#1339) * Highlight options (#1340) * Additional man page enhancements (#1341) * Group options by the parent category & highlight -o/--o * Display (and underline) the METAVAR on man pages. * Make help message processing more robust (#1342) * Inherit `help` from `short_help` * Don't mirror short_help directly. * Fixup the serialization * Use `pager` and `man` on `--manual` when applicable (#1343) * Run `man $program` on --manual * Page the output of `--manual` for systems that lack man pages * Improvements over progress bars (separate bar, status line, etc.) (#1346) * Redesign the --help layout. * Make our usage of rich compatible with 9.10.0 * Add `HTTPIE_NO_MAN_PAGES` * Make tests also patch os.get_terminal_size * Generate CLI spec from HTTPie & Man Page Hook (#1354) * Generate CLI spec from HTTPie & add man page hook * Use the full command space for the option headers
219 lines
6.6 KiB
Python
219 lines
6.6 KiB
Python
import argparse
|
||
import sys
|
||
import os
|
||
import warnings
|
||
from contextlib import contextmanager
|
||
from pathlib import Path
|
||
from typing import Iterator, IO, Optional, TYPE_CHECKING
|
||
from enum import Enum
|
||
|
||
|
||
try:
|
||
import curses
|
||
except ImportError:
|
||
curses = None # Compiled w/o curses
|
||
|
||
from .compat import is_windows, cached_property
|
||
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
|
||
|
||
if TYPE_CHECKING:
|
||
from rich.console import Console
|
||
|
||
|
||
class Levels(str, Enum):
|
||
WARNING = 'warning'
|
||
ERROR = 'error'
|
||
|
||
|
||
DISPLAY_THRESHOLDS = {
|
||
Levels.WARNING: 2,
|
||
Levels.ERROR: float('inf'), # Never hide errors.
|
||
}
|
||
|
||
|
||
class Environment:
|
||
"""
|
||
Information about the execution context
|
||
(standard streams, config directory, etc).
|
||
|
||
By default, it represents the actual environment.
|
||
All of the attributes can be overwritten though, which
|
||
is used by the test suite to simulate various scenarios.
|
||
|
||
"""
|
||
args = argparse.Namespace()
|
||
is_windows: bool = is_windows
|
||
config_dir: Path = DEFAULT_CONFIG_DIR
|
||
stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)
|
||
stdin_isatty: bool = stdin.isatty() if stdin else False
|
||
stdin_encoding: str = None
|
||
stdout: IO = sys.stdout
|
||
stdout_isatty: bool = stdout.isatty()
|
||
stdout_encoding: str = None
|
||
stderr: IO = sys.stderr
|
||
stderr_isatty: bool = stderr.isatty()
|
||
colors = 256
|
||
program_name: str = 'http'
|
||
|
||
# Whether to show progress bars / status spinners etc.
|
||
show_displays: bool = True
|
||
|
||
if not is_windows:
|
||
if curses:
|
||
try:
|
||
curses.setupterm()
|
||
colors = curses.tigetnum('colors')
|
||
except curses.error:
|
||
pass
|
||
else:
|
||
# noinspection PyUnresolvedReferences
|
||
import colorama.initialise
|
||
stdout = colorama.initialise.wrap_stream(
|
||
stdout, convert=None, strip=None,
|
||
autoreset=True, wrap=True
|
||
)
|
||
stderr = colorama.initialise.wrap_stream(
|
||
stderr, convert=None, strip=None,
|
||
autoreset=True, wrap=True
|
||
)
|
||
del colorama
|
||
|
||
def __init__(self, devnull=None, **kwargs):
|
||
"""
|
||
Use keyword arguments to overwrite
|
||
any of the class attributes for this instance.
|
||
|
||
"""
|
||
assert all(hasattr(type(self), attr) for attr in kwargs.keys())
|
||
self.__dict__.update(**kwargs)
|
||
|
||
# The original STDERR unaffected by --quiet’ing.
|
||
self._orig_stderr = self.stderr
|
||
self._devnull = devnull
|
||
|
||
# Keyword arguments > stream.encoding > default UTF-8
|
||
if self.stdin and self.stdin_encoding is None:
|
||
self.stdin_encoding = getattr(
|
||
self.stdin, 'encoding', None) or UTF8
|
||
if self.stdout_encoding is None:
|
||
actual_stdout = self.stdout
|
||
if is_windows:
|
||
# noinspection PyUnresolvedReferences
|
||
from colorama import AnsiToWin32
|
||
if isinstance(self.stdout, AnsiToWin32):
|
||
# noinspection PyUnresolvedReferences
|
||
actual_stdout = self.stdout.wrapped
|
||
self.stdout_encoding = getattr(
|
||
actual_stdout, 'encoding', None) or UTF8
|
||
|
||
self.quiet = kwargs.pop('quiet', 0)
|
||
|
||
def __str__(self):
|
||
defaults = dict(type(self).__dict__)
|
||
actual = dict(defaults)
|
||
actual.update(self.__dict__)
|
||
actual['config'] = self.config
|
||
return repr_dict({
|
||
key: value
|
||
for key, value in actual.items()
|
||
if not key.startswith('_')
|
||
})
|
||
|
||
def __repr__(self):
|
||
return f'<{type(self).__name__} {self}>'
|
||
|
||
_config: Config = None
|
||
|
||
@property
|
||
def config(self) -> Config:
|
||
config = self._config
|
||
if not config:
|
||
self._config = config = Config(directory=self.config_dir)
|
||
if not config.is_new():
|
||
try:
|
||
config.load()
|
||
except ConfigFileError as e:
|
||
self.log_error(e, level='warning')
|
||
return config
|
||
|
||
@property
|
||
def devnull(self) -> IO:
|
||
if self._devnull is None:
|
||
self._devnull = open(os.devnull, 'w+')
|
||
return self._devnull
|
||
|
||
@contextmanager
|
||
def as_silent(self) -> Iterator[None]:
|
||
original_stdout = self.stdout
|
||
original_stderr = self.stderr
|
||
|
||
try:
|
||
self.stdout = self.devnull
|
||
self.stderr = self.devnull
|
||
yield
|
||
finally:
|
||
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]:
|
||
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')
|
||
|
||
def apply_warnings_filter(self) -> None:
|
||
if self.quiet >= DISPLAY_THRESHOLDS[Levels.WARNING]:
|
||
warnings.simplefilter("ignore")
|
||
|
||
def _make_rich_console(
|
||
self,
|
||
file: IO[str],
|
||
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
|
||
})
|
||
|
||
# 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)
|
||
)
|
||
|
||
# Rich recommends separting the actual console (stdout) from
|
||
# the error (stderr) console for better isolation between parts.
|
||
# https://rich.readthedocs.io/en/stable/console.html#error-console
|
||
|
||
@cached_property
|
||
def rich_console(self):
|
||
return self._make_rich_console(self.stdout, self.stdout_isatty)
|
||
|
||
@cached_property
|
||
def rich_error_console(self):
|
||
return self._make_rich_console(self.stderr, self.stderr_isatty)
|