1
0
mirror of https://github.com/httpie/cli.git synced 2025-03-31 21:55:16 +02:00

Streamed terminal output

`--stream` can be used to enable streaming also with `--pretty` and to ensure
a more frequent output flushing.
This commit is contained in:
Jakub Roztocil 2012-08-03 01:01:15 +02:00
parent 4615011f2e
commit c7657e3c4b
8 changed files with 615 additions and 385 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@ build
*.pyc *.pyc
.tox .tox
README.html README.html
.coverage
htmlcov

View File

@ -79,7 +79,7 @@ There are five different types of key/value pair ``items`` available:
| | nested ``Object``, or an ``Array``. It's because | | | nested ``Object``, or an ``Array``. It's because |
| | simple data items are always serialized as a | | | simple data items are always serialized as a |
| | ``String``. E.g., ``pies:=[1,2,3]``, or | | | ``String``. E.g., ``pies:=[1,2,3]``, or |
| | ``'meals:=["ham","spam"]'`` (note the quotes). | | | ``meals:='["ham","spam"]'`` (note the quotes). |
| | It may be more convenient to pass the whole JSON | | | It may be more convenient to pass the whole JSON |
| | body via ``stdin`` when it's more complex | | | body via ``stdin`` when it's more complex |
| | (see examples bellow). | | | (see examples bellow). |
@ -221,18 +221,31 @@ respectively:
esac esac
fi fi
**The output is always streamed** unless ``--pretty`` is set or implied. You
can use ``--stream`` / ``-S`` to enable streaming even with ``--pretty``, in
which case every line of the output will processed and flushed as soon as it's
avaialbe (as opossed to buffering the whole response which wouldn't work for
long-lived requests). You can test it with the Twitter streaming API:
.. code-block:: shell
http -Sfa <your-twitter-username> https://stream.twitter.com/1/statuses/filter.json track='Justin Bieber'
# \/
# The short options for --stream, --form and --auth.
``--stream`` can also be used regardless of ``--pretty`` to ensure a more
frequent output flushing (sort of like ``tail -f``).
Flags Flags
----- -----
``$ http --help``:: ``$ http --help``::
usage: http [--help] [--version] [--json | --form] [--traceback] usage: http [--help] [--version] [--json | --form] [--pretty | --ugly]
[--pretty | --ugly]
[--print OUTPUT_OPTIONS | --verbose | --headers | --body] [--print OUTPUT_OPTIONS | --verbose | --headers | --body]
[--style STYLE] [--check-status] [--auth AUTH] [--style STYLE] [--stream] [--check-status] [--auth AUTH]
[--auth-type {basic,digest}] [--verify VERIFY] [--proxy PROXY] [--auth-type {basic,digest}] [--verify VERIFY] [--proxy PROXY]
[--allow-redirects] [--timeout TIMEOUT] [--allow-redirects] [--timeout TIMEOUT] [--debug]
[METHOD] URL [ITEM [ITEM ...]] [METHOD] URL [ITEM [ITEM ...]]
HTTPie - cURL for humans. <http://httpie.org> HTTPie - cURL for humans. <http://httpie.org>
@ -266,7 +279,6 @@ Flags
-www-form-urlencoded (if not specified). The presence -www-form-urlencoded (if not specified). The presence
of any file fields results into a multipart/form-data of any file fields results into a multipart/form-data
request. request.
--traceback Print exception traceback should one occur.
--pretty If stdout is a terminal, the response is prettified by --pretty If stdout is a terminal, the response is prettified by
default (colorized and indented if it is JSON). This default (colorized and indented if it is JSON). This
flag ensures prettifying even when stdout is flag ensures prettifying even when stdout is
@ -282,7 +294,7 @@ Flags
piped to another program or to a file, then only the piped to another program or to a file, then only the
body is printed by default. body is printed by default.
--verbose, -v Print the whole request as well as the response. --verbose, -v Print the whole request as well as the response.
Shortcut for --print=HBhb. Shortcut for --print=HBbh.
--headers, -h Print only the response headers. Shortcut for --headers, -h Print only the response headers. Shortcut for
--print=h. --print=h.
--body, -b Print only the response body. Shortcut for --print=b. --body, -b Print only the response body. Shortcut for --print=b.
@ -291,10 +303,19 @@ Flags
colorful, default, emacs, friendly, fruity, manni, colorful, default, emacs, friendly, fruity, manni,
monokai, murphy, native, pastie, perldoc, rrt, monokai, murphy, native, pastie, perldoc, rrt,
solarized, tango, trac, vim, vs. Defaults to solarized, tango, trac, vim, vs. Defaults to
solarized. For this option to work properly, please "solarized". For this option to work properly, please
make sure that the $TERM environment variable is set make sure that the $TERM environment variable is set
to "xterm-256color" or similar (e.g., via `export TERM to "xterm-256color" or similar (e.g., via `export TERM
=xterm-256color' in your ~/.bashrc). =xterm-256color' in your ~/.bashrc).
--stream, -S Always stream the output by line, i.e., behave like
`tail -f'. Without --stream and with --pretty (either
set or implied), HTTPie fetches the whole response
before it outputs the processed data. Set this option
when you want to continuously display a prettified
long-lived response, such as one from the Twitter
streaming API. It is useful also without --pretty: It
ensures that the output is flushed more often and in
smaller chunks.
--check-status By default, HTTPie exits with 0 when no network or --check-status By default, HTTPie exits with 0 when no network or
other fatal errors occur. This flag instructs HTTPie other fatal errors occur. This flag instructs HTTPie
to also check the HTTP status code and exit with an to also check the HTTP status code and exit with an
@ -321,6 +342,9 @@ Flags
POST-ing of data at new ``Location``) POST-ing of data at new ``Location``)
--timeout TIMEOUT Float describes the timeout of the request (Use --timeout TIMEOUT Float describes the timeout of the request (Use
socket.setdefaulttimeout() as fallback). socket.setdefaulttimeout() as fallback).
--debug Prints exception traceback should one occur and other
information useful for debugging HTTPie itself.
Contribute Contribute
@ -373,6 +397,9 @@ Changelog
========= =========
* `0.2.7dev`_ * `0.2.7dev`_
* Streamed terminal output. ``--stream`` / ``-S`` can be used to enable
streaming also with ``--pretty`` and to ensure a more frequent output
flushing.
* Support for efficient large file downloads. * Support for efficient large file downloads.
* Response body is fetched only when needed (e.g., not with ``--headers``). * Response body is fetched only when needed (e.g., not with ``--headers``).
* Improved content type matching. * Improved content type matching.

View File

@ -5,6 +5,3 @@ HTTPie - cURL for humans.
__author__ = 'Jakub Roztocil' __author__ = 'Jakub Roztocil'
__version__ = '0.2.7dev' __version__ = '0.2.7dev'
__licence__ = 'BSD' __licence__ = 'BSD'
CONTENT_TYPE = 'Content-Type'

View File

@ -9,7 +9,7 @@ from requests.compat import is_windows
from . import __doc__ from . import __doc__
from . import __version__ from . import __version__
from .output import AVAILABLE_STYLES from .output import AVAILABLE_STYLES, DEFAULT_STYLE
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
PRETTIFY_STDOUT_TTY_ONLY, PRETTIFY_STDOUT_TTY_ONLY,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS, SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
@ -56,7 +56,7 @@ group_type.add_argument(
parser.add_argument( parser.add_argument(
'--output', '-o', type=argparse.FileType('wb'), '--output', '-o', type=argparse.FileType('w+b'),
metavar='FILE', metavar='FILE',
help= argparse.SUPPRESS if not is_windows else _( help= argparse.SUPPRESS if not is_windows else _(
''' '''
@ -131,16 +131,31 @@ output_options.add_argument(
) )
parser.add_argument( parser.add_argument(
'--style', '-s', dest='style', default='solarized', metavar='STYLE', '--style', '-s', dest='style', default=DEFAULT_STYLE, metavar='STYLE',
choices=AVAILABLE_STYLES, choices=AVAILABLE_STYLES,
help=_(''' help=_('''
Output coloring style, one of %s. Defaults to solarized. Output coloring style, one of %s. Defaults to "%s".
For this option to work properly, please make sure that the For this option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color" or similar $TERM environment variable is set to "xterm-256color" or similar
(e.g., via `export TERM=xterm-256color' in your ~/.bashrc). (e.g., via `export TERM=xterm-256color' in your ~/.bashrc).
''') % ', '.join(sorted(AVAILABLE_STYLES)) ''') % (', '.join(sorted(AVAILABLE_STYLES)), DEFAULT_STYLE)
) )
parser.add_argument('--stream', '-S', action='store_true', default=False, help=_(
'''
Always stream the output by line, i.e., behave like `tail -f'.
Without --stream and with --pretty (either set or implied),
HTTPie fetches the whole response before it outputs the processed data.
Set this option when you want to continuously display a prettified
long-lived response, such as one from the Twitter streaming API.
It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks.
'''
))
parser.add_argument( parser.add_argument(
'--check-status', default=False, action='store_true', '--check-status', default=False, action='store_true',
help=_(''' help=_('''

View File

@ -3,20 +3,27 @@
Invocation flow: Invocation flow:
1. Read, validate and process the input (args, `stdin`). 1. Read, validate and process the input (args, `stdin`).
2. Create a request and send it, get the response. 2. Create and send a request.
3. Process and format the requested parts of the request-response exchange. 3. Stream, and possibly process and format, the requested parts
4. Write to `stdout` and exit. of the request-response exchange.
4. Simultaneously write to `stdout`
5. Exit.
""" """
import sys import sys
import json import json
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 .models import HTTPRequest, HTTPResponse, Environment
from .output import OutputProcessor, formatted_stream from .output import (OutputProcessor, RawStream, PrettyStream,
BufferedPrettyStream, EncodedStream)
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD, from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_HEAD, OUT_RESP_BODY) OUT_RESP_HEAD, OUT_RESP_BODY)
from .cli import parser from .cli import parser
@ -85,41 +92,50 @@ def output_stream(args, env, request, response):
""" """
prettifier = (OutputProcessor(env, pygments_style=args.style) # Pick the right stream type for this exchange based on `env` and `args`.
if args.prettify else None) 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)
with_request = (OUT_REQ_HEAD in args.output_options req_h = OUT_REQ_HEAD in args.output_options
or OUT_REQ_BODY in args.output_options) req_b = OUT_REQ_BODY in args.output_options
with_response = (OUT_RESP_HEAD in args.output_options resp_h = OUT_RESP_HEAD in args.output_options
or OUT_RESP_BODY in args.output_options) resp_b = OUT_RESP_BODY in args.output_options
if with_request: req = req_h or req_b
request_iter = formatted_stream( resp = resp_h or resp_b
output = []
if req:
output.append(Stream(
msg=HTTPRequest(request), msg=HTTPRequest(request),
env=env, with_headers=req_h,
prettifier=prettifier, with_body=req_b))
with_headers=OUT_REQ_HEAD in args.output_options,
with_body=OUT_REQ_BODY in args.output_options)
for chunk in request_iter: if req and resp:
yield chunk output.append([b'\n\n\n'])
if with_request and with_response: if resp:
yield b'\n\n\n' output.append(Stream(
if with_response:
response_iter = formatted_stream(
msg=HTTPResponse(response), msg=HTTPResponse(response),
env=env, with_headers=resp_h,
prettifier=prettifier, with_body=resp_b))
with_headers=OUT_RESP_HEAD in args.output_options,
with_body=OUT_RESP_BODY in args.output_options)
for chunk in response_iter:
yield chunk
if env.stdout_isatty: if env.stdout_isatty:
yield b'\n\n' output.append([b'\n\n'])
return chain(*output)
def get_exist_status(code, allow_redirects=False): def get_exist_status(code, allow_redirects=False):
@ -170,18 +186,30 @@ def main(args=sys.argv[1:], env=Environment()):
except AttributeError: except AttributeError:
buffer = env.stdout buffer = env.stdout
for chunk in output_stream(args, env, response.request, response): try:
buffer.write(chunk) for chunk in output_stream(args, env, response.request, response):
if env.stdout_isatty: buffer.write(chunk)
env.stdout.flush() if env.stdout_isatty or args.stream:
env.stdout.flush()
except IOError as e:
if debug:
raise
if e.errno == errno.EPIPE:
env.stderr.write('\n')
else:
env.stderr.write(str(e) + '\n')
return 1
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
if debug:
raise
env.stderr.write('\n') env.stderr.write('\n')
return 1 return 1
except Exception as e: except Exception as e:
if debug: if debug:
raise raise
env.stderr.write(str(e.message) + '\n') env.stderr.write(str(e) + '\n')
return 1 return 1
return status return status

View File

@ -18,6 +18,10 @@ class Environment(object):
if progname not in ['http', 'https']: if progname not in ['http', 'https']:
progname = 'http' progname = 'http'
if is_windows:
import colorama.initialise
colorama.initialise.init()
stdin_isatty = sys.stdin.isatty() stdin_isatty = sys.stdin.isatty()
stdin = sys.stdin stdin = sys.stdin
stdout_isatty = sys.stdout.isatty() stdout_isatty = sys.stdout.isatty()
@ -30,50 +34,65 @@ class Environment(object):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.__dict__.update(**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): class HTTPMessage(object):
"""Model representing an HTTP message.""" """Abstract class for HTTP messages."""
def __init__(self, orig): def __init__(self, orig):
self._orig = orig self._orig = orig
def iter_body(self, chunk_size):
"""Return an iterator over the body."""
raise NotImplementedError()
def iter_lines(self, chunk_size):
"""Return an iterator over the body yielding (`line`, `line_feed`)."""
raise NotImplementedError()
@property
def headers(self):
"""Return a `str` with the message's headers."""
raise NotImplementedError()
@property
def encoding(self):
"""Return a `str` with the message's encoding, if known."""
raise NotImplementedError()
@property
def body(self):
"""Return a `bytes` with the message's body."""
raise NotImplementedError()
@property @property
def content_type(self): def content_type(self):
return str(self._orig.headers.get('Content-Type', '')) """Return the message content type."""
ct = self._orig.headers.get('Content-Type', '')
if isinstance(ct, bytes):
ct = ct.decode()
return ct
class HTTPResponse(HTTPMessage): class HTTPResponse(HTTPMessage):
"""A `requests.models.Response` wrapper.""" """A `requests.models.Response` wrapper."""
def __iter__(self): def iter_body(self, chunk_size=1):
mb = 1024 * 1024 return self._orig.iter_content(chunk_size=chunk_size)
return self._orig.iter_content(chunk_size=2 * mb)
@property def iter_lines(self, chunk_size):
def line(self): for line in self._orig.iter_lines(chunk_size):
"""Return Status-Line""" yield line, b'\n'
original = self._orig.raw._original_response
return str('HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status,
reason=original.reason
))
@property @property
def headers(self): def headers(self):
return str(self._orig.raw._original_response.msg) original = self._orig.raw._original_response
status_line = 'HTTP/{version} {status} {reason}'.format(
version='.'.join(str(original.version)),
status=original.status,
reason=original.reason
)
headers = str(original.msg)
return '\n'.join([status_line, headers]).strip()
@property @property
def encoding(self): def encoding(self):
@ -89,11 +108,14 @@ class HTTPResponse(HTTPMessage):
class HTTPRequest(HTTPMessage): class HTTPRequest(HTTPMessage):
"""A `requests.models.Request` wrapper.""" """A `requests.models.Request` wrapper."""
def __iter__(self): def iter_body(self, chunk_size):
yield self.body yield self.body
def iter_lines(self, chunk_size):
yield self.body, b''
@property @property
def line(self): def headers(self):
"""Return Request-Line""" """Return Request-Line"""
url = urlparse(self._orig.url) url = urlparse(self._orig.url)
@ -111,27 +133,23 @@ class HTTPRequest(HTTPMessage):
qs += type(self._orig)._encode_params(self._orig.params) qs += type(self._orig)._encode_params(self._orig.params)
# Request-Line # Request-Line
return str('{method} {path}{query} HTTP/1.1'.format( request_line = '{method} {path}{query} HTTP/1.1'.format(
method=self._orig.method, method=self._orig.method,
path=url.path or '/', path=url.path or '/',
query=qs query=qs
)) )
@property
def headers(self):
headers = dict(self._orig.headers) headers = dict(self._orig.headers)
content_type = headers.get('Content-Type')
if isinstance(content_type, bytes):
# Happens when uploading files.
# TODO: submit a bug report for Requests
headers['Content-Type'] = str(content_type)
if 'Host' not in headers: if 'Host' not in headers:
headers['Host'] = urlparse(self._orig.url).netloc headers['Host'] = urlparse(self._orig.url).netloc
return '\n'.join('%s: %s' % (name, value) headers = ['%s: %s' % (name, value)
for name, value in headers.items()) for name, value in headers.items()]
headers.insert(0, request_line)
return '\n'.join(headers).strip()
@property @property
def encoding(self): def encoding(self):

View File

@ -1,7 +1,6 @@
"""Output processing and formatting. """Output streaming, processing and formatting.
""" """
import re
import json import json
import pygments import pygments
@ -17,92 +16,193 @@ from .solarized import Solarized256Style
from .models import Environment from .models import Environment
DEFAULT_STYLE = 'solarized' # Colors on Windows via colorama aren't that great and fruity
AVAILABLE_STYLES = [DEFAULT_STYLE] + list(STYLE_MAP.keys()) # seems to give the best result there.
DEFAULT_STYLE = 'solarized' if not is_windows else 'fruity'
#noinspection PySetFunctionToLiteral
AVAILABLE_STYLES = set([DEFAULT_STYLE]) | set(STYLE_MAP.keys())
BINARY_SUPPRESSED_NOTICE = ( BINARY_SUPPRESSED_NOTICE = (
'+-----------------------------------------+\n' b'\n'
'| NOTE: binary data not shown in terminal |\n' b'+-----------------------------------------+\n'
'+-----------------------------------------+' b'| NOTE: binary data not shown in terminal |\n'
b'+-----------------------------------------+'
) )
def formatted_stream(msg, prettifier=None, with_headers=True, with_body=True, class BinarySuppressedError(Exception):
env=Environment()): """An error indicating that the body is binary and won't be written,
"""Return an iterator yielding `bytes` representing `msg` e.g., for terminal output)."""
(a `models.HTTPMessage` subclass).
The body can be binary so we always yield `bytes`. message = BINARY_SUPPRESSED_NOTICE
If `prettifier` is set or the output is a terminal then a binary
body is not included in the output and is replaced with notice.
Generally, when the `stdout` is redirected, the output matches the actual ###############################################################################
message as much as possible (formatting and character encoding-wise). # Output Streams
When `--pretty` is set (or implied), or when the output is a terminal, ###############################################################################
then we prefer readability over precision.
class BaseStream(object):
"""Base HTTP message stream class."""
def __init__(self, msg, with_headers=True, with_body=True):
"""
:param msg: a :class:`models.HTTPMessage` subclass
:param with_headers: if `True`, headers will be included
:param with_body: if `True`, body will be included
"""
self.msg = msg
self.with_headers = with_headers
self.with_body = with_body
def _headers(self):
"""Return the headers' bytes."""
return self.msg.headers.encode('ascii')
def _body(self):
"""Return an iterator over the message body."""
raise NotImplementedError()
def __iter__(self):
"""Return an iterator over `self.msg`."""
if self.with_headers:
yield self._headers()
if self.with_body:
it = self._body()
try:
if self.with_headers:
# Yield the headers/body separator only if needed.
chunk = next(it)
if chunk:
yield b'\n\n'
yield chunk
for chunk in it:
yield chunk
except BinarySuppressedError as e:
yield e.message
class RawStream(BaseStream):
"""The message is streamed in chunks with no processing."""
CHUNK_SIZE = 1024 * 100
CHUNK_SIZE_BY_LINE = 1024 * 5
def __init__(self, chunk_size=CHUNK_SIZE, **kwargs):
super(RawStream, self).__init__(**kwargs)
self.chunk_size = chunk_size
def _body(self):
return self.msg.iter_body(self.chunk_size)
class EncodedStream(BaseStream):
"""Encoded HTTP message stream.
The message bytes are converted to an encoding suitable for
`self.env.stdout`. Unicode errors are replaced and binary data
is suppressed. The body is always streamed by line.
""" """
# Output encoding. CHUNK_SIZE = 1024 * 5
if env.stdout_isatty: def __init__(self, env=Environment(), **kwargs):
# 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: super(EncodedStream, self).__init__(**kwargs)
env.init_colors()
if with_headers: if env.stdout_isatty:
headers = '\n'.join([msg.line, msg.headers]) # Use the encoding supported by the terminal.
output_encoding = getattr(env.stdout, 'encoding', None)
else:
# Preserve the message encoding.
output_encoding = self.msg.encoding
if prettifier: # Default to utf8 when unsure.
headers = prettifier.process_headers(headers) self.output_encoding = output_encoding or 'utf8'
yield headers.encode(output_encoding, errors).strip() def _body(self):
if with_body: for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
prefix = b'\n\n' if with_headers else None if b'\0' in line:
raise BinarySuppressedError()
if not (env.stdout_isatty or prettifier): yield line.decode(self.msg.encoding)\
# Verbatim body even if it's binary. .encode(self.output_encoding, 'replace') + lf
for body_chunk in msg:
if prefix:
yield prefix
prefix = None
yield body_chunk
elif msg.body:
try:
body = msg.body.decode(msg.encoding)
except UnicodeDecodeError:
# Suppress binary data.
body = BINARY_SUPPRESSED_NOTICE.encode(output_encoding)
if not with_headers:
yield b'\n'
else:
if prettifier and msg.content_type:
body = prettifier.process_body(
body, msg.content_type).strip()
body = body.encode(output_encoding, errors)
if prefix:
yield prefix
yield body
class PrettyStream(EncodedStream):
"""In addition to :class:`EncodedStream` behaviour, this stream applies
content processing.
Useful for long-lived HTTP responses that stream by lines
such as the Twitter streaming API.
"""
CHUNK_SIZE = 1024 * 5
def __init__(self, processor, **kwargs):
super(PrettyStream, self).__init__(**kwargs)
self.processor = processor
def _headers(self):
return self.processor.process_headers(
self.msg.headers).encode(self.output_encoding)
def _body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
yield self._process_body(line) + lf
def _process_body(self, chunk):
return (self.processor
.process_body(
chunk.decode(self.msg.encoding, 'replace'),
self.msg.content_type)
.encode(self.output_encoding, 'replace'))
class BufferedPrettyStream(PrettyStream):
"""The same as :class:`PrettyStream` except that the body is fully
fetched before it's processed.
Suitable regular HTTP responses.
"""
CHUNK_SIZE = 1024 * 10
def _body(self):
#noinspection PyArgumentList
# Read the whole body before prettifying it,
# but bail out immediately if the body is binary.
body = bytearray()
for chunk in self.msg.iter_body(self.CHUNK_SIZE):
if b'\0' in chunk:
raise BinarySuppressedError()
body.extend(chunk)
yield self._process_body(body)
###############################################################################
# Processing
###############################################################################
class HTTPLexer(lexer.RegexLexer): class HTTPLexer(lexer.RegexLexer):
"""Simplified HTTP lexer for Pygments. """Simplified HTTP lexer for Pygments.
It only operates on headers and provides a stronger contrast between It only operates on headers and provides a stronger contrast between
their names and values than the original one bundled with Pygments their names and values than the original one bundled with Pygments
(`pygments.lexers.text import HttpLexer`), especially when (:class:`pygments.lexers.text import HttpLexer`), especially when
Solarized color scheme is used. Solarized color scheme is used.
""" """
@ -111,7 +211,6 @@ class HTTPLexer(lexer.RegexLexer):
filenames = ['*.http'] filenames = ['*.http']
tokens = { tokens = {
'root': [ 'root': [
# Request-Line # Request-Line
(r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)', (r'([A-Z]+)( +)([^ ]+)( +)(HTTP)(/)(\d+\.\d+)',
lexer.bygroups( lexer.bygroups(
@ -123,7 +222,6 @@ class HTTPLexer(lexer.RegexLexer):
token.Operator, token.Operator,
token.Number token.Number
)), )),
# Response Status-Line # Response Status-Line
(r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)', (r'(HTTP)(/)(\d+\.\d+)( +)(\d{3})( +)(.+)',
lexer.bygroups( lexer.bygroups(
@ -135,7 +233,6 @@ class HTTPLexer(lexer.RegexLexer):
token.Text, token.Text,
token.Name.Exception, # Reason token.Name.Exception, # Reason
)), )),
# Header # Header
(r'(.*?)( *)(:)( *)(.+)', lexer.bygroups( (r'(.*?)( *)(:)( *)(.+)', lexer.bygroups(
token.Name.Attribute, # Name token.Name.Attribute, # Name
@ -148,28 +245,48 @@ class HTTPLexer(lexer.RegexLexer):
class BaseProcessor(object): class BaseProcessor(object):
"""Base, noop output processor class."""
enabled = True enabled = True
def __init__(self, env, **kwargs): def __init__(self, env, **kwargs):
"""
:param env:
an class:`Environment` instance
:param kwargs:
additional keyword argument that some processor might require.
"""
self.env = env self.env = env
self.kwargs = kwargs self.kwargs = kwargs
def process_headers(self, headers): def process_headers(self, headers):
"""Return processed `headers`
:param headers:
The headers as text.
"""
return headers return headers
def process_body(self, content, content_type, subtype): def process_body(self, content, content_type, subtype):
"""Return processed `content`. """Return processed `content`.
:param content: `str` :param content:
:param content_type: full content type, e.g., 'application/atom+xml' The body content as text
:param subtype: e.g., 'xml'
:param content_type:
Full content type, e.g., 'application/atom+xml'.
:param subtype:
E.g. 'xml'.
""" """
return content return content
class JSONProcessor(BaseProcessor): class JSONProcessor(BaseProcessor):
"""JSON body processor."""
def process_body(self, content, content_type, subtype): def process_body(self, content, content_type, subtype):
if subtype == 'json': if subtype == 'json':
@ -187,21 +304,26 @@ class JSONProcessor(BaseProcessor):
class PygmentsProcessor(BaseProcessor): class PygmentsProcessor(BaseProcessor):
"""A processor that applies syntax-highlighting using Pygments
to the headers, and to the body as well if its content type is recognized.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PygmentsProcessor, self).__init__(*args, **kwargs) super(PygmentsProcessor, self).__init__(*args, **kwargs)
# Cache that speeds up when we process streamed body by line.
self.lexers_by_type = {}
if not self.env.colors: if not self.env.colors:
self.enabled = False self.enabled = False
return return
try: try:
style = get_style_by_name( style = get_style_by_name(self.kwargs['pygments_style'])
self.kwargs.get('pygments_style', DEFAULT_STYLE))
except ClassNotFound: except ClassNotFound:
style = Solarized256Style style = Solarized256Style
if is_windows or self.env.colors == 256: if self.env.is_windows or self.env.colors == 256:
fmt_class = Terminal256Formatter fmt_class = Terminal256Formatter
else: else:
fmt_class = TerminalFormatter fmt_class = TerminalFormatter
@ -209,24 +331,26 @@ class PygmentsProcessor(BaseProcessor):
def process_headers(self, headers): def process_headers(self, headers):
return pygments.highlight( return pygments.highlight(
headers, HTTPLexer(), self.formatter) headers, HTTPLexer(), self.formatter).strip()
def process_body(self, content, content_type, subtype): def process_body(self, content, content_type, subtype):
try: try:
try: lexer = self.lexers_by_type.get(content_type)
lexer = get_lexer_for_mimetype(content_type) if not lexer:
except ClassNotFound: try:
lexer = get_lexer_by_name(subtype) lexer = get_lexer_for_mimetype(content_type)
except ClassNotFound:
lexer = get_lexer_by_name(subtype)
self.lexers_by_type[content_type] = lexer
except ClassNotFound: except ClassNotFound:
pass pass
else: else:
content = pygments.highlight(content, lexer, self.formatter) content = pygments.highlight(content, lexer, self.formatter)
return content return content.strip()
class HeadersProcessor(BaseProcessor): class HeadersProcessor(BaseProcessor):
""" """Sorts headers by name retaining relative order of multiple headers
Sorts headers by name retaining relative order of multiple headers
with the same name. with the same name.
""" """
@ -237,6 +361,7 @@ class HeadersProcessor(BaseProcessor):
class OutputProcessor(object): class OutputProcessor(object):
"""A delegate class that invokes the actual processors."""
installed_processors = [ installed_processors = [
JSONProcessor, JSONProcessor,

View File

@ -22,64 +22,94 @@ To make it run faster and offline you can::
import os import os
import sys import sys
import json import json
import unittest
import argparse import argparse
import tempfile 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: try:
from urllib.request import urlopen from urllib.request import urlopen
except ImportError: except ImportError:
from urllib2 import urlopen from urllib2 import urlopen
import requests from requests.compat import is_windows, is_py26, bytes, str
from requests.compat import is_py26, is_py3, bytes, str
################################################################# #################################################################
# Utils/setup # Utils/setup
################################################################# #################################################################
# HACK: Prepend ../ to PYTHONPATH so that we can import httpie form there. # HACK: Prepend ../ to PYTHONPATH so that we can import httpie form there.
TESTS_ROOT = 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 input from httpie import input
from httpie.models import Environment from httpie.models import Environment
from httpie.core import main, output_stream from httpie.core import main
from httpie.output import BINARY_SUPPRESSED_NOTICE from httpie.output import BINARY_SUPPRESSED_NOTICE
from httpie.input import ParseError from httpie.input import ParseError
HTTPBIN_URL = os.environ.get('HTTPBIN_URL', HTTPBIN_URL = os.environ.get('HTTPBIN_URL',
'http://httpbin.org') 'http://httpbin.org').rstrip('/')
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt')
TEST_FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt')
with open(TEST_FILE_PATH) as f: OK = 'HTTP/1.1 200'
TEST_FILE_CONTENT = f.read().strip() 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'
)
COLOR = '\x1b['
TEST_BIN_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.bin')
with open(TEST_BIN_FILE_PATH, 'rb') as f:
TEST_BIN_FILE_CONTENT = f.read()
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b[' def patharg(path):
"""Back slashes need to be escaped in ITEM args, even in Windows paths."""
return path.replace('\\', '\\\\\\')
# Test files
FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.txt')
FILE2_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file2.txt')
BIN_FILE_PATH = os.path.join(TESTS_ROOT, 'fixtures', 'file.bin')
FILE_PATH_ARG = patharg(FILE_PATH)
FILE2_PATH_ARG = patharg(FILE2_PATH)
BIN_FILE_PATH_ARG = patharg(BIN_FILE_PATH)
with open(FILE_PATH) as f:
FILE_CONTENT = f.read().strip()
with open(BIN_FILE_PATH, 'rb') as f:
BIN_FILE_CONTENT = f.read()
def httpbin(path): def httpbin(path):
return HTTPBIN_URL + path return HTTPBIN_URL + path
class ResponseMixin(object): class TestEnvironment(Environment):
exit_status = None colors = 0
stderr = None stdin_isatty = True,
json = None stdout_isatty = True
is_windows = False
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')
super(TestEnvironment, self).__init__(**kwargs)
class BytesResponse(bytes, ResponseMixin): class BytesResponse(bytes): pass
pass class StrResponse(str): pass
class StrResponse(str, ResponseMixin):
pass
def http(*args, **kwargs): def http(*args, **kwargs):
@ -87,37 +117,41 @@ def http(*args, **kwargs):
Invoke `httpie.core.main()` with `args` and `kwargs`, Invoke `httpie.core.main()` with `args` and `kwargs`,
and return a unicode response. and return a unicode response.
""" Return a `StrResponse`, or `BytesResponse` if unable to decode the output.
if 'env' not in kwargs: The response has the following attributes:
# Ensure that we have terminal by default (needed for Travis).
kwargs['env'] = Environment(
colors=0,
stdin_isatty=True,
stdout_isatty=True,
)
stdout = kwargs['env'].stdout = tempfile.TemporaryFile('w+b') `stderr`: text written to stderr
stderr = kwargs['env'].stderr = tempfile.TemporaryFile('w+t') `exit_status`: the exit status
`json`: decoded JSON (if possible) or `None`
Exceptions are propagated except for SystemExit.
"""
env = kwargs.get('env')
if not env:
env = kwargs['env'] = TestEnvironment()
try: try:
exit_status = main(args=['--debug'] + list(args), **kwargs)
except (Exception, SystemExit) as e:
sys.stderr.write(stderr.read())
raise
else:
stdout.seek(0)
stderr.seek(0)
output = stdout.read()
try: try:
#noinspection PyArgumentList exit_status = main(args=['--debug'] + list(args), **kwargs)
except Exception:
sys.stderr.write(env.stderr.read())
raise
except SystemExit:
exit_status = 1
env.stdout.seek(0)
env.stderr.seek(0)
output = env.stdout.read()
try:
r = StrResponse(output.decode('utf8')) r = StrResponse(output.decode('utf8'))
except UnicodeDecodeError: except UnicodeDecodeError:
#noinspection PyArgumentList
r = BytesResponse(output) r = BytesResponse(output)
else: else:
if TERMINAL_COLOR_PRESENCE_CHECK not in r: if COLOR not in r:
# De-serialize JSON body if possible. # De-serialize JSON body if possible.
if r.strip().startswith('{'): if r.strip().startswith('{'):
#noinspection PyTypeChecker #noinspection PyTypeChecker
@ -133,14 +167,14 @@ def http(*args, **kwargs):
except ValueError: except ValueError:
pass pass
r.stderr = stderr.read() r.stderr = env.stderr.read()
r.exit_status = exit_status r.exit_status = exit_status
return r return r
finally: finally:
stdout.close() env.stdout.close()
stderr.close() env.stderr.close()
class BaseTestCase(unittest.TestCase): class BaseTestCase(unittest.TestCase):
@ -168,14 +202,14 @@ class HTTPieTest(BaseTestCase):
'GET', 'GET',
httpbin('/get') httpbin('/get')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
def test_DELETE(self): def test_DELETE(self):
r = http( r = http(
'DELETE', 'DELETE',
httpbin('/delete') httpbin('/delete')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
def test_PUT(self): def test_PUT(self):
r = http( r = http(
@ -183,7 +217,7 @@ class HTTPieTest(BaseTestCase):
httpbin('/put'), httpbin('/put'),
'foo=bar' 'foo=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"foo": "bar"', r) self.assertIn('"foo": "bar"', r)
def test_POST_JSON_data(self): def test_POST_JSON_data(self):
@ -192,7 +226,7 @@ class HTTPieTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'foo=bar' 'foo=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"foo": "bar"', r) self.assertIn('"foo": "bar"', r)
def test_POST_form(self): def test_POST_form(self):
@ -202,7 +236,7 @@ class HTTPieTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'foo=bar' 'foo=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"foo": "bar"', r) self.assertIn('"foo": "bar"', r)
def test_POST_form_multiple_values(self): def test_POST_form_multiple_values(self):
@ -213,19 +247,17 @@ class HTTPieTest(BaseTestCase):
'foo=bar', 'foo=bar',
'foo=baz', 'foo=baz',
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertDictEqual(r.json['form'], { self.assertDictEqual(r.json['form'], {
'foo': ['bar', 'baz'] 'foo': ['bar', 'baz']
}) })
def test_POST_stdin(self): def test_POST_stdin(self):
with open(TEST_FILE_PATH) as f: with open(FILE_PATH) as f:
env = Environment( env = TestEnvironment(
stdin=f, stdin=f,
stdin_isatty=False, stdin_isatty=False,
stdout_isatty=True,
colors=0,
) )
r = http( r = http(
@ -234,8 +266,8 @@ class HTTPieTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
env=env env=env
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn(TEST_FILE_CONTENT, r) self.assertIn(FILE_CONTENT, r)
def test_headers(self): def test_headers(self):
r = http( r = http(
@ -243,7 +275,7 @@ class HTTPieTest(BaseTestCase):
httpbin('/headers'), httpbin('/headers'),
'Foo:bar' 'Foo:bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"User-Agent": "HTTPie', r) self.assertIn('"User-Agent": "HTTPie', r)
self.assertIn('"Foo": "bar"', r) self.assertIn('"Foo": "bar"', r)
@ -260,7 +292,7 @@ class QuerystringTest(BaseTestCase):
path = '/get?a=1&b=2' path = '/get?a=1&b=2'
url = httpbin(path) url = httpbin(path)
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('GET %s HTTP/1.1' % path, r) self.assertIn('GET %s HTTP/1.1' % path, r)
self.assertIn('"url": "%s"' % url, r) self.assertIn('"url": "%s"' % url, r)
@ -276,7 +308,7 @@ class QuerystringTest(BaseTestCase):
path = '/get?a=1&b=2' path = '/get?a=1&b=2'
url = httpbin(path) url = httpbin(path)
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('GET %s HTTP/1.1' % path, r) self.assertIn('GET %s HTTP/1.1' % path, r)
self.assertIn('"url": "%s"' % url, r) self.assertIn('"url": "%s"' % url, r)
@ -293,7 +325,7 @@ class QuerystringTest(BaseTestCase):
path = '/get?a=1&a=1&a=1&a=1&b=2' path = '/get?a=1&a=1&a=1&a=1&b=2'
url = httpbin(path) url = httpbin(path)
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('GET %s HTTP/1.1' % path, r) self.assertIn('GET %s HTTP/1.1' % path, r)
self.assertIn('"url": "%s"' % url, r) self.assertIn('"url": "%s"' % url, r)
@ -311,7 +343,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
'GET', 'GET',
httpbin('/headers') httpbin('/headers')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Accept": "*/*"', r) self.assertIn('"Accept": "*/*"', r)
self.assertNotIn('"Content-Type": "application/json', r) self.assertNotIn('"Content-Type": "application/json', r)
@ -321,7 +353,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
'POST', 'POST',
httpbin('/post') httpbin('/post')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Accept": "*/*"', r) self.assertIn('"Accept": "*/*"', r)
self.assertNotIn('"Content-Type": "application/json', r) self.assertNotIn('"Content-Type": "application/json', r)
@ -331,7 +363,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'a=b' 'a=b'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Accept": "application/json"', r) self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r) self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
@ -342,7 +374,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'a=b' 'a=b'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Accept": "application/json"', r) self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r) self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
@ -352,7 +384,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
'POST', 'POST',
httpbin('/post') httpbin('/post')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Accept": "application/json"', r) self.assertIn('"Accept": "application/json"', r)
self.assertIn('"Content-Type": "application/json; charset=utf-8', r) self.assertIn('"Content-Type": "application/json; charset=utf-8', r)
@ -364,7 +396,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
'Accept:application/xml', 'Accept:application/xml',
'Content-Type:application/xml' 'Content-Type:application/xml'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Accept": "application/xml"', r) self.assertIn('"Accept": "application/xml"', r)
self.assertIn('"Content-Type": "application/xml"', r) self.assertIn('"Content-Type": "application/xml"', r)
@ -374,7 +406,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
'POST', 'POST',
httpbin('/post') httpbin('/post')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn( self.assertIn(
'"Content-Type":' '"Content-Type":'
' "application/x-www-form-urlencoded; charset=utf-8"', ' "application/x-www-form-urlencoded; charset=utf-8"',
@ -388,7 +420,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'Content-Type:application/xml' 'Content-Type:application/xml'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Content-Type": "application/xml"', r) self.assertIn('"Content-Type": "application/xml"', r)
def test_print_only_body_when_stdout_redirected_by_default(self): def test_print_only_body_when_stdout_redirected_by_default(self):
@ -396,7 +428,7 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
r = http( r = http(
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=Environment( env=TestEnvironment(
stdin_isatty=True, stdin_isatty=True,
stdout_isatty=False stdout_isatty=False
) )
@ -409,26 +441,26 @@ class AutoContentTypeAndAcceptHeadersTest(BaseTestCase):
'--print=h', '--print=h',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=Environment( env=TestEnvironment(
stdin_isatty=True, stdin_isatty=True,
stdout_isatty=False stdout_isatty=False
) )
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
class ImplicitHTTPMethodTest(BaseTestCase): class ImplicitHTTPMethodTest(BaseTestCase):
def test_implicit_GET(self): def test_implicit_GET(self):
r = http(httpbin('/get')) r = http(httpbin('/get'))
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
def test_implicit_GET_with_headers(self): def test_implicit_GET_with_headers(self):
r = http( r = http(
httpbin('/headers'), httpbin('/headers'),
'Foo:bar' 'Foo:bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"Foo": "bar"', r) self.assertIn('"Foo": "bar"', r)
def test_implicit_POST_json(self): def test_implicit_POST_json(self):
@ -436,7 +468,7 @@ class ImplicitHTTPMethodTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'hello=world' 'hello=world'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"hello": "world"', r) self.assertIn('"hello": "world"', r)
def test_implicit_POST_form(self): def test_implicit_POST_form(self):
@ -445,23 +477,21 @@ class ImplicitHTTPMethodTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'foo=bar' 'foo=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"foo": "bar"', r) self.assertIn('"foo": "bar"', r)
def test_implicit_POST_stdin(self): def test_implicit_POST_stdin(self):
with open(TEST_FILE_PATH) as f: with open(FILE_PATH) as f:
env = Environment( env = TestEnvironment(
stdin_isatty=False, stdin_isatty=False,
stdin=f, stdin=f,
stdout_isatty=True,
colors=0,
) )
r = http( r = http(
'--form', '--form',
httpbin('/post'), httpbin('/post'),
env=env env=env
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
class PrettyFlagTest(BaseTestCase): class PrettyFlagTest(BaseTestCase):
@ -471,31 +501,25 @@ class PrettyFlagTest(BaseTestCase):
r = http( r = http(
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=Environment( env=TestEnvironment(colors=256),
stdin_isatty=True,
stdout_isatty=True,
),
) )
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r) self.assertIn(COLOR, r)
def test_pretty_enabled_by_default_unless_stdout_redirected(self): def test_pretty_enabled_by_default_unless_stdout_redirected(self):
r = http( r = http(
'GET', 'GET',
httpbin('/get') httpbin('/get')
) )
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r) self.assertNotIn(COLOR, r)
def test_force_pretty(self): def test_force_pretty(self):
r = http( r = http(
'--pretty', '--pretty',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=Environment( env=TestEnvironment(stdout_isatty=False, colors=256),
stdin_isatty=True,
stdout_isatty=False
),
) )
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r) self.assertIn(COLOR, r)
def test_force_ugly(self): def test_force_ugly(self):
r = http( r = http(
@ -503,7 +527,7 @@ class PrettyFlagTest(BaseTestCase):
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
) )
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r) self.assertNotIn(COLOR, r)
def test_subtype_based_pygments_lexer_match(self): def test_subtype_based_pygments_lexer_match(self):
"""Test that media subtype is used if type/subtype doesn't """Test that media subtype is used if type/subtype doesn't
@ -516,9 +540,9 @@ class PrettyFlagTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
'Content-Type:text/foo+json', 'Content-Type:text/foo+json',
'a=b', 'a=b',
env=Environment() env=TestEnvironment(colors=256)
) )
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r) self.assertIn(COLOR, r)
class VerboseFlagTest(BaseTestCase): class VerboseFlagTest(BaseTestCase):
@ -530,7 +554,7 @@ class VerboseFlagTest(BaseTestCase):
httpbin('/get'), httpbin('/get'),
'test-header:__test__' 'test-header:__test__'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
#noinspection PyUnresolvedReferences #noinspection PyUnresolvedReferences
self.assertEqual(r.count('__test__'), 2) self.assertEqual(r.count('__test__'), 2)
@ -544,7 +568,7 @@ class VerboseFlagTest(BaseTestCase):
'foo=bar', 'foo=bar',
'baz=bar' 'baz=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('foo=bar&baz=bar', r) self.assertIn('foo=bar&baz=bar', r)
def test_verbose_json(self): def test_verbose_json(self):
@ -555,7 +579,7 @@ class VerboseFlagTest(BaseTestCase):
'foo=bar', 'foo=bar',
'baz=bar' 'baz=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
#noinspection PyUnresolvedReferences #noinspection PyUnresolvedReferences
self.assertEqual(r.count('"baz": "bar"'), 2) self.assertEqual(r.count('"baz": "bar"'), 2)
@ -576,24 +600,24 @@ class MultipartFormDataFileUploadTest(BaseTestCase):
'--verbose', '--verbose',
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'test-file@%s' % TEST_FILE_PATH, 'test-file@%s' % FILE_PATH_ARG,
'foo=bar' 'foo=bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('Content-Disposition: form-data; name="foo"', r) self.assertIn('Content-Disposition: form-data; name="foo"', r)
self.assertIn('Content-Disposition: form-data; name="test-file";' self.assertIn('Content-Disposition: form-data; name="test-file";'
' filename="%s"' % os.path.basename(TEST_FILE_PATH), r) ' filename="%s"' % os.path.basename(FILE_PATH), r)
#noinspection PyUnresolvedReferences #noinspection PyUnresolvedReferences
self.assertEqual(r.count(TEST_FILE_CONTENT), 2) self.assertEqual(r.count(FILE_CONTENT), 2)
self.assertIn('"foo": "bar"', r) self.assertIn('"foo": "bar"', r)
class BinaryRequestDataTest(BaseTestCase): class BinaryRequestDataTest(BaseTestCase):
def test_binary_stdin(self): def test_binary_stdin(self):
with open(TEST_BIN_FILE_PATH, 'rb') as stdin: with open(BIN_FILE_PATH, 'rb') as stdin:
env = Environment( env = TestEnvironment(
stdin=stdin, stdin=stdin,
stdin_isatty=False, stdin_isatty=False,
stdout_isatty=False stdout_isatty=False
@ -604,10 +628,10 @@ class BinaryRequestDataTest(BaseTestCase):
httpbin('/post'), httpbin('/post'),
env=env, env=env,
) )
self.assertEqual(r, TEST_BIN_FILE_CONTENT) self.assertEqual(r, BIN_FILE_CONTENT)
def test_binary_file_path(self): def test_binary_file_path(self):
env = Environment( env = TestEnvironment(
stdin_isatty=True, stdin_isatty=True,
stdout_isatty=False stdout_isatty=False
) )
@ -615,14 +639,14 @@ class BinaryRequestDataTest(BaseTestCase):
'--print=B', '--print=B',
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'@' + TEST_BIN_FILE_PATH, '@' + BIN_FILE_PATH_ARG,
env=env, env=env,
) )
self.assertEqual(r, TEST_BIN_FILE_CONTENT) self.assertEqual(r, BIN_FILE_CONTENT)
def test_binary_file_form(self): def test_binary_file_form(self):
env = Environment( env = TestEnvironment(
stdin_isatty=True, stdin_isatty=True,
stdout_isatty=False stdout_isatty=False
) )
@ -631,10 +655,10 @@ class BinaryRequestDataTest(BaseTestCase):
'--form', '--form',
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'test@' + TEST_BIN_FILE_PATH, 'test@' + BIN_FILE_PATH_ARG,
env=env, env=env,
) )
self.assertIn(bytes(TEST_BIN_FILE_CONTENT), bytes(r)) self.assertIn(bytes(BIN_FILE_CONTENT), bytes(r))
class BinaryResponseDataTest(BaseTestCase): class BinaryResponseDataTest(BaseTestCase):
@ -652,23 +676,23 @@ class BinaryResponseDataTest(BaseTestCase):
'GET', 'GET',
self.url self.url
) )
self.assertIn(BINARY_SUPPRESSED_NOTICE, r) self.assertIn(BINARY_SUPPRESSED_NOTICE.decode(), r)
def test_binary_suppresses_when_not_terminal_but_pretty(self): def test_binary_suppresses_when_not_terminal_but_pretty(self):
r = http( r = http(
'--pretty', '--pretty',
'GET', 'GET',
self.url, self.url,
env=Environment(stdin_isatty=True, env=TestEnvironment(stdin_isatty=True,
stdout_isatty=False) stdout_isatty=False)
) )
self.assertIn(BINARY_SUPPRESSED_NOTICE, r) self.assertIn(BINARY_SUPPRESSED_NOTICE.decode(), r)
def test_binary_included_and_correct_when_suitable(self): def test_binary_included_and_correct_when_suitable(self):
r = http( r = http(
'GET', 'GET',
self.url, self.url,
env=Environment(stdin_isatty=True, env=TestEnvironment(stdin_isatty=True,
stdout_isatty=False) stdout_isatty=False)
) )
self.assertEqual(r, self.bindata) self.assertEqual(r, self.bindata)
@ -683,41 +707,40 @@ class RequestBodyFromFilePathTest(BaseTestCase):
r = http( r = http(
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'@' + TEST_FILE_PATH '@' + FILE_PATH_ARG
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn(TEST_FILE_CONTENT, r) self.assertIn(FILE_CONTENT, r)
self.assertIn('"Content-Type": "text/plain"', r) self.assertIn('"Content-Type": "text/plain"', r)
def test_request_body_from_file_by_path_with_explicit_content_type(self): def test_request_body_from_file_by_path_with_explicit_content_type(self):
r = http( r = http(
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'@' + TEST_FILE_PATH, '@' + FILE_PATH_ARG,
'Content-Type:x-foo/bar' 'Content-Type:x-foo/bar'
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn(TEST_FILE_CONTENT, r) self.assertIn(FILE_CONTENT, r)
self.assertIn('"Content-Type": "x-foo/bar"', r) self.assertIn('"Content-Type": "x-foo/bar"', r)
def test_request_body_from_file_by_path_no_field_name_allowed(self): def test_request_body_from_file_by_path_no_field_name_allowed(self):
env = Environment(stdin_isatty=True) env = TestEnvironment(stdin_isatty=True)
r = http( r = http(
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'field-name@' + TEST_FILE_PATH, 'field-name@' + FILE_PATH_ARG,
env=env env=env
) )
self.assertIn('perhaps you meant --form?', r.stderr) self.assertIn('perhaps you meant --form?', r.stderr)
def test_request_body_from_file_by_path_no_data_items_allowed(self): def test_request_body_from_file_by_path_no_data_items_allowed(self):
env = Environment(stdin_isatty=True)
r = http( r = http(
'POST', 'POST',
httpbin('/post'), httpbin('/post'),
'@' + TEST_FILE_PATH, '@' + FILE_PATH_ARG,
'foo=bar', 'foo=bar',
env=env env=TestEnvironment(stdin_isatty=False)
) )
self.assertIn('cannot be mixed', r.stderr) self.assertIn('cannot be mixed', r.stderr)
@ -730,7 +753,7 @@ class AuthTest(BaseTestCase):
'GET', 'GET',
httpbin('/basic-auth/user/password') httpbin('/basic-auth/user/password')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"authenticated": true', r) self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r) self.assertIn('"user": "user"', r)
@ -741,7 +764,7 @@ class AuthTest(BaseTestCase):
'GET', 'GET',
httpbin('/digest-auth/auth/user/password') httpbin('/digest-auth/auth/user/password')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"authenticated": true', r) self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r) self.assertIn('"user": "user"', r)
@ -756,7 +779,7 @@ class AuthTest(BaseTestCase):
httpbin('/basic-auth/user/password') httpbin('/basic-auth/user/password')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertIn('"authenticated": true', r) self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r) self.assertIn('"user": "user"', r)
@ -768,7 +791,7 @@ class ExitStatusTest(BaseTestCase):
'GET', 'GET',
httpbin('/status/200') httpbin('/status/200')
) )
self.assertIn('HTTP/1.1 200', r) self.assertIn(OK, r)
self.assertEqual(r.exit_status, 0) self.assertEqual(r.exit_status, 0)
def test_error_response_exits_0_without_check_status(self): def test_error_response_exits_0_without_check_status(self):
@ -785,10 +808,7 @@ class ExitStatusTest(BaseTestCase):
'--headers', # non-terminal, force headers '--headers', # non-terminal, force headers
'GET', 'GET',
httpbin('/status/301'), httpbin('/status/301'),
env=Environment( env=TestEnvironment(stdout_isatty=False,)
stdout_isatty=False,
stdin_isatty=True,
)
) )
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, 3)
@ -829,7 +849,7 @@ class ExitStatusTest(BaseTestCase):
class FakeWindowsTest(BaseTestCase): class FakeWindowsTest(BaseTestCase):
def test_stdout_redirect_not_supported_on_windows(self): def test_stdout_redirect_not_supported_on_windows(self):
env = Environment(is_windows=True, stdout_isatty=False) env = TestEnvironment(is_windows=True, stdout_isatty=False)
r = http( r = http(
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
@ -840,11 +860,6 @@ class FakeWindowsTest(BaseTestCase):
self.assertIn('--output', r.stderr) self.assertIn('--output', r.stderr)
def test_output_file_pretty_not_allowed_on_windows(self): def test_output_file_pretty_not_allowed_on_windows(self):
env = Environment(
is_windows=True,
stdout_isatty=True,
stdin_isatty=True
)
r = http( r = http(
'--output', '--output',
@ -852,12 +867,71 @@ class FakeWindowsTest(BaseTestCase):
'--pretty', '--pretty',
'GET', 'GET',
httpbin('/get'), httpbin('/get'),
env=env env=TestEnvironment(is_windows=True)
) )
self.assertIn( self.assertIn(
'Only terminal output can be prettified on Windows', r.stderr) 'Only terminal output can be prettified on Windows', r.stderr)
class StreamTest(BaseTestCase):
# GET because httpbin 500s with binary POST body.
@skipIf(is_windows, 'Pretty redirect not supported under Windows')
def test_pretty_redirected_stream(self):
"""Test that --stream works with prettified redirected output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--verbose',
'--pretty',
'--stream',
'GET',
httpbin('/get'),
env=TestEnvironment(
colors=256,
stdin=f,
stdin_isatty=False,
stdout_isatty=False,
)
)
self.assertIn(BINARY_SUPPRESSED_NOTICE.decode(), r)
self.assertIn(OK_COLOR, r)
def test_encoded_stream(self):
"""Test that --stream works with non-prettified redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--ugly',
'--stream',
'--verbose',
'GET',
httpbin('/get'),
env=TestEnvironment(
stdin=f,
stdin_isatty=False
),
)
self.assertIn(BINARY_SUPPRESSED_NOTICE.decode(), r)
self.assertIn(OK, r)
def test_redirected_stream(self):
"""Test that --stream works with non-prettified redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--ugly',
'--stream',
'--verbose',
'GET',
httpbin('/get'),
env=TestEnvironment(
stdout_isatty=False,
stdin=f,
stdin_isatty=False
)
)
self.assertIn(OK.encode(), r)
self.assertIn(BIN_FILE_CONTENT, r)
################################################################# #################################################################
# CLI argument parsing related tests. # CLI argument parsing related tests.
################################################################# #################################################################
@ -887,7 +961,7 @@ class ItemParsingTest(BaseTestCase):
# data # data
self.key_value_type('baz\\=bar=foo'), self.key_value_type('baz\\=bar=foo'),
# files # files
self.key_value_type('bar\\@baz@%s' % TEST_FILE_PATH) self.key_value_type('bar\\@baz@%s' % FILE_PATH_ARG)
]) ])
self.assertDictEqual(headers, { self.assertDictEqual(headers, {
'foo:bar': 'baz', 'foo:bar': 'baz',
@ -915,7 +989,7 @@ class ItemParsingTest(BaseTestCase):
self.key_value_type('eh:'), self.key_value_type('eh:'),
self.key_value_type('ed='), self.key_value_type('ed='),
self.key_value_type('bool:=true'), self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % TEST_FILE_PATH), self.key_value_type('test-file@%s' % FILE_PATH_ARG),
self.key_value_type('query==value'), self.key_value_type('query==value'),
]) ])
self.assertDictEqual(headers, { self.assertDictEqual(headers, {
@ -946,7 +1020,7 @@ class ArgumentParserTestCase(unittest.TestCase):
args.url = 'http://example.com/' args.url = 'http://example.com/'
args.items = [] args.items = []
self.parser._guess_method(args, Environment()) self.parser._guess_method(args, TestEnvironment())
self.assertEqual(args.method, 'GET') self.assertEqual(args.method, 'GET')
self.assertEqual(args.url, 'http://example.com/') self.assertEqual(args.url, 'http://example.com/')
@ -958,10 +1032,7 @@ class ArgumentParserTestCase(unittest.TestCase):
args.url = 'http://example.com/' args.url = 'http://example.com/'
args.items = [] args.items = []
self.parser._guess_method(args, Environment( self.parser._guess_method(args, TestEnvironment())
stdin_isatty=True,
stdout_isatty=True,
))
self.assertEqual(args.method, 'GET') self.assertEqual(args.method, 'GET')
self.assertEqual(args.url, 'http://example.com/') self.assertEqual(args.url, 'http://example.com/')
@ -973,7 +1044,7 @@ class ArgumentParserTestCase(unittest.TestCase):
args.url = 'data=field' args.url = 'data=field'
args.items = [] args.items = []
self.parser._guess_method(args, Environment()) self.parser._guess_method(args, TestEnvironment())
self.assertEqual(args.method, 'POST') self.assertEqual(args.method, 'POST')
self.assertEqual(args.url, 'http://example.com/') self.assertEqual(args.url, 'http://example.com/')
@ -988,10 +1059,7 @@ class ArgumentParserTestCase(unittest.TestCase):
args.url = 'test:header' args.url = 'test:header'
args.items = [] args.items = []
self.parser._guess_method(args, Environment( self.parser._guess_method(args, TestEnvironment())
stdin_isatty=True,
stdout_isatty=True,
))
self.assertEqual(args.method, 'GET') self.assertEqual(args.method, 'GET')
self.assertEqual(args.url, 'http://example.com/') self.assertEqual(args.url, 'http://example.com/')
@ -1009,7 +1077,7 @@ class ArgumentParserTestCase(unittest.TestCase):
key='old_item', value='b', sep='=', orig='old_item=b') key='old_item', value='b', sep='=', orig='old_item=b')
] ]
self.parser._guess_method(args, Environment()) self.parser._guess_method(args, TestEnvironment())
self.assertEqual(args.items, [ self.assertEqual(args.items, [
input.KeyValue( input.KeyValue(
@ -1019,57 +1087,6 @@ class ArgumentParserTestCase(unittest.TestCase):
]) ])
class FakeResponse(requests.Response):
class Mock(object):
def __getattr__(self, item):
return self
def __repr__(self):
return 'Mock string'
def __unicode__(self):
return self.__repr__()
def __init__(self, content=None, encoding='utf-8'):
super(FakeResponse, self).__init__()
self.headers['Content-Type'] = 'application/json'
self.encoding = encoding
self._content = content.encode(encoding)
self.raw = self.Mock()
class UnicodeOutputTestCase(BaseTestCase):
def test_unicode_output(self):
# some cyrillic and simplified chinese symbols
response_dict = {'Привет': 'Мир!',
'Hello': '世界'}
if not is_py3:
response_dict = dict(
(k.decode('utf8'), v.decode('utf8'))
for k, v in response_dict.items()
)
response_body = json.dumps(response_dict)
# emulate response
response = FakeResponse(response_body)
# emulate cli arguments
args = argparse.Namespace()
args.prettify = True
args.output_options = 'b'
args.forced_content_type = None
args.style = 'default'
# colorized output contains escape sequences
output = output_stream(args, Environment(), response.request, response)
output = b''.join(output).decode('utf8')
for key, value in response_dict.items():
self.assertIn(key, output)
self.assertIn(value, output)
if __name__ == '__main__': if __name__ == '__main__':
#noinspection PyCallingNonCallable #noinspection PyCallingNonCallable
unittest.main() unittest.main()