codekingpro/portable-devtools
115k
1import argparse2import sys3import os4import warnings5from contextlib import contextmanager6from pathlib import Path7from typing import Iterator, IO, Optional, TYPE_CHECKING8from enum import Enum9 10 11try:12 import curses13except ImportError:14 curses = None # Compiled w/o curses15 16from .compat import is_windows, cached_property17from .config import DEFAULT_CONFIG_DIR, Config, ConfigFileError18from .encoding import UTF819 20from .utils import repr_dict21from .output.ui.palette import GenericColor22 23if TYPE_CHECKING:24 from rich.console import Console25 26 27class LogLevel(str, Enum):28 INFO = 'info'29 WARNING = 'warning'30 ERROR = 'error'31 32 33LOG_LEVEL_COLORS = {34 LogLevel.INFO: GenericColor.PINK,35 LogLevel.WARNING: GenericColor.ORANGE,36 LogLevel.ERROR: GenericColor.RED,37}38 39LOG_LEVEL_DISPLAY_THRESHOLDS = {40 LogLevel.INFO: 1,41 LogLevel.WARNING: 2,42 LogLevel.ERROR: float('inf'), # Never hide errors.43}44 45 46class Environment:47 """48 Information about the execution context49 (standard streams, config directory, etc).50 51 By default, it represents the actual environment.52 All of the attributes can be overwritten though, which53 is used by the test suite to simulate various scenarios.54 55 """56 args = argparse.Namespace()57 is_windows: bool = is_windows58 config_dir: Path = DEFAULT_CONFIG_DIR59 stdin: Optional[IO] = sys.stdin # `None` when closed fd (#791)60 stdin_isatty: bool = stdin.isatty() if stdin else False61 stdin_encoding: str = None62 stdout: IO = sys.stdout63 stdout_isatty: bool = stdout.isatty()64 stdout_encoding: str = None65 stderr: IO = sys.stderr66 stderr_isatty: bool = stderr.isatty()67 colors = 25668 program_name: str = 'http'69 70 # Whether to show progress bars / status spinners etc.71 show_displays: bool = True72 73 if not is_windows:74 if curses:75 try:76 curses.setupterm()77 colors = curses.tigetnum('colors')78 except curses.error:79 pass80 else:81 # noinspection PyUnresolvedReferences82 import colorama.initialise83 stdout = colorama.initialise.wrap_stream(84 stdout, convert=None, strip=None,85 autoreset=True, wrap=True86 )87 stderr = colorama.initialise.wrap_stream(88 stderr, convert=None, strip=None,89 autoreset=True, wrap=True90 )91 del colorama92 93 def __init__(self, devnull=None, **kwargs):94 """95 Use keyword arguments to overwrite96 any of the class attributes for this instance.97 98 """99 assert all(hasattr(type(self), attr) for attr in kwargs.keys())100 self.__dict__.update(**kwargs)101 102 # The original STDERR unaffected by --quiet’ing.103 self._orig_stderr = self.stderr104 self._devnull = devnull105 106 # Keyword arguments > stream.encoding > default UTF-8107 if self.stdin and self.stdin_encoding is None:108 self.stdin_encoding = getattr(109 self.stdin, 'encoding', None) or UTF8110 if self.stdout_encoding is None:111 actual_stdout = self.stdout112 if is_windows:113 # noinspection PyUnresolvedReferences114 from colorama import AnsiToWin32115 if isinstance(self.stdout, AnsiToWin32):116 # noinspection PyUnresolvedReferences117 actual_stdout = self.stdout.wrapped118 self.stdout_encoding = getattr(119 actual_stdout, 'encoding', None) or UTF8120 121 self.quiet = kwargs.pop('quiet', 0)122 123 def __str__(self):124 defaults = dict(type(self).__dict__)125 actual = dict(defaults)126 actual.update(self.__dict__)127 actual['config'] = self.config128 return repr_dict({129 key: value130 for key, value in actual.items()131 if not key.startswith('_')132 })133 134 def __repr__(self):135 return f'<{type(self).__name__} {self}>'136 137 _config: Config = None138 139 @property140 def config(self) -> Config:141 config = self._config142 if not config:143 self._config = config = Config(directory=self.config_dir)144 if not config.is_new():145 try:146 config.load()147 except ConfigFileError as e:148 self.log_error(e, level=LogLevel.WARNING)149 return config150 151 @property152 def devnull(self) -> IO:153 if self._devnull is None:154 self._devnull = open(os.devnull, 'w+')155 return self._devnull156 157 @contextmanager158 def as_silent(self) -> Iterator[None]:159 original_stdout = self.stdout160 original_stderr = self.stderr161 162 try:163 self.stdout = self.devnull164 self.stderr = self.devnull165 yield166 finally:167 self.stdout = original_stdout168 self.stderr = original_stderr169 170 def log_error(self, msg: str, level: LogLevel = LogLevel.ERROR) -> None:171 if self.stdout_isatty and self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[level]:172 stderr = self.stderr # Not directly /dev/null, since stderr might be mocked173 else:174 stderr = self._orig_stderr175 rich_console = self._make_rich_console(file=stderr, force_terminal=stderr.isatty())176 rich_console.print(177 f'\n{self.program_name}: {level.value}: {msg}\n\n',178 style=LOG_LEVEL_COLORS[level],179 markup=False,180 highlight=False,181 soft_wrap=True182 )183 184 def apply_warnings_filter(self) -> None:185 if self.quiet >= LOG_LEVEL_DISPLAY_THRESHOLDS[LogLevel.WARNING]:186 warnings.simplefilter("ignore")187 188 def _make_rich_console(189 self,190 file: IO[str],191 force_terminal: bool192 ) -> 'Console':193 from rich.console import Console194 from httpie.output.ui.rich_palette import _make_rich_color_theme195 196 style = getattr(self.args, 'style', None)197 theme = _make_rich_color_theme(style)198 # Rich infers the rest of the knowledge (e.g encoding)199 # dynamically by looking at the file/stderr.200 return Console(201 file=file,202 force_terminal=force_terminal,203 no_color=(self.colors == 0),204 theme=theme205 )206 207 # Rich recommends separating the actual console (stdout) from208 # the error (stderr) console for better isolation between parts.209 # https://rich.readthedocs.io/en/stable/console.html#error-console210 211 @cached_property212 def rich_console(self):213 return self._make_rich_console(self.stdout, self.stdout_isatty)214 215 @cached_property216 def rich_error_console(self):217 return self._make_rich_console(self.stderr, self.stderr_isatty)218 