mirror of
https://github.com/httpie/cli.git
synced 2024-11-21 23:33:12 +01:00
Remove automatic config file creation to avoid concurrency issues.
Close #788 Close #812
This commit is contained in:
parent
f0058eeaee
commit
f202f338a4
@ -11,6 +11,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
* Removed Python 2.7 support (`EOL Jan 2020 <https://www.python.org/doc/sunset-python-2/>`_).
|
||||
* Removed the default 30-second connection ``--timeout`` limit.
|
||||
* Removed Python’s default limit of 100 response headers.
|
||||
* Removed automatic config file creation to avoid concurrency issues.
|
||||
* Replaced the old collect-all-then-process handling of HTTP communication
|
||||
with one-by-one processing of each HTTP request or response as they become
|
||||
available. This means that you can see headers immediately,
|
||||
@ -26,7 +27,6 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
|
||||
* Added ``tests/`` to the PyPi package for the convenience of
|
||||
downstream package maintainers.
|
||||
* Fixed an error when ``stdin`` was a closed fd.
|
||||
* Fixed an error when the config directory was not writeable.
|
||||
* Improved ``--debug`` output formatting.
|
||||
|
||||
|
||||
|
56
README.rst
56
README.rst
@ -1487,7 +1487,8 @@ To create or reuse a different session, simple specify a different name:
|
||||
|
||||
$ http --session=user2 -a user2:password example.org X-Bar:Foo
|
||||
|
||||
Named sessions' data is stored in JSON files in the directory
|
||||
Named sessions’s data is stored in JSON files in the the ``sessions``
|
||||
subdirectory of the `config`_ directory:
|
||||
``~/.httpie/sessions/<host>/<name>.json``
|
||||
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
|
||||
|
||||
@ -1517,46 +1518,61 @@ exchange once it is created, specify the session name via
|
||||
Config
|
||||
======
|
||||
|
||||
HTTPie uses a simple JSON config file.
|
||||
HTTPie uses a simple ``config.json`` file. The file doesn’t exist by default
|
||||
but you can create it manually.
|
||||
|
||||
|
||||
|
||||
Config file location
|
||||
--------------------
|
||||
|
||||
Config file directory
|
||||
---------------------
|
||||
|
||||
The default location of the configuration file is ``~/.httpie/config.json``
|
||||
(or ``%APPDATA%\httpie\config.json`` on Windows). The config directory
|
||||
location can be changed by setting the ``HTTPIE_CONFIG_DIR``
|
||||
environment variable. To view the exact location run ``http --debug``.
|
||||
(or ``%APPDATA%\httpie\config.json`` on Windows).
|
||||
|
||||
The config directory can be changed by setting the ``$HTTPIE_CONFIG_DIR``
|
||||
environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ export HTTPIE_CONFIG_DIR=/tmp/httpie
|
||||
$ http example.org
|
||||
|
||||
To view the exact location run ``http --debug``.
|
||||
|
||||
|
||||
Configurable options
|
||||
--------------------
|
||||
|
||||
The JSON file contains an object with the following keys:
|
||||
Currently HTTPie offers a single configurable option:
|
||||
|
||||
|
||||
``default_options``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
An ``Array`` (by default empty) of default options that should be applied to
|
||||
every invocation of HTTPie.
|
||||
|
||||
For instance, you can use this option to change the default style and output
|
||||
options: ``"default_options": ["--style=fruity", "--body"]`` Another useful
|
||||
default option could be ``"--session=default"`` to make HTTPie always
|
||||
use `sessions`_ (one named ``default`` will automatically be used).
|
||||
Or you could change the implicit request content type from JSON to form by
|
||||
adding ``--form`` to the list.
|
||||
For instance, you can use this config option to change your default color theme:
|
||||
|
||||
|
||||
``__meta__``
|
||||
~~~~~~~~~~~~
|
||||
.. code-block:: bash
|
||||
|
||||
HTTPie automatically stores some of its metadata here. Please do not change.
|
||||
$ cat ~/.httpie/config.json
|
||||
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"default_options": [
|
||||
"--style=fruity"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Even though it is technically possible to include there any of HTTPie’s
|
||||
options, it is not recommended to modify the default behaviour in a way
|
||||
that would break your compatibility with the wider world as that can
|
||||
generate a lot of confusion.
|
||||
|
||||
|
||||
Un-setting previously specified options
|
||||
---------------------------------------
|
||||
|
@ -61,7 +61,6 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
def parse_args(
|
||||
self,
|
||||
env: Environment,
|
||||
program_name='http',
|
||||
args=None,
|
||||
namespace=None
|
||||
) -> argparse.Namespace:
|
||||
@ -89,7 +88,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
|
||||
if self.has_stdin_data:
|
||||
self._body_from_file(self.env.stdin)
|
||||
if not URL_SCHEME_RE.match(self.args.url):
|
||||
if os.path.basename(program_name) == 'https':
|
||||
if os.path.basename(env.program_name) == 'https':
|
||||
scheme = 'https://'
|
||||
else:
|
||||
scheme = self.args.default_scheme + "://"
|
||||
|
@ -35,7 +35,7 @@ DEFAULT_UA = f'HTTPie/{__version__}'
|
||||
|
||||
def collect_messages(
|
||||
args: argparse.Namespace,
|
||||
config_dir: Path
|
||||
config_dir: Path,
|
||||
) -> Iterable[Union[requests.PreparedRequest, requests.Response]]:
|
||||
httpie_session = None
|
||||
httpie_session_headers = None
|
||||
|
@ -15,42 +15,43 @@ DEFAULT_CONFIG_DIR = Path(os.environ.get(
|
||||
))
|
||||
|
||||
|
||||
class ConfigFileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BaseConfigDict(dict):
|
||||
name = None
|
||||
helpurl = None
|
||||
about = None
|
||||
|
||||
def _get_path(self) -> Path:
|
||||
"""Return the config file path without side-effects."""
|
||||
raise NotImplementedError()
|
||||
def __init__(self, path: Path):
|
||||
super().__init__()
|
||||
self.path = path
|
||||
|
||||
def path(self) -> Path:
|
||||
"""Return the config file path creating basedir, if needed."""
|
||||
path = self._get_path()
|
||||
def ensure_directory(self):
|
||||
try:
|
||||
path.parent.mkdir(mode=0o700, parents=True)
|
||||
self.path.parent.mkdir(mode=0o700, parents=True)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
return path
|
||||
|
||||
def is_new(self) -> bool:
|
||||
return not self._get_path().exists()
|
||||
return not self.path.exists()
|
||||
|
||||
def load(self):
|
||||
config_type = type(self).__name__.lower()
|
||||
try:
|
||||
with self.path().open('rt') as f:
|
||||
with self.path.open('rt') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except ValueError as e:
|
||||
raise ValueError(
|
||||
'Invalid %s JSON: %s [%s]' %
|
||||
(type(self).__name__, str(e), self.path())
|
||||
raise ConfigFileError(
|
||||
f'invalid {config_type} file: {e} [{self.path}]'
|
||||
)
|
||||
self.update(data)
|
||||
except IOError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
raise ConfigFileError(f'cannot read {config_type} file: {e}')
|
||||
|
||||
def save(self, fail_silently=False):
|
||||
self['__meta__'] = {
|
||||
@ -62,9 +63,17 @@ class BaseConfigDict(dict):
|
||||
if self.about:
|
||||
self['__meta__']['about'] = self.about
|
||||
|
||||
self.ensure_directory()
|
||||
|
||||
try:
|
||||
with self.path().open('w') as f:
|
||||
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
|
||||
with self.path.open('w') as f:
|
||||
json.dump(
|
||||
obj=self,
|
||||
fp=f,
|
||||
indent=4,
|
||||
sort_keys=True,
|
||||
ensure_ascii=True,
|
||||
)
|
||||
f.write('\n')
|
||||
except IOError:
|
||||
if not fail_silently:
|
||||
@ -72,27 +81,22 @@ class BaseConfigDict(dict):
|
||||
|
||||
def delete(self):
|
||||
try:
|
||||
self.path().unlink()
|
||||
self.path.unlink()
|
||||
except OSError as e:
|
||||
if e.errno != errno.ENOENT:
|
||||
raise
|
||||
|
||||
|
||||
class Config(BaseConfigDict):
|
||||
name = 'config'
|
||||
helpurl = 'https://httpie.org/doc#config'
|
||||
about = 'HTTPie configuration file'
|
||||
FILENAME = 'config.json'
|
||||
DEFAULTS = {
|
||||
'default_options': []
|
||||
}
|
||||
|
||||
def __init__(self, directory: Union[str, Path] = DEFAULT_CONFIG_DIR):
|
||||
super().__init__()
|
||||
self.update(self.DEFAULTS)
|
||||
self.directory = Path(directory)
|
||||
|
||||
def _get_path(self) -> Path:
|
||||
return self.directory / (self.name + '.json')
|
||||
super().__init__(path=self.directory / self.FILENAME)
|
||||
self.update(self.DEFAULTS)
|
||||
|
||||
@property
|
||||
def default_options(self) -> list:
|
||||
|
@ -1,3 +1,4 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Union, IO, Optional
|
||||
@ -9,7 +10,7 @@ except ImportError:
|
||||
curses = None # Compiled w/o curses
|
||||
|
||||
from httpie.compat import is_windows
|
||||
from httpie.config import DEFAULT_CONFIG_DIR, Config
|
||||
from httpie.config import DEFAULT_CONFIG_DIR, Config, ConfigFileError
|
||||
|
||||
from httpie.utils import repr_dict
|
||||
|
||||
@ -35,6 +36,7 @@ class Environment:
|
||||
stderr: IO = sys.stderr
|
||||
stderr_isatty: bool = stderr.isatty()
|
||||
colors = 256
|
||||
program_name: str = 'http'
|
||||
if not is_windows:
|
||||
if curses:
|
||||
try:
|
||||
@ -79,16 +81,6 @@ class Environment:
|
||||
self.stdout_encoding = getattr(
|
||||
actual_stdout, 'encoding', None) or 'utf8'
|
||||
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
if not hasattr(self, '_config'):
|
||||
self._config = Config(directory=self.config_dir)
|
||||
if self._config.is_new():
|
||||
self._config.save(fail_silently=True)
|
||||
else:
|
||||
self._config.load()
|
||||
return self._config
|
||||
|
||||
def __str__(self):
|
||||
defaults = dict(type(self).__dict__)
|
||||
actual = dict(defaults)
|
||||
@ -102,3 +94,21 @@ class Environment:
|
||||
|
||||
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
|
||||
|
||||
def log_error(self, msg, level='error'):
|
||||
assert level in ['error', 'warning']
|
||||
self.stderr.write(f'\n{self.program_name}: {level}: {msg}\n\n')
|
||||
|
@ -1,25 +1,25 @@
|
||||
import argparse
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from typing import Callable, List, Union
|
||||
from typing import List, Union
|
||||
|
||||
import requests
|
||||
from pygments import __version__ as pygments_version
|
||||
from requests import __version__ as requests_version
|
||||
|
||||
from httpie import __version__ as httpie_version
|
||||
from httpie.status import ExitStatus, http_status_to_exit_status
|
||||
from httpie.client import collect_messages
|
||||
from httpie.context import Environment
|
||||
from httpie.downloads import Downloader
|
||||
from httpie.output.writer import write_message, write_stream
|
||||
from httpie.plugins import plugin_manager
|
||||
from httpie.status import ExitStatus, http_status_to_exit_status
|
||||
|
||||
|
||||
def main(
|
||||
args: List[Union[str, bytes]] = sys.argv,
|
||||
env=Environment(),
|
||||
custom_log_error: Callable = None
|
||||
) -> ExitStatus:
|
||||
"""
|
||||
The main function.
|
||||
@ -30,22 +30,16 @@ def main(
|
||||
Return exit status code.
|
||||
|
||||
"""
|
||||
args = decode_raw_args(args, env.stdin_encoding)
|
||||
program_name, *args = args
|
||||
env.program_name = os.path.basename(program_name)
|
||||
args = decode_raw_args(args, env.stdin_encoding)
|
||||
plugin_manager.load_installed_plugins()
|
||||
|
||||
def log_error(msg, level='error'):
|
||||
assert level in ['error', 'warning']
|
||||
env.stderr.write(f'\n{program_name}: {level}: {msg}\n')
|
||||
|
||||
from httpie.cli.definition import parser
|
||||
|
||||
if env.config.default_options:
|
||||
args = env.config.default_options + args
|
||||
|
||||
if custom_log_error:
|
||||
log_error = custom_log_error
|
||||
|
||||
include_debug_info = '--debug' in args
|
||||
include_traceback = include_debug_info or '--traceback' in args
|
||||
|
||||
@ -59,7 +53,6 @@ def main(
|
||||
try:
|
||||
parsed_args = parser.parse_args(
|
||||
args=args,
|
||||
program_name=program_name,
|
||||
env=env,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
@ -78,7 +71,6 @@ def main(
|
||||
exit_status = program(
|
||||
args=parsed_args,
|
||||
env=env,
|
||||
log_error=log_error,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
env.stderr.write('\n')
|
||||
@ -93,10 +85,10 @@ def main(
|
||||
exit_status = ExitStatus.ERROR
|
||||
except requests.Timeout:
|
||||
exit_status = ExitStatus.ERROR_TIMEOUT
|
||||
log_error(f'Request timed out ({parsed_args.timeout}s).')
|
||||
env.log_error(f'Request timed out ({parsed_args.timeout}s).')
|
||||
except requests.TooManyRedirects:
|
||||
exit_status = ExitStatus.ERROR_TOO_MANY_REDIRECTS
|
||||
log_error(
|
||||
env.log_error(
|
||||
f'Too many redirects'
|
||||
f' (--max-redirects=parsed_args.max_redirects).'
|
||||
)
|
||||
@ -110,7 +102,7 @@ def main(
|
||||
f'{msg} while doing a {request.method}'
|
||||
f' request to URL: {request.url}'
|
||||
)
|
||||
log_error(f'{type(e).__name__}: {msg}')
|
||||
env.log_error(f'{type(e).__name__}: {msg}')
|
||||
if include_traceback:
|
||||
raise
|
||||
exit_status = ExitStatus.ERROR
|
||||
@ -121,7 +113,6 @@ def main(
|
||||
def program(
|
||||
args: argparse.Namespace,
|
||||
env: Environment,
|
||||
log_error: Callable
|
||||
) -> ExitStatus:
|
||||
"""
|
||||
The main program without error handling.
|
||||
@ -159,8 +150,9 @@ def program(
|
||||
http_status=message.status_code,
|
||||
follow=args.follow
|
||||
)
|
||||
if not env.stdout_isatty and exit_status != ExitStatus.SUCCESS:
|
||||
log_error(
|
||||
if (not env.stdout_isatty
|
||||
and exit_status != ExitStatus.SUCCESS):
|
||||
env.log_error(
|
||||
f'HTTP {message.raw.status} {message.raw.reason}',
|
||||
level='warning'
|
||||
)
|
||||
@ -179,10 +171,11 @@ def program(
|
||||
downloader.finish()
|
||||
if downloader.interrupted:
|
||||
exit_status = ExitStatus.ERROR
|
||||
log_error('Incomplete download: size=%d; downloaded=%d' % (
|
||||
downloader.status.total_size,
|
||||
downloader.status.downloaded
|
||||
))
|
||||
env.log_error(
|
||||
'Incomplete download: size=%d; downloaded=%d' % (
|
||||
downloader.status.total_size,
|
||||
downloader.status.downloaded
|
||||
))
|
||||
return exit_status
|
||||
|
||||
finally:
|
||||
@ -196,11 +189,11 @@ def program(
|
||||
|
||||
def print_debug_info(env: Environment):
|
||||
env.stderr.writelines([
|
||||
'HTTPie %s\n' % httpie_version,
|
||||
'Requests %s\n' % requests_version,
|
||||
'Pygments %s\n' % pygments_version,
|
||||
'Python %s\n%s\n' % (sys.version, sys.executable),
|
||||
'%s %s' % (platform.system(), platform.release()),
|
||||
f'HTTPie {httpie_version}\n',
|
||||
f'Requests {requests_version}\n',
|
||||
f'Pygments {pygments_version}\n',
|
||||
f'Python {sys.version}\n{sys.executable}\n',
|
||||
f'{platform.system()} {platform.release()}',
|
||||
])
|
||||
env.stderr.write('\n\n')
|
||||
env.stderr.write(repr(env))
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Persistent, JSON-serialized sessions.
|
||||
"""
|
||||
Persistent, JSON-serialized sessions.
|
||||
|
||||
"""
|
||||
import os
|
||||
@ -53,8 +54,7 @@ class Session(BaseConfigDict):
|
||||
about = 'HTTPie session file'
|
||||
|
||||
def __init__(self, path: Union[str, Path]):
|
||||
super().__init__()
|
||||
self._path = Path(path)
|
||||
super().__init__(path=Path(path))
|
||||
self['headers'] = {}
|
||||
self['cookies'] = {}
|
||||
self['auth'] = {
|
||||
@ -63,9 +63,6 @@ class Session(BaseConfigDict):
|
||||
'password': None
|
||||
}
|
||||
|
||||
def _get_path(self) -> Path:
|
||||
return self._path
|
||||
|
||||
def update_headers(self, request_headers: RequestHeadersDict):
|
||||
"""
|
||||
Update the session headers with the request ones while ignoring
|
||||
|
@ -1,6 +1,5 @@
|
||||
from httpie import __version__
|
||||
from utils import MockEnvironment, http, HTTP_OK
|
||||
from httpie.context import Environment
|
||||
from httpie.config import Config
|
||||
from utils import HTTP_OK, MockEnvironment, http
|
||||
|
||||
|
||||
def test_default_options(httpbin):
|
||||
@ -8,15 +7,33 @@ def test_default_options(httpbin):
|
||||
env.config['default_options'] = ['--form']
|
||||
env.config.save()
|
||||
r = http(httpbin.url + '/post', 'foo=bar', env=env)
|
||||
assert r.json['form'] == {"foo": "bar"}
|
||||
assert r.json['form'] == {
|
||||
"foo": "bar"
|
||||
}
|
||||
|
||||
|
||||
def test_config_dir_not_writeable(httpbin):
|
||||
r = http(httpbin + '/get', env=MockEnvironment(
|
||||
config_dir='/',
|
||||
create_temp_config_dir=False,
|
||||
))
|
||||
def test_config_file_not_valid(httpbin):
|
||||
env = MockEnvironment()
|
||||
env.create_temp_config_dir()
|
||||
with (env.config_dir / Config.FILENAME).open('w') as f:
|
||||
f.write('{invalid json}')
|
||||
r = http(httpbin + '/get', env=env)
|
||||
assert HTTP_OK in r
|
||||
assert 'http: warning' in r.stderr
|
||||
assert 'invalid config file' in r.stderr
|
||||
|
||||
|
||||
def test_config_file_not_inaccessible(httpbin):
|
||||
env = MockEnvironment()
|
||||
env.create_temp_config_dir()
|
||||
config_path = env.config_dir / Config.FILENAME
|
||||
assert not config_path.exists()
|
||||
config_path.touch(0o000)
|
||||
assert config_path.exists()
|
||||
r = http(httpbin + '/get', env=env)
|
||||
assert HTTP_OK in r
|
||||
assert 'http: warning' in r.stderr
|
||||
assert 'cannot read config file' in r.stderr
|
||||
|
||||
|
||||
def test_default_options_overwrite(httpbin):
|
||||
@ -24,9 +41,6 @@ def test_default_options_overwrite(httpbin):
|
||||
env.config['default_options'] = ['--form']
|
||||
env.config.save()
|
||||
r = http('--json', httpbin.url + '/post', 'foo=bar', env=env)
|
||||
assert r.json['json'] == {"foo": "bar"}
|
||||
|
||||
|
||||
def test_current_version():
|
||||
version = MockEnvironment().config['__meta__']['httpie']
|
||||
assert version == __version__
|
||||
assert r.json['json'] == {
|
||||
"foo": "bar"
|
||||
}
|
||||
|
@ -4,37 +4,30 @@ from requests import Request
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.core import main
|
||||
from utils import HTTP_OK, http
|
||||
|
||||
|
||||
error_msg = None
|
||||
@mock.patch('httpie.core.program')
|
||||
def test_error(program):
|
||||
exc = ConnectionError('Connection aborted')
|
||||
exc.request = Request(method='GET', url='http://www.google.com')
|
||||
program.side_effect = exc
|
||||
r = http('www.google.com', tolerate_error_exit_status=True)
|
||||
assert r.exit_status == ExitStatus.ERROR
|
||||
assert (
|
||||
'ConnectionError: '
|
||||
'Connection aborted while doing a GET request to URL: '
|
||||
'http://www.google.com'
|
||||
) in r.stderr
|
||||
|
||||
|
||||
@mock.patch('httpie.core.program')
|
||||
def test_error(get_response):
|
||||
def error(msg, *args, **kwargs):
|
||||
global error_msg
|
||||
error_msg = msg % args
|
||||
|
||||
def test_error_traceback(program):
|
||||
exc = ConnectionError('Connection aborted')
|
||||
exc.request = Request(method='GET', url='http://www.google.com')
|
||||
get_response.side_effect = exc
|
||||
ret = main(['http', '--ignore-stdin', 'www.google.com'], custom_log_error=error)
|
||||
assert ret == ExitStatus.ERROR
|
||||
assert error_msg == (
|
||||
'ConnectionError: '
|
||||
'Connection aborted while doing a GET request to URL: '
|
||||
'http://www.google.com')
|
||||
|
||||
|
||||
@mock.patch('httpie.core.program')
|
||||
def test_error_traceback(get_response):
|
||||
exc = ConnectionError('Connection aborted')
|
||||
exc.request = Request(method='GET', url='http://www.google.com')
|
||||
get_response.side_effect = exc
|
||||
program.side_effect = exc
|
||||
with raises(ConnectionError):
|
||||
main(['http', '--ignore-stdin', '--traceback', 'www.google.com'])
|
||||
http('--traceback', 'www.google.com')
|
||||
|
||||
|
||||
def test_max_headers_limit(httpbin_both):
|
||||
|
@ -6,7 +6,7 @@ import time
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Optional, Union
|
||||
|
||||
from httpie.status import ExitStatus
|
||||
from httpie.config import Config
|
||||
@ -18,6 +18,7 @@ TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
||||
CRLF = '\r\n'
|
||||
COLOR = '\x1b['
|
||||
HTTP_OK = '200 OK'
|
||||
# noinspection GrazieInspection
|
||||
HTTP_OK_COLOR = (
|
||||
'HTTP\x1b[39m\x1b[38;5;245m/\x1b[39m\x1b'
|
||||
'[38;5;37m1.1\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;37m200'
|
||||
@ -62,10 +63,13 @@ class MockEnvironment(Environment):
|
||||
def config(self) -> Config:
|
||||
if (self._create_temp_config_dir
|
||||
and self._temp_dir not in self.config_dir.parents):
|
||||
self.config_dir = mk_config_dir()
|
||||
self._delete_config_dir = True
|
||||
self.create_temp_config_dir()
|
||||
return super().config
|
||||
|
||||
def create_temp_config_dir(self):
|
||||
self.config_dir = mk_config_dir()
|
||||
self._delete_config_dir = True
|
||||
|
||||
def cleanup(self):
|
||||
self.stdout.close()
|
||||
self.stderr.close()
|
||||
@ -75,6 +79,7 @@ class MockEnvironment(Environment):
|
||||
rmtree(self.config_dir)
|
||||
|
||||
def __del__(self):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
self.cleanup()
|
||||
except Exception:
|
||||
@ -83,7 +88,7 @@ class MockEnvironment(Environment):
|
||||
|
||||
class BaseCLIResponse:
|
||||
"""
|
||||
Represents the result of simulated `$ http' invocation via `http()`.
|
||||
Represents the result of simulated `$ http' invocation via `http()`.
|
||||
|
||||
Holds and provides access to:
|
||||
|
||||
@ -113,8 +118,8 @@ class StrCLIResponse(str, BaseCLIResponse):
|
||||
@property
|
||||
def json(self) -> Optional[dict]:
|
||||
"""
|
||||
Return deserialized JSON body, if one included in the output
|
||||
and is parsable.
|
||||
Return deserialized the request or response JSON body,
|
||||
if one (and only one) included in the output and is parsable.
|
||||
|
||||
"""
|
||||
if not hasattr(self, '_json'):
|
||||
@ -147,7 +152,12 @@ class ExitStatusError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def http(*args, program_name='http', **kwargs):
|
||||
def http(
|
||||
*args,
|
||||
program_name='http',
|
||||
tolerate_error_exit_status=False,
|
||||
**kwargs,
|
||||
) -> Union[StrCLIResponse, BytesCLIResponse]:
|
||||
# noinspection PyUnresolvedReferences
|
||||
"""
|
||||
Run HTTPie and capture stderr/out and exit status.
|
||||
@ -188,7 +198,6 @@ def http(*args, program_name='http', **kwargs):
|
||||
True
|
||||
|
||||
"""
|
||||
tolerate_error_exit_status = kwargs.pop('tolerate_error_exit_status', False)
|
||||
env = kwargs.get('env')
|
||||
if not env:
|
||||
env = kwargs['env'] = MockEnvironment()
|
||||
@ -200,7 +209,8 @@ def http(*args, program_name='http', **kwargs):
|
||||
args_with_config_defaults = args + env.config.default_options
|
||||
add_to_args = []
|
||||
if '--debug' not in args_with_config_defaults:
|
||||
if not tolerate_error_exit_status and '--traceback' not in args_with_config_defaults:
|
||||
if (not tolerate_error_exit_status
|
||||
and '--traceback' not in args_with_config_defaults):
|
||||
add_to_args.append('--traceback')
|
||||
if not any('--timeout' in arg for arg in args_with_config_defaults):
|
||||
add_to_args.append('--timeout=3')
|
||||
@ -228,7 +238,8 @@ def http(*args, program_name='http', **kwargs):
|
||||
sys.stderr.write(stderr.read())
|
||||
raise
|
||||
else:
|
||||
if not tolerate_error_exit_status and exit_status != ExitStatus.SUCCESS:
|
||||
if (not tolerate_error_exit_status
|
||||
and exit_status != ExitStatus.SUCCESS):
|
||||
dump_stderr()
|
||||
raise ExitStatusError(
|
||||
'httpie.core.main() unexpectedly returned'
|
||||
|
Loading…
Reference in New Issue
Block a user