summaryrefslogtreecommitdiffstats
path: root/httpie/context.py
blob: febf8c2e0b378ccc5674df8ff0192de26d004169 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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 .output.ui.palette import RichColor

if TYPE_CHECKING:
    from rich.console import Console


class LogLevel(str, Enum):
    INFO = 'info'
    WARNING = 'warning'
    ERROR = 'error'


LOG_LEVEL_COLORS = {
    LogLevel.INFO: RichColor.PINK,
    LogLevel.WARNING: RichColor.ORANGE,
    LogLevel.ERROR: RichColor.RED,
}

LOG_LEVEL_DISPLAY_THRESHOLDS = {
    LogLevel.INFO: 1,
    LogLevel.WARNING: 2,
    LogLevel.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: 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
        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 >= LOG_LEVEL_DISPLAY_THRESHOLDS[LogLevel.WARNING]:
            warnings.simplefilter("ignore")

    def _make_rich_console(
        self,
        file: IO[str],
        force_terminal: bool
    ) -> 'Console':
        from rich.console import Console
        from httpie.output.ui.palette import make_rich_theme_from_style

        style = getattr(self.args, 'style', None)
        theme = make_rich_theme_from_style(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
        )

    # Rich recommends separating 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)