Revorked output

Binary now works everywhere. Also added `--output FILE` for Windows.
This commit is contained in:
Jakub Roztocil 2012-07-30 10:58:16 +02:00
parent 6eed0d92eb
commit 923a8b71bd
7 changed files with 223 additions and 84 deletions

View File

@ -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``.

View File

@ -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.

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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):

View File

@ -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.
#################################################################