mirror of
https://github.com/httpie/cli.git
synced 2024-11-28 08:38:44 +02:00
Revorked output
Binary now works everywhere. Also added `--output FILE` for Windows.
This commit is contained in:
parent
6eed0d92eb
commit
923a8b71bd
@ -334,6 +334,8 @@ Changelog
|
||||
=========
|
||||
|
||||
* `0.2.7dev`_
|
||||
* Windows: Added ``--output FILE`` to store output into a file
|
||||
(piping results into corrupted data on Windows).
|
||||
* Proper handling of binary requests and responses.
|
||||
* Fixed printing of ``multipart/form-data`` requests.
|
||||
* Renamed ``--traceback`` to ``--debug``.
|
||||
|
@ -3,6 +3,10 @@
|
||||
NOTE: the CLI interface may change before reaching v1.0.
|
||||
|
||||
"""
|
||||
import argparse
|
||||
|
||||
from requests.compat import is_windows
|
||||
|
||||
from . import __doc__
|
||||
from . import __version__
|
||||
from .output import AVAILABLE_STYLES
|
||||
@ -50,12 +54,19 @@ group_type.add_argument(
|
||||
# Output options.
|
||||
#############################################
|
||||
|
||||
|
||||
parser.add_argument(
|
||||
'--debug', action='store_true', default=False,
|
||||
help=_('''
|
||||
Prints exception traceback should one occur and other
|
||||
information useful for debugging HTTPie itself.
|
||||
''')
|
||||
'--output', '-o', type=argparse.FileType('wb'),
|
||||
metavar='FILE',
|
||||
help= argparse.SUPPRESS if not is_windows else _(
|
||||
'''
|
||||
Save output to FILE.
|
||||
This option is a replacement for piping output to FILE,
|
||||
which would on Windows result into corrupted data
|
||||
being saved.
|
||||
|
||||
'''
|
||||
)
|
||||
)
|
||||
|
||||
prettify = parser.add_mutually_exclusive_group(required=False)
|
||||
@ -200,6 +211,13 @@ parser.add_argument(
|
||||
(Use socket.setdefaulttimeout() as fallback).
|
||||
''')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--debug', action='store_true', default=False,
|
||||
help=_('''
|
||||
Prints exception traceback should one occur and other
|
||||
information useful for debugging HTTPie itself.
|
||||
''')
|
||||
)
|
||||
|
||||
|
||||
# Positional arguments.
|
||||
|
@ -148,7 +148,15 @@ def main(args=sys.argv[1:], env=Environment()):
|
||||
Return exit status.
|
||||
|
||||
"""
|
||||
|
||||
if env.is_windows and not env.stdout_isatty:
|
||||
env.stderr.write(
|
||||
'http: error: Output redirection is not supported on Windows.'
|
||||
' Please use `--output FILE\' instead.\n')
|
||||
return 1
|
||||
|
||||
args = parser.parse_args(args=args, env=env)
|
||||
|
||||
response = get_response(args, env)
|
||||
|
||||
status = 0
|
||||
@ -159,12 +167,13 @@ def main(args=sys.argv[1:], env=Environment()):
|
||||
if status and not env.stdout_isatty:
|
||||
err = 'http error: %s %s\n' % (
|
||||
response.raw.status, response.raw.reason)
|
||||
env.stderr.write(err.encode('utf8'))
|
||||
env.stderr.write(err)
|
||||
|
||||
output_bytes = get_output(args, env, response.request, response)
|
||||
output = get_output(args, env, response.request, response)
|
||||
|
||||
# output_bytes = output.encode('utf8')
|
||||
f = getattr(env.stdout, 'buffer', env.stdout)
|
||||
f.write(output_bytes)
|
||||
try:
|
||||
env.stdout.buffer.write(output)
|
||||
except AttributeError:
|
||||
env.stdout.write(output)
|
||||
|
||||
return status
|
||||
|
@ -89,8 +89,15 @@ class Parser(argparse.ArgumentParser):
|
||||
|
||||
#noinspection PyMethodOverriding
|
||||
def parse_args(self, env, args=None, namespace=None):
|
||||
|
||||
self.env = env
|
||||
|
||||
args = super(Parser, self).parse_args(args, namespace)
|
||||
|
||||
if args.output:
|
||||
env.stdout = args.output
|
||||
env.stdout_isatty = False
|
||||
|
||||
self._process_output_options(args, env)
|
||||
self._guess_method(args, env)
|
||||
self._parse_items(args)
|
||||
@ -104,9 +111,24 @@ class Parser(argparse.ArgumentParser):
|
||||
|
||||
if args.prettify == PRETTIFY_STDOUT_TTY_ONLY:
|
||||
args.prettify = env.stdout_isatty
|
||||
elif args.prettify and env.is_windows:
|
||||
self.error('Only terminal output can be prettified on Windows.')
|
||||
|
||||
return args
|
||||
|
||||
def _print_message(self, message, file=None):
|
||||
# Sneak in our stderr/stdout.
|
||||
file = {
|
||||
sys.stdout: self.env.stdout,
|
||||
sys.stderr: self.env.stderr,
|
||||
None: self.env.stderr
|
||||
}.get(file, file)
|
||||
|
||||
#if isinstance(message, str):
|
||||
# message = message.encode('utf8')
|
||||
|
||||
super(Parser, self)._print_message(message, file)
|
||||
|
||||
def _body_from_file(self, args, data):
|
||||
"""There can only be one source of request data."""
|
||||
if args.data:
|
||||
@ -398,7 +420,7 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
|
||||
target = params
|
||||
elif item.sep == SEP_FILES:
|
||||
try:
|
||||
with open(os.path.expanduser(value), 'r') as f:
|
||||
with open(os.path.expanduser(value)) as f:
|
||||
value = (os.path.basename(f.name), f.read())
|
||||
except IOError as e:
|
||||
raise ParseError(
|
||||
|
@ -10,23 +10,18 @@ class Environment(object):
|
||||
and allows for mocking.
|
||||
|
||||
"""
|
||||
|
||||
#noinspection PyUnresolvedReferences
|
||||
is_windows = is_windows
|
||||
|
||||
progname = os.path.basename(sys.argv[0])
|
||||
if progname not in ['http', 'https']:
|
||||
progname = 'http'
|
||||
|
||||
stdin_isatty = sys.stdin.isatty()
|
||||
stdin = sys.stdin
|
||||
|
||||
if is_windows:
|
||||
# `colorama` patches `sys.stdout` so its initialization
|
||||
# needs to happen before the default environment is set.
|
||||
import colorama
|
||||
colorama.init()
|
||||
del colorama
|
||||
|
||||
stdout_isatty = sys.stdout.isatty()
|
||||
stdout = sys.stdout
|
||||
|
||||
stderr = sys.stderr
|
||||
|
||||
# Can be set to 0 to disable colors completely.
|
||||
@ -35,6 +30,18 @@ class Environment(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(**kwargs)
|
||||
|
||||
def init_colors(self):
|
||||
# We check for real Window here, not self.is_windows as
|
||||
# it could be mocked.
|
||||
if (is_windows and not self.__colors_initialized
|
||||
and self.stdout == sys.stdout):
|
||||
import colorama.initialise
|
||||
self.stdout = colorama.initialise.wrap_stream(
|
||||
self.stdout, autoreset=False,
|
||||
convert=None, strip=None, wrap=True)
|
||||
self.__colors_initialized = True
|
||||
__colors_initialized = False
|
||||
|
||||
|
||||
class HTTPMessage(object):
|
||||
"""Model representing an HTTP message."""
|
||||
@ -49,7 +56,7 @@ class HTTPMessage(object):
|
||||
self.line = line # {Request,Status}-Line
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
self.encoding = encoding
|
||||
self.encoding = encoding or 'utf8'
|
||||
self.content_type = content_type
|
||||
|
||||
@classmethod
|
||||
|
@ -41,7 +41,26 @@ def format(msg, prettifier=None, with_headers=True, with_body=True,
|
||||
precision.
|
||||
|
||||
"""
|
||||
chunks = []
|
||||
|
||||
# Output encoding.
|
||||
if env.stdout_isatty:
|
||||
# Use encoding suitable for the terminal. Unsupported characters
|
||||
# will be replaced in the output.
|
||||
errors = 'replace'
|
||||
output_encoding = getattr(env.stdout, 'encoding', None)
|
||||
else:
|
||||
# Preserve the message encoding.
|
||||
errors = 'strict'
|
||||
output_encoding = msg.encoding
|
||||
if not output_encoding:
|
||||
# Default to utf8
|
||||
output_encoding = 'utf8'
|
||||
|
||||
if prettifier:
|
||||
env.init_colors()
|
||||
|
||||
#noinspection PyArgumentList
|
||||
output = bytearray()
|
||||
|
||||
if with_headers:
|
||||
headers = '\n'.join([msg.line, msg.headers])
|
||||
@ -49,37 +68,37 @@ def format(msg, prettifier=None, with_headers=True, with_body=True,
|
||||
if prettifier:
|
||||
headers = prettifier.process_headers(headers)
|
||||
|
||||
chunks.append(headers.strip().encode('utf8'))
|
||||
output.extend(
|
||||
headers.encode(output_encoding, errors).strip())
|
||||
|
||||
if with_body and msg.body or env.stdout_isatty:
|
||||
chunks.append(b'\n\n')
|
||||
if with_body and msg.body:
|
||||
output.extend(b'\n\n')
|
||||
|
||||
if with_body and msg.body:
|
||||
|
||||
body = msg.body
|
||||
bin_suppressed = False
|
||||
|
||||
if prettifier or env.stdout_isatty:
|
||||
if not (env.stdout_isatty or prettifier):
|
||||
# Verbatim body even if it's binary.
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
body = msg.body.decode(msg.encoding or 'utf8')
|
||||
body = body.decode(msg.encoding)
|
||||
except UnicodeDecodeError:
|
||||
# Assume binary
|
||||
bin_suppressed = True
|
||||
body = BINARY_SUPPRESSED_NOTICE.encode('utf8')
|
||||
# Suppress binary data.
|
||||
body = BINARY_SUPPRESSED_NOTICE.encode(output_encoding)
|
||||
if not with_headers:
|
||||
body = b'\n' + body
|
||||
output.extend(b'\n')
|
||||
else:
|
||||
body = body.encode('utf8')
|
||||
if prettifier and msg.content_type:
|
||||
body = prettifier.process_body(
|
||||
body, msg.content_type).strip()
|
||||
|
||||
if not bin_suppressed and prettifier and msg.content_type:
|
||||
body = (prettifier
|
||||
.process_body(body.decode('utf8'), msg.content_type)
|
||||
.strip()
|
||||
.encode('utf8'))
|
||||
body = body.encode(output_encoding, errors)
|
||||
|
||||
chunks.append(body)
|
||||
output.extend(body)
|
||||
|
||||
return b''.join(chunks)
|
||||
return bytes(output)
|
||||
|
||||
|
||||
class HTTPLexer(lexer.RegexLexer):
|
||||
|
150
tests/tests.py
150
tests/tests.py
@ -45,6 +45,7 @@ from httpie import input
|
||||
from httpie.models import Environment
|
||||
from httpie.core import main, get_output
|
||||
from httpie.output import BINARY_SUPPRESSED_NOTICE
|
||||
from httpie.input import ParseError
|
||||
|
||||
|
||||
HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
|
||||
@ -52,7 +53,8 @@ HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
|
||||
|
||||
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
|
||||
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'file2.txt')
|
||||
TEST_FILE_CONTENT = open(TEST_FILE_PATH).read().strip()
|
||||
with open(TEST_FILE_PATH) as f:
|
||||
TEST_FILE_CONTENT = f.read().strip()
|
||||
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
|
||||
|
||||
|
||||
@ -89,8 +91,8 @@ def http(*args, **kwargs):
|
||||
stdout_isatty=True,
|
||||
)
|
||||
|
||||
stdout = kwargs['env'].stdout = tempfile.TemporaryFile()
|
||||
stderr = kwargs['env'].stderr = tempfile.TemporaryFile()
|
||||
stdout = kwargs['env'].stdout = tempfile.TemporaryFile('w+b')
|
||||
stderr = kwargs['env'].stderr = tempfile.TemporaryFile('w+t')
|
||||
|
||||
exit_status = main(args=['--debug'] + list(args), **kwargs)
|
||||
|
||||
@ -100,8 +102,10 @@ def http(*args, **kwargs):
|
||||
output = stdout.read()
|
||||
|
||||
try:
|
||||
#noinspection PyArgumentList
|
||||
r = StrResponse(output.decode('utf8'))
|
||||
except UnicodeDecodeError:
|
||||
#noinspection PyArgumentList
|
||||
r = BytesResponse(output)
|
||||
else:
|
||||
if TERMINAL_COLOR_PRESENCE_CHECK not in r:
|
||||
@ -120,7 +124,7 @@ def http(*args, **kwargs):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
r.stderr = stderr.read().decode('utf8')
|
||||
r.stderr = stderr.read()
|
||||
r.exit_status = exit_status
|
||||
|
||||
stdout.close()
|
||||
@ -133,10 +137,10 @@ class BaseTestCase(unittest.TestCase):
|
||||
|
||||
if is_py26:
|
||||
def assertIn(self, member, container, msg=None):
|
||||
self.assert_(member in container, msg)
|
||||
self.assertTrue(member in container, msg)
|
||||
|
||||
def assertNotIn(self, member, container, msg=None):
|
||||
self.assert_(member not in container, msg)
|
||||
self.assertTrue(member not in container, msg)
|
||||
|
||||
def assertDictEqual(self, d1, d2, msg=None):
|
||||
self.assertEqual(set(d1.keys()), set(d2.keys()), msg)
|
||||
@ -206,19 +210,20 @@ class HTTPieTest(BaseTestCase):
|
||||
|
||||
def test_POST_stdin(self):
|
||||
|
||||
env = Environment(
|
||||
stdin=open(TEST_FILE_PATH),
|
||||
stdin_isatty=False,
|
||||
stdout_isatty=True,
|
||||
colors=0,
|
||||
)
|
||||
with open(TEST_FILE_PATH) as f:
|
||||
env = Environment(
|
||||
stdin=f,
|
||||
stdin_isatty=False,
|
||||
stdout_isatty=True,
|
||||
colors=0,
|
||||
)
|
||||
|
||||
r = http(
|
||||
'--form',
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
env=env
|
||||
)
|
||||
r = http(
|
||||
'--form',
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
env=env
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
self.assertIn(TEST_FILE_CONTENT, r)
|
||||
|
||||
@ -434,17 +439,18 @@ class ImplicitHTTPMethodTest(BaseTestCase):
|
||||
self.assertIn('"foo": "bar"', r)
|
||||
|
||||
def test_implicit_POST_stdin(self):
|
||||
env = Environment(
|
||||
stdin_isatty=False,
|
||||
stdin=open(TEST_FILE_PATH),
|
||||
stdout_isatty=True,
|
||||
colors=0,
|
||||
)
|
||||
r = http(
|
||||
'--form',
|
||||
httpbin('/post'),
|
||||
env=env
|
||||
)
|
||||
with open(TEST_FILE_PATH) as f:
|
||||
env = Environment(
|
||||
stdin_isatty=False,
|
||||
stdin=f,
|
||||
stdout_isatty=True,
|
||||
colors=0,
|
||||
)
|
||||
r = http(
|
||||
'--form',
|
||||
httpbin('/post'),
|
||||
env=env
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
|
||||
|
||||
@ -500,6 +506,7 @@ class VerboseFlagTest(BaseTestCase):
|
||||
'test-header:__test__'
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
#noinspection PyUnresolvedReferences
|
||||
self.assertEqual(r.count('__test__'), 2)
|
||||
|
||||
def test_verbose_form(self):
|
||||
@ -524,18 +531,18 @@ class VerboseFlagTest(BaseTestCase):
|
||||
'baz=bar'
|
||||
)
|
||||
self.assertIn('HTTP/1.1 200', r)
|
||||
#noinspection PyUnresolvedReferences
|
||||
self.assertEqual(r.count('"baz": "bar"'), 2)
|
||||
|
||||
|
||||
class MultipartFormDataFileUploadTest(BaseTestCase):
|
||||
|
||||
def test_non_existent_file_raises_parse_error(self):
|
||||
self.assertRaises(SystemExit, http,
|
||||
self.assertRaises(ParseError, http,
|
||||
'--form',
|
||||
'--traceback',
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'foo@/__does_not_exist__'
|
||||
'foo@/__does_not_exist__',
|
||||
)
|
||||
|
||||
def test_upload_ok(self):
|
||||
@ -552,6 +559,7 @@ class MultipartFormDataFileUploadTest(BaseTestCase):
|
||||
self.assertIn('Content-Disposition: form-data; name="foo"', r)
|
||||
self.assertIn('Content-Disposition: form-data; name="test-file";'
|
||||
' filename="%s"' % os.path.basename(TEST_FILE_PATH), r)
|
||||
#noinspection PyUnresolvedReferences
|
||||
self.assertEqual(r.count(TEST_FILE_CONTENT), 2)
|
||||
self.assertIn('"foo": "bar"', r)
|
||||
|
||||
@ -620,19 +628,38 @@ class RequestBodyFromFilePathTest(BaseTestCase):
|
||||
self.assertIn('"Content-Type": "x-foo/bar"', r)
|
||||
|
||||
def test_request_body_from_file_by_path_no_field_name_allowed(self):
|
||||
self.assertRaises(SystemExit, lambda: http(
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'field-name@' + TEST_FILE_PATH)
|
||||
)
|
||||
env = Environment(stdin_isatty=True)
|
||||
try:
|
||||
http(
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'field-name@' + TEST_FILE_PATH,
|
||||
env=env
|
||||
)
|
||||
except SystemExit:
|
||||
env.stderr.seek(0)
|
||||
stderr = env.stderr.read()
|
||||
self.assertIn('perhaps you meant --form?', stderr)
|
||||
else:
|
||||
self.fail('validation did not work')
|
||||
|
||||
def test_request_body_from_file_by_path_no_data_items_allowed(self):
|
||||
self.assertRaises(SystemExit, lambda: http(
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'@' + TEST_FILE_PATH,
|
||||
'foo=bar')
|
||||
)
|
||||
env = Environment(stdin_isatty=True)
|
||||
try:
|
||||
http(
|
||||
'POST',
|
||||
httpbin('/post'),
|
||||
'@' + TEST_FILE_PATH,
|
||||
'foo=bar',
|
||||
env=env
|
||||
)
|
||||
except SystemExit:
|
||||
env.stderr.seek(0)
|
||||
self.assertIn(
|
||||
'cannot be mixed',
|
||||
env.stderr.read())
|
||||
else:
|
||||
self.fail('validation did not work')
|
||||
|
||||
|
||||
class AuthTest(BaseTestCase):
|
||||
@ -727,7 +754,7 @@ class ExitStatusTest(BaseTestCase):
|
||||
self.assertIn('HTTP/1.1 401', r)
|
||||
self.assertEqual(r.exit_status, 4)
|
||||
# Also stderr should be empty since stdout isn't redirected.
|
||||
self.assert_(not r.stderr)
|
||||
self.assertTrue(not r.stderr)
|
||||
|
||||
def test_5xx_check_status_exits_5(self):
|
||||
r = http(
|
||||
@ -739,6 +766,41 @@ class ExitStatusTest(BaseTestCase):
|
||||
self.assertEqual(r.exit_status, 5)
|
||||
|
||||
|
||||
class FakeWindowsTest(BaseTestCase):
|
||||
|
||||
def test_stdout_redirect_not_supported_on_windows(self):
|
||||
env = Environment(is_windows=True, stdout_isatty=False)
|
||||
r = http(
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
env=env
|
||||
)
|
||||
self.assertNotEqual(r.exit_status, 0)
|
||||
self.assertIn('Windows', r.stderr)
|
||||
self.assertIn('--output', r.stderr)
|
||||
|
||||
def test_output_file_pretty_not_allowed_on_windows(self):
|
||||
env = Environment(
|
||||
is_windows=True, stdout_isatty=True, stdin_isatty=True)
|
||||
|
||||
try:
|
||||
http(
|
||||
'--output',
|
||||
os.path.join(tempfile.gettempdir(), '__httpie_test_output__'),
|
||||
'--pretty',
|
||||
'GET',
|
||||
httpbin('/get'),
|
||||
env=env
|
||||
)
|
||||
except SystemExit:
|
||||
env.stderr.seek(0)
|
||||
err = env.stderr.read()
|
||||
self.assertIn(
|
||||
'Only terminal output can be prettified on Windows', err)
|
||||
else:
|
||||
self.fail('validation did not work')
|
||||
|
||||
|
||||
#################################################################
|
||||
# CLI argument parsing related tests.
|
||||
#################################################################
|
||||
|
Loading…
Reference in New Issue
Block a user