mirror of
https://github.com/httpie/cli.git
synced 2024-11-25 01:03:27 +01:00
203 lines
5.0 KiB
Python
203 lines
5.0 KiB
Python
"""Utilities used by HTTPie tests.
|
|
|
|
"""
|
|
import os
|
|
import sys
|
|
import time
|
|
import json
|
|
import shutil
|
|
import tempfile
|
|
|
|
import httpie
|
|
from httpie.models import Environment
|
|
from httpie.core import main
|
|
from httpie.compat import bytes, str
|
|
|
|
|
|
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
|
|
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
|
|
'http://httpbin.org').rstrip('/')
|
|
|
|
|
|
CRLF = '\r\n'
|
|
COLOR = '\x1b['
|
|
HTTP_OK = 'HTTP/1.1 200'
|
|
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'
|
|
'\x1b[39m\x1b[38;5;245m \x1b[39m\x1b[38;5;136mOK'
|
|
)
|
|
|
|
|
|
def httpbin(path):
|
|
"""
|
|
Return a fully-qualified URL for `path`.
|
|
|
|
>>> httpbin('/get')
|
|
'http://httpbin.org/get'
|
|
|
|
"""
|
|
url = HTTPBIN_URL + path
|
|
return url
|
|
|
|
|
|
def mk_config_dir():
|
|
return tempfile.mkdtemp(prefix='httpie_test_config_dir_')
|
|
|
|
|
|
class TestEnvironment(Environment):
|
|
"""
|
|
Environment subclass with reasonable defaults suitable for testing.
|
|
|
|
"""
|
|
colors = 0
|
|
stdin_isatty = True,
|
|
stdout_isatty = True
|
|
is_windows = False
|
|
|
|
_shutil = shutil # needed by __del__ (would get gc'd)
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
if 'stdout' not in kwargs:
|
|
kwargs['stdout'] = tempfile.TemporaryFile('w+b')
|
|
|
|
if 'stderr' not in kwargs:
|
|
kwargs['stderr'] = tempfile.TemporaryFile('w+t')
|
|
|
|
self.delete_config_dir = False
|
|
if 'config_dir' not in kwargs:
|
|
kwargs['config_dir'] = mk_config_dir()
|
|
self.delete_config_dir = True
|
|
|
|
super(TestEnvironment, self).__init__(**kwargs)
|
|
|
|
def __del__(self):
|
|
if self.delete_config_dir:
|
|
self._shutil.rmtree(self.config_dir)
|
|
|
|
|
|
def http(*args, **kwargs):
|
|
"""
|
|
Invoke `httpie.core.main()` with `args` and `kwargs`,
|
|
and return a `CLIResponse` subclass.
|
|
|
|
It is either a `StrResponse`, or `BytesResponse`
|
|
if unable to decode the output.
|
|
|
|
The response has the following attributes:
|
|
|
|
`stderr`: text written to stderr
|
|
`exit_status`: the exit status
|
|
`json`: decoded JSON (if possible) or `None`
|
|
|
|
Exceptions are propagated except for SystemExit.
|
|
|
|
$ http GET example.org:
|
|
|
|
>>> r = http('GET', 'example.org')
|
|
>>> type(r) == StrCLIResponse
|
|
True
|
|
>>> r.exit_status
|
|
0
|
|
>>> r.stderr
|
|
''
|
|
>>> 'HTTP/1.1 200 OK' in r
|
|
True
|
|
>>> r.json is None
|
|
True
|
|
|
|
"""
|
|
env = kwargs.get('env')
|
|
if not env:
|
|
env = kwargs['env'] = TestEnvironment()
|
|
|
|
stdout = env.stdout
|
|
stderr = env.stderr
|
|
args = list(args)
|
|
|
|
if '--debug' not in args and '--traceback' not in args:
|
|
args = ['--traceback'] + args
|
|
try:
|
|
try:
|
|
exit_status = main(args=args, **kwargs)
|
|
if '--download' in args:
|
|
# Let the progress reporter thread finish.
|
|
time.sleep(.5)
|
|
except Exception:
|
|
sys.stderr.write(stderr.read())
|
|
raise
|
|
except SystemExit:
|
|
exit_status = httpie.ExitStatus.ERROR
|
|
|
|
stdout.seek(0)
|
|
stderr.seek(0)
|
|
output = stdout.read()
|
|
try:
|
|
# noinspection PyArgumentList
|
|
r = StrCLIResponse(output.decode('utf8'))
|
|
except UnicodeDecodeError:
|
|
# noinspection PyArgumentList
|
|
r = BytesCLIResponse(output)
|
|
r.stderr = stderr.read()
|
|
r.exit_status = exit_status
|
|
|
|
return r
|
|
|
|
finally:
|
|
stdout.close()
|
|
stderr.close()
|
|
|
|
|
|
class BaseCLIResponse(object):
|
|
"""
|
|
Represents the result of simulated `$ http' invocation via `http()`.
|
|
|
|
Holds and provides access to:
|
|
|
|
- stdout output: print(self)
|
|
- stderr output: print(self.stderr)
|
|
- exit_status output: print(self.exit_status)
|
|
|
|
"""
|
|
stderr = None
|
|
json = None
|
|
exit_status = None
|
|
|
|
|
|
class BytesCLIResponse(bytes, BaseCLIResponse):
|
|
"""
|
|
Used as a fallback when a StrCLIResponse cannot be used.
|
|
|
|
E.g. the output contains binary data. `.json` will always be None.
|
|
|
|
"""
|
|
|
|
|
|
class StrCLIResponse(str, BaseCLIResponse):
|
|
|
|
@property
|
|
def json(self):
|
|
if not hasattr(self, '_json'):
|
|
self._json = None
|
|
# De-serialize JSON body if possible.
|
|
if COLOR in self:
|
|
# Colorized output cannot be parsed.
|
|
pass
|
|
elif self.strip().startswith('{'):
|
|
# Looks like JSON body.
|
|
self._json = json.loads(self)
|
|
elif (self.count('Content-Type:') == 1
|
|
and 'application/json' in self):
|
|
# Looks like a JSON HTTP message, try to extract its body.
|
|
try:
|
|
j = self.strip()[self.strip().rindex('\r\n\r\n'):]
|
|
except ValueError:
|
|
pass
|
|
else:
|
|
try:
|
|
self._json = json.loads(j)
|
|
except ValueError:
|
|
pass
|
|
return self._json
|