Added exit status constants, cleaned up main().

This commit is contained in:
Jakub Roztocil 2012-08-04 19:12:51 +02:00
parent 94c77c9bfc
commit 4e58a3849a
5 changed files with 149 additions and 115 deletions

View File

@ -5,3 +5,12 @@ HTTPie - cURL for humans.
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.2.7dev' __version__ = '0.2.7dev'
__licence__ = 'BSD' __licence__ = 'BSD'
class EXIT:
OK = 0
ERROR = 1
# Used only when requested:
ERROR_HTTP_3XX = 3
ERROR_HTTP_4XX = 4
ERROR_HTTP_5XX = 5

View File

@ -13,27 +13,22 @@ Invocation flow:
import sys import sys
import json import json
import errno import errno
from itertools import chain
from functools import partial
import requests import requests
import requests.auth import requests.auth
from requests.compat import str 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 .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' FORM = 'application/x-www-form-urlencoded; charset=utf-8'
JSON = 'application/json; 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`.""" """Send the request and return a `request.Response`."""
auto_json = args.data and not args.form 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): def get_exist_status(code, allow_redirects=False):
"""Translate HTTP status code to exit status.""" """Translate HTTP status code to exit status."""
if 300 <= code <= 399 and not allow_redirects: if 300 <= code <= 399 and not allow_redirects:
# Redirect # Redirect
return 3 return EXIT.ERROR_HTTP_3XX
elif 400 <= code <= 499: elif 400 <= code <= 499:
# Client Error # Client Error
return 4 return EXIT.ERROR_HTTP_4XX
elif 500 <= code <= 599: elif 500 <= code <= 599:
# Server Error # Server Error
return 5 return EXIT.ERROR_HTTP_5XX
else: else:
return 0 return EXIT.OK
def main(args=sys.argv[1:], env=Environment()): def main(args=sys.argv[1:], env=Environment()):
@ -151,57 +94,50 @@ def main(args=sys.argv[1:], env=Environment()):
Return exit status. Return exit status.
""" """
debug = '--debug' in args
if env.is_windows and not env.stdout_isatty: def error(msg, *args):
env.stderr.write( msg = msg % args
'http: error: Output redirection is not supported on Windows.' env.stderr.write('http: error: %s\n' % msg)
' Please use `--output FILE\' instead.\n')
return 1 debug = '--debug' in args
status = EXIT.OK
try: try:
args = parser.parse_args(args=args, env=env) args = parser.parse_args(args=args, env=env)
response = get_response(args, env) response = get_response(args)
status = 0
if args.check_status: if args.check_status:
status = get_exist_status(response.status_code, status = get_exist_status(response.status_code,
args.allow_redirects) args.allow_redirects)
if status and not env.stdout_isatty: if status and not env.stdout_isatty:
err = 'http error: %s %s\n' % ( error('%s %s', response.raw.status, response.raw.reason)
response.raw.status, response.raw.reason)
env.stderr.write(err) stream = output_stream(args, env, response.request, response)
try: try:
# We are writing bytes so we use buffer on Python 3 write(stream=stream,
buffer = env.stdout.buffer outfile=env.stdout,
except AttributeError: flush=env.stdout_isatty or args.stream)
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()
except IOError as e: except IOError as e:
if debug: if not debug and e.errno == errno.EPIPE:
raise # Ignore broken pipes unless --debug.
if e.errno == errno.EPIPE:
env.stderr.write('\n') env.stderr.write('\n')
else: else:
env.stderr.write(str(e) + '\n') raise
return 1
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
if debug: if debug:
raise raise
env.stderr.write('\n') env.stderr.write('\n')
return 1 status = EXIT.ERROR
except Exception as e: except Exception as e:
# TODO: distinguish between expected and unexpected errors.
# network errors vs. bugs, etc.
if debug: if debug:
raise raise
env.stderr.write(str(e) + '\n') error('%s: %s', type(e).__name__, str(e))
return 1 status = EXIT.ERROR
return status return status

View File

@ -95,6 +95,10 @@ class Parser(argparse.ArgumentParser):
self.env = env 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) args = super(Parser, self).parse_args(args, namespace)
if args.output: if args.output:
@ -131,9 +135,6 @@ class Parser(argparse.ArgumentParser):
None: self.env.stderr None: self.env.stderr
}.get(file, file) }.get(file, file)
#if isinstance(message, str):
# message = message.encode('utf8')
super(Parser, self)._print_message(message, file) super(Parser, self)._print_message(message, file)
def _body_from_file(self, args, fd): def _body_from_file(self, args, fd):

View File

@ -2,6 +2,8 @@
""" """
import json import json
from functools import partial
from itertools import chain
import pygments import pygments
from pygments import token, lexer from pygments import token, lexer
@ -13,7 +15,9 @@ from pygments.util import ClassNotFound
from requests.compat import is_windows from requests.compat import is_windows
from .solarized import Solarized256Style 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 # Colors on Windows via colorama aren't that great and fruity
@ -43,6 +47,82 @@ class BinarySuppressedError(Exception):
# Output Streams # 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): class BaseStream(object):
"""Base HTTP message stream class.""" """Base HTTP message stream class."""

View File

@ -25,21 +25,26 @@ import json
import argparse import argparse
import tempfile import tempfile
import unittest import unittest
try:
from unittest import skipIf
except ImportError:
def skipIf(cond, test_method):
if cond:
return test_method
return lambda self: None
try: try:
from urllib.request import urlopen from urllib.request import urlopen
except ImportError: except ImportError:
from urllib2 import urlopen 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 from requests.compat import is_windows, is_py26, bytes, str
################################################################# #################################################################
# Utils/setup # 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__)) TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..'))) sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import EXIT
from httpie import input from httpie import input
from httpie.models import Environment from httpie.models import Environment
from httpie.core import main from httpie.core import main
@ -108,8 +114,10 @@ class TestEnvironment(Environment):
super(TestEnvironment, self).__init__(**kwargs) super(TestEnvironment, self).__init__(**kwargs)
class BytesResponse(bytes): pass class BytesResponse(bytes):
class StrResponse(str): pass stderr = json = exit_status = None
class StrResponse(str):
stderr = json = exit_status = None
def http(*args, **kwargs): def http(*args, **kwargs):
@ -139,7 +147,7 @@ def http(*args, **kwargs):
sys.stderr.write(env.stderr.read()) sys.stderr.write(env.stderr.read())
raise raise
except SystemExit: except SystemExit:
exit_status = 1 exit_status = EXIT.ERROR
env.stdout.seek(0) env.stdout.seek(0)
env.stderr.seek(0) env.stderr.seek(0)
@ -792,7 +800,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/200') httpbin('/status/200')
) )
self.assertIn(OK, r) 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): def test_error_response_exits_0_without_check_status(self):
r = http( r = http(
@ -800,7 +808,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500') httpbin('/status/500')
) )
self.assertIn('HTTP/1.1 500', r) 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): def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
r = http( r = http(
@ -811,7 +819,7 @@ class ExitStatusTest(BaseTestCase):
env=TestEnvironment(stdout_isatty=False,) env=TestEnvironment(stdout_isatty=False,)
) )
self.assertIn('HTTP/1.1 301', r) 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()) self.assertIn('301 moved permanently', r.stderr.lower())
def test_3xx_check_status_redirects_allowed_exits_0(self): 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. # The redirect will be followed so 200 is expected.
self.assertIn('HTTP/1.1 200 OK', r) 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): def test_4xx_check_status_exits_4(self):
r = http( r = http(
@ -832,7 +840,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/401') httpbin('/status/401')
) )
self.assertIn('HTTP/1.1 401', r) 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. # Also stderr should be empty since stdout isn't redirected.
self.assertTrue(not r.stderr) self.assertTrue(not r.stderr)
@ -843,7 +851,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500') httpbin('/status/500')
) )
self.assertIn('HTTP/1.1 500', r) 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): class FakeWindowsTest(BaseTestCase):
@ -855,7 +863,7 @@ class FakeWindowsTest(BaseTestCase):
httpbin('/get'), httpbin('/get'),
env=env env=env
) )
self.assertNotEqual(r.exit_status, 0) self.assertNotEqual(r.exit_status, EXIT.OK)
self.assertIn('Windows', r.stderr) self.assertIn('Windows', r.stderr)
self.assertIn('--output', r.stderr) self.assertIn('--output', r.stderr)