From 4e58a3849a120ab6cb3966d9656ce65cf7d0aefc Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Sat, 4 Aug 2012 19:12:51 +0200 Subject: [PATCH] Added exit status constants, cleaned up main(). --- httpie/__init__.py | 9 ++++ httpie/core.py | 124 +++++++++++---------------------------------- httpie/input.py | 7 +-- httpie/output.py | 82 +++++++++++++++++++++++++++++- tests/tests.py | 42 ++++++++------- 5 files changed, 149 insertions(+), 115 deletions(-) diff --git a/httpie/__init__.py b/httpie/__init__.py index 6cda0dee..2b249acc 100644 --- a/httpie/__init__.py +++ b/httpie/__init__.py @@ -5,3 +5,12 @@ HTTPie - cURL for humans. __author__ = 'Jakub Roztocil' __version__ = '0.2.7dev' __licence__ = 'BSD' + + +class EXIT: + OK = 0 + ERROR = 1 + # Used only when requested: + ERROR_HTTP_3XX = 3 + ERROR_HTTP_4XX = 4 + ERROR_HTTP_5XX = 5 diff --git a/httpie/core.py b/httpie/core.py index 9c30b279..ab7773cd 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -13,27 +13,22 @@ Invocation flow: import sys import json import errno -from itertools import chain -from functools import partial import requests import requests.auth from requests.compat import str -from .models import HTTPRequest, HTTPResponse, Environment -from .output import (OutputProcessor, RawStream, PrettyStream, - BufferedPrettyStream, EncodedStream) - -from .input import (OUT_REQ_BODY, OUT_REQ_HEAD, - OUT_RESP_HEAD, OUT_RESP_BODY) from .cli import parser +from .models import Environment +from .output import output_stream, write +from . import EXIT FORM = 'application/x-www-form-urlencoded; charset=utf-8' JSON = 'application/json; charset=utf-8' -def get_response(args, env): +def get_response(args): """Send the request and return a `request.Response`.""" auto_json = args.data and not args.form @@ -78,71 +73,19 @@ def get_response(args, env): ) -def output_stream(args, env, request, response): - """Format parts of the `request`-`response` exchange - according to `args` and `env` and return `bytes`. - - """ - - # Pick the right stream type for this exchange based on `env` and `args`. - if not env.stdout_isatty and not args.prettify: - Stream = partial( - RawStream, - chunk_size=RawStream.CHUNK_SIZE_BY_LINE - if args.stream - else RawStream.CHUNK_SIZE) - elif args.prettify: - Stream = partial( - PrettyStream if args.stream else BufferedPrettyStream, - processor=OutputProcessor(env, pygments_style=args.style), - env=env) - else: - Stream = partial(EncodedStream, env=env) - - req_h = OUT_REQ_HEAD in args.output_options - req_b = OUT_REQ_BODY in args.output_options - resp_h = OUT_RESP_HEAD in args.output_options - resp_b = OUT_RESP_BODY in args.output_options - - req = req_h or req_b - resp = resp_h or resp_b - - output = [] - - if req: - output.append(Stream( - msg=HTTPRequest(request), - with_headers=req_h, - with_body=req_b)) - - if req and resp: - output.append([b'\n\n\n']) - - if resp: - output.append(Stream( - msg=HTTPResponse(response), - with_headers=resp_h, - with_body=resp_b)) - - if env.stdout_isatty: - output.append([b'\n\n']) - - return chain(*output) - - def get_exist_status(code, allow_redirects=False): """Translate HTTP status code to exit status.""" if 300 <= code <= 399 and not allow_redirects: # Redirect - return 3 + return EXIT.ERROR_HTTP_3XX elif 400 <= code <= 499: # Client Error - return 4 + return EXIT.ERROR_HTTP_4XX elif 500 <= code <= 599: # Server Error - return 5 + return EXIT.ERROR_HTTP_5XX else: - return 0 + return EXIT.OK def main(args=sys.argv[1:], env=Environment()): @@ -151,57 +94,50 @@ def main(args=sys.argv[1:], env=Environment()): Return exit status. """ - debug = '--debug' in args - 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 + def error(msg, *args): + msg = msg % args + env.stderr.write('http: error: %s\n' % msg) + + debug = '--debug' in args + status = EXIT.OK try: args = parser.parse_args(args=args, env=env) - response = get_response(args, env) - status = 0 + response = get_response(args) if args.check_status: status = get_exist_status(response.status_code, args.allow_redirects) if status and not env.stdout_isatty: - err = 'http error: %s %s\n' % ( - response.raw.status, response.raw.reason) - env.stderr.write(err) + error('%s %s', response.raw.status, response.raw.reason) + + stream = output_stream(args, env, response.request, response) try: - # We are writing bytes so we use buffer on Python 3 - buffer = env.stdout.buffer - except AttributeError: - buffer = env.stdout - - try: - for chunk in output_stream(args, env, response.request, response): - buffer.write(chunk) - if env.stdout_isatty or args.stream: - env.stdout.flush() + write(stream=stream, + outfile=env.stdout, + flush=env.stdout_isatty or args.stream) except IOError as e: - if debug: - raise - if e.errno == errno.EPIPE: + if not debug and e.errno == errno.EPIPE: + # Ignore broken pipes unless --debug. env.stderr.write('\n') else: - env.stderr.write(str(e) + '\n') - return 1 + raise except (KeyboardInterrupt, SystemExit): if debug: raise env.stderr.write('\n') - return 1 + status = EXIT.ERROR + except Exception as e: + # TODO: distinguish between expected and unexpected errors. + # network errors vs. bugs, etc. if debug: raise - env.stderr.write(str(e) + '\n') - return 1 + error('%s: %s', type(e).__name__, str(e)) + status = EXIT.ERROR return status diff --git a/httpie/input.py b/httpie/input.py index f818e194..aca7f6a3 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -95,6 +95,10 @@ class Parser(argparse.ArgumentParser): self.env = env + if env.is_windows and not env.stdout_isatty: + self.error('Output redirection is not supported on Windows.' + ' Please use `--output FILE\' instead.') + args = super(Parser, self).parse_args(args, namespace) if args.output: @@ -131,9 +135,6 @@ class Parser(argparse.ArgumentParser): 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, fd): diff --git a/httpie/output.py b/httpie/output.py index 78ff52da..669040c9 100644 --- a/httpie/output.py +++ b/httpie/output.py @@ -2,6 +2,8 @@ """ import json +from functools import partial +from itertools import chain import pygments from pygments import token, lexer @@ -13,7 +15,9 @@ from pygments.util import ClassNotFound from requests.compat import is_windows from .solarized import Solarized256Style -from .models import Environment +from .models import HTTPRequest, HTTPResponse, Environment +from .input import (OUT_REQ_BODY, OUT_REQ_HEAD, + OUT_RESP_HEAD, OUT_RESP_BODY) # Colors on Windows via colorama aren't that great and fruity @@ -43,6 +47,82 @@ class BinarySuppressedError(Exception): # Output Streams ############################################################################### + +def write(stream, outfile, flush): + """Write the output stream.""" + try: + # Writing bytes so we use the buffer interface (Python 3). + buf = outfile.buffer + except AttributeError: + buf = outfile + + for chunk in stream: + buf.write(chunk) + if flush: + outfile.flush() + + +def output_stream(args, env, request, response): + """Build and return a chain of iterators over the `request`-`response` + exchange each of which yields `bytes` chunks. + + """ + + Stream = make_stream(env, args) + + req_h = OUT_REQ_HEAD in args.output_options + req_b = OUT_REQ_BODY in args.output_options + resp_h = OUT_RESP_HEAD in args.output_options + resp_b = OUT_RESP_BODY in args.output_options + + req = req_h or req_b + resp = resp_h or resp_b + + output = [] + + if req: + output.append(Stream( + msg=HTTPRequest(request), + with_headers=req_h, + with_body=req_b)) + + if req and resp: + output.append([b'\n\n\n']) + + if resp: + output.append(Stream( + msg=HTTPResponse(response), + with_headers=resp_h, + with_body=resp_b)) + + if env.stdout_isatty: + output.append([b'\n\n']) + + return chain(*output) + + +def make_stream(env, args): + """Pick the right stream type for based on `env` and `args`. + and wrap it with a partial with the type-specific args. + + """ + if not env.stdout_isatty and not args.prettify: + Stream = partial( + RawStream, + chunk_size=RawStream.CHUNK_SIZE_BY_LINE + if args.stream + else RawStream.CHUNK_SIZE) + elif args.prettify: + Stream = partial( + PrettyStream if args.stream else BufferedPrettyStream, + processor=OutputProcessor(env, pygments_style=args.style), + env=env) + else: + Stream = partial(EncodedStream, env=env) + + return Stream + + class BaseStream(object): """Base HTTP message stream class.""" diff --git a/tests/tests.py b/tests/tests.py index 436ff941..6b2db81e 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -25,21 +25,26 @@ import json import argparse import tempfile import unittest -try: - from unittest import skipIf -except ImportError: - def skipIf(cond, test_method): - if cond: - return test_method - return lambda self: None try: from urllib.request import urlopen except ImportError: from urllib2 import urlopen +try: + from unittest import skipIf +except ImportError: + + def skipIf(cond, reason): + def decorator(test_method): + if cond: + return lambda self: None + return test_method + return decorator + from requests.compat import is_windows, is_py26, bytes, str + ################################################################# # Utils/setup ################################################################# @@ -48,6 +53,7 @@ from requests.compat import is_windows, is_py26, bytes, str TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..'))) +from httpie import EXIT from httpie import input from httpie.models import Environment from httpie.core import main @@ -108,8 +114,10 @@ class TestEnvironment(Environment): super(TestEnvironment, self).__init__(**kwargs) -class BytesResponse(bytes): pass -class StrResponse(str): pass +class BytesResponse(bytes): + stderr = json = exit_status = None +class StrResponse(str): + stderr = json = exit_status = None def http(*args, **kwargs): @@ -139,7 +147,7 @@ def http(*args, **kwargs): sys.stderr.write(env.stderr.read()) raise except SystemExit: - exit_status = 1 + exit_status = EXIT.ERROR env.stdout.seek(0) env.stderr.seek(0) @@ -792,7 +800,7 @@ class ExitStatusTest(BaseTestCase): httpbin('/status/200') ) self.assertIn(OK, r) - self.assertEqual(r.exit_status, 0) + self.assertEqual(r.exit_status, EXIT.OK) def test_error_response_exits_0_without_check_status(self): r = http( @@ -800,7 +808,7 @@ class ExitStatusTest(BaseTestCase): httpbin('/status/500') ) self.assertIn('HTTP/1.1 500', r) - self.assertEqual(r.exit_status, 0) + self.assertEqual(r.exit_status, EXIT.OK) def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self): r = http( @@ -811,7 +819,7 @@ class ExitStatusTest(BaseTestCase): env=TestEnvironment(stdout_isatty=False,) ) self.assertIn('HTTP/1.1 301', r) - self.assertEqual(r.exit_status, 3) + self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_3XX) self.assertIn('301 moved permanently', r.stderr.lower()) def test_3xx_check_status_redirects_allowed_exits_0(self): @@ -823,7 +831,7 @@ class ExitStatusTest(BaseTestCase): ) # The redirect will be followed so 200 is expected. self.assertIn('HTTP/1.1 200 OK', r) - self.assertEqual(r.exit_status, 0) + self.assertEqual(r.exit_status, EXIT.OK) def test_4xx_check_status_exits_4(self): r = http( @@ -832,7 +840,7 @@ class ExitStatusTest(BaseTestCase): httpbin('/status/401') ) self.assertIn('HTTP/1.1 401', r) - self.assertEqual(r.exit_status, 4) + self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_4XX) # Also stderr should be empty since stdout isn't redirected. self.assertTrue(not r.stderr) @@ -843,7 +851,7 @@ class ExitStatusTest(BaseTestCase): httpbin('/status/500') ) self.assertIn('HTTP/1.1 500', r) - self.assertEqual(r.exit_status, 5) + self.assertEqual(r.exit_status, EXIT.ERROR_HTTP_5XX) class FakeWindowsTest(BaseTestCase): @@ -855,7 +863,7 @@ class FakeWindowsTest(BaseTestCase): httpbin('/get'), env=env ) - self.assertNotEqual(r.exit_status, 0) + self.assertNotEqual(r.exit_status, EXIT.OK) self.assertIn('Windows', r.stderr) self.assertIn('--output', r.stderr)