Compare commits

..

2 Commits
0.4.1 ... 0.3.1

Author SHA1 Message Date
753aa69a8a v0.3.1: Require Requests < v1.0 due to unsolved compatibility issues. 2012-12-18 13:45:50 +01:00
f7e62336db 0.3.0 2012-09-21 05:43:34 +02:00
19 changed files with 362 additions and 479 deletions

3
.gitignore vendored
View File

@ -6,5 +6,4 @@ build
README.html
.coverage
htmlcov
.idea
.DS_Store

View File

@ -3,7 +3,7 @@ python:
- 2.6
- 2.7
- pypy
- 3.3
- 3.2
script: python setup.py test
install:
- pip install . --use-mirrors

View File

@ -8,7 +8,6 @@ HTTPie authors
Patches and ideas
-----------------
* `Cláudia T. Delgado <https://github.com/claudiatd>`_ (logo)
* `Hank Gay <https://github.com/gthank>`_
* `Jake Basile <https://github.com/jakebasile>`_
* `Vladimir Berkutov <https://github.com/dair-targ>`_

View File

@ -1,11 +1,12 @@
****************************************
HTTPie: a CLI, cURL-like tool for humans
****************************************
***********************
HTTPie: cURL for Humans
***********************
v0.3.0
HTTPie is a **command line HTTP client**. Its goal is to make CLI interaction
HTTPie is a **command line HTTP client** whose goal is to make CLI interaction
with web services as **human-friendly** as possible. It provides a
simple ``http`` command that allows for sending arbitrary HTTP requests using a
simple ``http`` command that allows for sending arbitrary HTTP requests with a
simple and natural syntax, and displays colorized responses. HTTPie can be used
for **testing, debugging**, and generally **interacting** with HTTP servers.
@ -14,14 +15,10 @@ for **testing, debugging**, and generally **interacting** with HTTP servers.
:alt: HTTPie compared to cURL
:width: 835
:height: 835
:align: center
.. image:: https://raw.github.com/claudiatd/httpie-artwork/master/images/httpie_logo_simple.png
:alt: HTTPie logo
:align: center
HTTPie is written in Python, and under the hood it uses the excellent
`Requests`_ and `Pygments`_ libraries.
`Requests`_ for HTTP and `Pygments`_ for colorizing.
**Table of Contents**
@ -33,6 +30,7 @@ HTTPie is written in Python, and under the hood it uses the excellent
:backlinks: none
=============
Main Features
=============
@ -61,11 +59,9 @@ or ``easy_install``:
.. code-block:: bash
$ pip install --upgrade httpie
$ pip install -U httpie
Alternatively:
.. code-block:: bash
$ easy_install httpie
@ -81,12 +77,12 @@ Or, you can install the **development version** directly from GitHub:
.. code-block:: bash
$ pip install --upgrade https://github.com/jkbr/httpie/tarball/master
$ pip install -U https://github.com/jkbr/httpie/tarball/master
There are also packages available for `Ubuntu`_, `Debian`_, and possibly other
Linux distributions as well. However, there may be a significant delay between
official HTTPie releases and package updates.
Linux distributions as well. However, they may be a significant delay between
releases and package updates.
=====
@ -131,7 +127,7 @@ Submitting `forms`_:
$ http -f POST example.org hello=World
See the request that is being sent using one of the `output options`_:
See the request that is being sent using on of the `output options`_:
.. code-block:: bash
@ -158,21 +154,11 @@ Download a file and save it via `redirected output`_:
$ http example.org/file > file
Use named `sessions`_ to make certain aspects or the communication persistent
between requests to the same host:
.. code-block:: bash
$ http --session=logged-in -a username:password httpbin.org/get API-Key:123
$ http --session=logged-in httpbin.org/headers
..
--------
*What follows is a detailed documentation. It covers the command syntax,
advanced usage, and also features additional examples.*
advances usage, and also features additional examples.*
============
@ -261,12 +247,11 @@ their type is distinguished only by the separator used:
+-----------------------+-----------------------------------------------------+
You can use ``\`` to escape characters that shouldn't be used as separators
(or parts thereof). For instance, ``foo\==bar`` will become a data key/value
(or parts thereof). e.g., ``foo\==bar`` will become a data key/value
pair (``foo=`` and ``bar``) instead of a URL parameter.
Note that data fields aren't the only way to specify request data:
`Redirected input`_ allows for passing arbitrary data to be sent with the
request.
Note that data fields aren't the only way to specify request data,
`redirected input`_ allows passing arbitrary data to be sent with the request.
====
@ -354,8 +339,8 @@ difference is in adding the ``--form`` / ``-f`` option, which ensures that
data fields are serialized as, and ``Content-Type`` is set to,
``application/x-www-form-urlencoded; charset=utf-8``.
It is possible to make form data the implicit content type instead of JSON
via the `config`_ file.
It is possible to make form data the implicit content type via the `config`_
file.
-------------
@ -449,16 +434,13 @@ come). There are two flags that control authentication:
(``-a username``), you'll be prompted for
the password before the request is sent.
To send a an empty password, pass ``username:``.
The ``username:password@hostname`` URL syntax is
supported as well (but credentials passed via ``-a``
have higher priority).
``--auth-type`` Specify the auth mechanism. Possible values are
``basic`` and ``digest``. The default value is
``basic`` so it can often be omitted.
=================== ======================================================
Authorization information from ``.netrc`` is honored as well.
Authorization information from ``.netrc`` is respected as well.
Basic auth:
@ -491,14 +473,14 @@ You can specify proxies to be used through the ``--proxy`` argument:
.. code-block:: bash
$ http --proxy=http:10.10.1.10:3128 --https:10.10.1.10:1080 example.org
http --proxy=http:10.10.1.10:3128 --https:10.10.1.10:1080 example.org
With Basic authentication:
.. code-block:: bash
$ http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
http --proxy=http:http://user:pass@10.10.1.10:3128 example.org
You can also configure proxies by environment variables ``HTTP_PROXY`` and
``HTTPS_PROXY``, and the underlying Requests library will pick them up as well.
@ -742,7 +724,7 @@ that the response body is binary,
.. code-block:: bash
$ http example.org/Movie.mov
http example.org/Movie.mov
You will nearly instantly see something like this:
@ -800,16 +782,6 @@ Force colorizing and formatting, and show both the request and the response in
The ``-R`` flag tells ``less`` to interpret color escape sequences included
HTTPie`s output.
You can create a shortcut for invoking HTTPie with colorized and paged output
by adding the following to your ``~/.bash_profile``:
.. code-block:: bash
function httpless {
# `httpless example.org'
http --pretty=all "$@" | less -R;
}
==================
Streamed Responses
@ -848,14 +820,17 @@ Streamed output by small chunks alá ``tail -f``:
$ http --stream -f -a YOUR-TWITTER-NAME https://stream.twitter.com/1/statuses/filter.json track=Apple \
| while read tweet; do echo "$tweet" | http POST example.org/tweets ; done
========
Sessions
========
By default, every request is completely independent of the previous ones.
HTTPie also supports persistent sessions, where custom headers, authorization,
HTTPie supports persistent sessions, where custom headers, authorization,
and cookies (manually specified or sent by the server) persist between
requests to the same host.
requests. Sessions are named and host-bound.
Create a new session named ``user1``:
@ -863,8 +838,7 @@ Create a new session named ``user1``:
$ http --session=user1 -a user1:password example.org X-Foo:Bar
Now you can refer to the session by its name, and the previously used
authorization and HTTP headers will automatically be set:
Now you can refer to the session by its name:
.. code-block:: bash
@ -880,20 +854,10 @@ To use a session without updating it from the request/response exchange
once it is created, specify the session name via
``--session-read-only=SESSION_NAME`` instead.
Session data are stored in JSON files in the directory
``~/.httpie/sessions/<host>/<name>.json``
Sessions are stored as JSON files in ``~/.httpie/sessions/<host>/<name>.json``
(``%APPDATA%\httpie\sessions\<host>\<name>.json`` on Windows).
**Warning:** All session data, including credentials, cookie data,
and custom headers are stored in plain text.
Session files can also be created or edited with a text editor.
.. code-block:: bash
$ httpie session edit example.org user1
See also `Config`_.
See also `config`_.
======
@ -904,7 +868,7 @@ HTTPie uses a simple configuration file that contains a JSON object with the
following keys:
========================= =================================================
``__meta__`` HTTPie automatically stores some metadata here.
``__version__`` HTTPie automatically sets this to its version.
Do not change.
``implicit_content_type`` A ``String`` specifying the implicit content type
@ -914,24 +878,10 @@ following keys:
``default_options`` An ``Array`` (by default empty) of options
that should be applied to every request.
For instance, you can use this option to change
the default style and output options:
``"default_options": ["--style=fruity", "--body"]``
Another useful default option is
``"--session=default"`` to make HTTPie always
use `sessions`_.
Default options from config file can be unset
for a particular invocation via
``--no-OPTION`` arguments passed on the
command line (e.g., ``--no-style``
or ``--no-session``).
========================= =================================================
The default location of the configuration file is ``~/.httpie/config.json``
(or ``%APPDATA%\httpie\config.json`` on Windows).
The default location is ``~/.httpie/config.json``
(``%APPDATA%\httpie\config.json`` on Windows).
The config directory location can be changed by setting the
``HTTPIE_CONFIG_DIR`` environment variable.
@ -1055,11 +1005,6 @@ Please run the existing suite of tests before a pull request is submitted:
Don't forget to add yourself to `AUTHORS.rst`_.
=======
Logo
=======
See `claudiatd/httpie-artwork`_
=======
Authors
@ -1068,6 +1013,7 @@ Authors
`Jakub Roztocil`_ (`@jakubroztocil`_) created HTTPie and `these fine people`_
have contributed.
=======
Licence
=======
@ -1081,15 +1027,8 @@ Changelog
*You can click a version name to see a diff with the previous one.*
* `0.4.1`_ (2013-02-26)
* Fixed ``setup.py``.
* `0.4.0`_ (2013-02-22)
* Python 3.3 compatibility.
* Requests >= v1.0.4 compatibility.
* Added support for credentials in URL.
* Added ``--no-option`` for every ``--option`` to be config-friendly.
* Mutually exclusive arguments can be specified multiple times. The
last value is used.
* `0.3.1`_ (2012-12-18)
* Require Requests < v1.0 due to unsolved compatibility issues.
* `0.3.0`_ (2012-09-21)
* Allow output redirection on Windows.
* Added configuration file.
@ -1175,7 +1114,6 @@ Changelog
.. _Jakub Roztocil: http://roztocil.name
.. _@jakubroztocil: https://twitter.com/jakubroztocil
.. _existing issues: https://github.com/jkbr/httpie/issues?state=open
.. _claudiatd/httpie-artwork: https://github.com/claudiatd/httpie-artwork
.. _0.1.6: https://github.com/jkbr/httpie/compare/0.1.4...0.1.6
.. _0.2.0: https://github.com/jkbr/httpie/compare/0.1.6...0.2.0
.. _0.2.1: https://github.com/jkbr/httpie/compare/0.2.0...0.2.1
@ -1184,9 +1122,7 @@ Changelog
.. _0.2.6: https://github.com/jkbr/httpie/compare/0.2.5...0.2.6
.. _0.2.7: https://github.com/jkbr/httpie/compare/0.2.5...0.2.7
.. _0.3.0: https://github.com/jkbr/httpie/compare/0.2.7...0.3.0
.. _0.4.0: https://github.com/jkbr/httpie/compare/0.3.0...0.4.0
.. _0.4.1: https://github.com/jkbr/httpie/compare/0.4.0...0.4.1
.. _0.5.0-alpha: https://github.com/jkbr/httpie/compare/0.4.0...master
.. _0.3.1: https://github.com/jkbr/httpie/compare/0.2.7...0.3.1
.. _stable version: https://github.com/jkbr/httpie/tree/0.3.0#readme
.. _AUTHORS.rst: https://github.com/jkbr/httpie/blob/master/AUTHORS.rst
.. _LICENSE: https://github.com/jkbr/httpie/blob/master/LICENSE

View File

@ -1,14 +1,13 @@
"""
HTTPie - a CLI, cURL-like tool for humans.
HTTPie - cURL for humans.
"""
__author__ = 'Jakub Roztocil'
__version__ = '0.4.1'
__version__ = '0.3.0'
__licence__ = 'BSD'
class ExitStatus:
"""Exit status code constants."""
class exit:
OK = 0
ERROR = 1
ERROR_TIMEOUT = 2

View File

@ -7,16 +7,17 @@ TODO: make the options config friendly, i.e., no mutually exclusive groups to
"""
from argparse import FileType, OPTIONAL, ZERO_OR_MORE, SUPPRESS
from requests.compat import is_windows
from . import __doc__
from . import __version__
from .compat import is_windows
from .sessions import DEFAULT_SESSIONS_DIR, Session
from .sessions import DEFAULT_SESSIONS_DIR
from .output import AVAILABLE_STYLES, DEFAULT_STYLE
from .input import (Parser, AuthCredentialsArgType, KeyValueArgType,
SEP_PROXY, SEP_CREDENTIALS, SEP_GROUP_ITEMS,
OUT_REQ_HEAD, OUT_REQ_BODY, OUT_RESP_HEAD,
OUT_RESP_BODY, OUTPUT_OPTIONS,
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY, RegexValidator)
PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY)
def _(text):
@ -26,13 +27,14 @@ def _(text):
parser = Parser(
description='%s <http://httpie.org>' % __doc__.strip(),
epilog='For every --option there is a --no-option'
' that reverts the option to its default value.\n\n'
'Suggestions and bug reports are greatly appreciated:\n'
'https://github.com/jkbr/httpie/issues'
epilog=_('''
Suggestions and bug reports are greatly appreciated:
https://github.com/jkbr/httpie/issues
''')
)
###############################################################################
# Positional arguments.
###############################################################################
@ -41,8 +43,8 @@ positional = parser.add_argument_group(
title='Positional arguments',
description=_('''
These arguments come after any flags and in the
order they are listed here. Only URL is required.
''')
order they are listed here. Only URL is required.'''
)
)
positional.add_argument(
'method', metavar='METHOD',
@ -87,7 +89,7 @@ positional.add_argument(
content_type = parser.add_argument_group(
title='Predefined content types',
description=None
)
).add_mutually_exclusive_group(required=False)
content_type.add_argument(
'--json', '-j', action='store_true',
@ -119,7 +121,7 @@ output_processing = parser.add_argument_group(title='Output processing')
output_processing.add_argument(
'--output', '-o', type=FileType('w+b'),
metavar='FILE',
help=SUPPRESS if not is_windows else _(
help= SUPPRESS if not is_windows else _(
'''
Save output to FILE.
This option is a replacement for piping output to FILE,
@ -151,13 +153,15 @@ output_processing.add_argument(
)
###############################################################################
# Output options
###############################################################################
output_options = parser.add_argument_group(title='Output options')
output_options.add_argument(
'--print', '-p', dest='output_options', metavar='WHAT',
output_print = output_options.add_mutually_exclusive_group(required=False)
output_print.add_argument('--print', '-p', dest='output_options',
metavar='WHAT',
help=_('''
String specifying what the output should contain:
"{request_headers}" stands for the request headers, and
@ -168,12 +172,14 @@ output_options.add_argument(
headers and body is printed), if standard output is not redirected.
If the output is piped to another program or to a file,
then only the body is printed by default.
'''.format(request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,))
'''.format(
request_headers=OUT_REQ_HEAD,
request_body=OUT_REQ_BODY,
response_headers=OUT_RESP_HEAD,
response_body=OUT_RESP_BODY,
))
)
output_options.add_argument(
output_print.add_argument(
'--verbose', '-v', dest='output_options',
action='store_const', const=''.join(OUTPUT_OPTIONS),
help=_('''
@ -181,7 +187,7 @@ output_options.add_argument(
Shortcut for --print={0}.
'''.format(''.join(OUTPUT_OPTIONS)))
)
output_options.add_argument(
output_print.add_argument(
'--headers', '-h', dest='output_options',
action='store_const', const=OUT_RESP_HEAD,
help=_('''
@ -189,7 +195,7 @@ output_options.add_argument(
Shortcut for --print={0}.
'''.format(OUT_RESP_HEAD))
)
output_options.add_argument(
output_print.add_argument(
'--body', '-b', dest='output_options',
action='store_const', const=OUT_RESP_BODY,
help=_('''
@ -198,8 +204,7 @@ output_options.add_argument(
'''.format(OUT_RESP_BODY))
)
output_options.add_argument(
'--stream', '-S', action='store_true', default=False,
output_options.add_argument('--stream', '-S', action='store_true', default=False,
help=_('''
Always stream the output by line, i.e., behave like `tail -f'.
@ -212,8 +217,8 @@ output_options.add_argument(
It is useful also without --pretty: It ensures that the output is flushed
more often and in smaller chunks.
''')
)
'''
))
###############################################################################
@ -223,13 +228,10 @@ sessions = parser.add_argument_group(title='Sessions')\
.add_mutually_exclusive_group(required=False)
sessions.add_argument(
'--session', metavar='SESSION_NAME', type=RegexValidator(
Session.VALID_NAME_PATTERN,
'Session name contains invalid characters.'
),
'--session', metavar='SESSION_NAME',
help=_('''
Create, or reuse and update a session.
Within a session, custom headers, auth credential, as well as any
Withing a session, custom headers, auth credential, as well as any
cookies sent by the server persist between requests.
Session files are stored in %s/<HOST>/<SESSION_NAME>.json.
''' % DEFAULT_SESSIONS_DIR)
@ -266,6 +268,7 @@ auth.add_argument(
)
# Network
#############################################
@ -336,8 +339,7 @@ troubleshooting.add_argument(
action='help', default=SUPPRESS,
help='Show this help message and exit'
)
troubleshooting.add_argument(
'--version', action='version', version=__version__)
troubleshooting.add_argument('--version', action='version', version=__version__)
troubleshooting.add_argument(
'--traceback', action='store_true', default=False,
help='Prints exception traceback should one occur.'

View File

@ -4,6 +4,7 @@ from pprint import pformat
import requests
import requests.auth
from requests.defaults import defaults
from . import sessions
from . import __version__
@ -20,51 +21,41 @@ def get_response(args, config_dir):
requests_kwargs = get_requests_kwargs(args)
if args.debug:
sys.stderr.write('\n>>> requests.request(%s)\n\n'
% pformat(requests_kwargs))
sys.stderr.write(
'\n>>> requests.request(%s)\n\n' % pformat(requests_kwargs))
if not args.session and not args.session_read_only:
response = requests.request(**requests_kwargs)
return requests.request(**requests_kwargs)
else:
response = sessions.get_response(
return sessions.get_response(
config_dir=config_dir,
name=args.session or args.session_read_only,
request_kwargs=requests_kwargs,
read_only=bool(args.session_read_only),
)
return response
def get_requests_kwargs(args):
"""Translate our `args` into `requests.request` keyword arguments."""
implicit_headers = {
'User-Agent': DEFAULT_UA
}
base_headers = defaults['base_headers'].copy()
base_headers['User-Agent'] = DEFAULT_UA
auto_json = args.data and not args.form
if args.json or auto_json:
implicit_headers['Accept'] = 'application/json'
base_headers['Accept'] = 'application/json'
if args.data:
implicit_headers['Content-Type'] = JSON
base_headers['Content-Type'] = JSON
if isinstance(args.data, dict):
if args.data:
args.data = json.dumps(args.data)
else:
# We need to set data to an empty string to prevent requests
# from assigning an empty list to `response.request.data`.
args.data = ''
# If not empty, serialize the data `dict` parsed from arguments.
# Otherwise set it to `None` avoid sending "{}".
args.data = json.dumps(args.data) if args.data else None
elif args.form and not args.files:
# If sending files, `requests` will set
# the `Content-Type` for us.
implicit_headers['Content-Type'] = FORM
for name, value in implicit_headers.items():
if name not in args.headers:
args.headers[name] = value
base_headers['Content-Type'] = FORM
credentials = None
if args.auth:
@ -74,7 +65,7 @@ def get_requests_kwargs(args):
}[args.auth_type](args.auth.key, args.auth.value)
kwargs = {
'stream': True,
'prefetch': False,
'method': args.method.lower(),
'url': args.url,
'headers': args.headers,
@ -89,6 +80,9 @@ def get_requests_kwargs(args):
'files': args.files,
'allow_redirects': args.follow,
'params': args.params,
'config': {
'base_headers': base_headers
}
}
return kwargs

View File

@ -1,18 +0,0 @@
"""
Python 2/3 compatibility.
"""
#noinspection PyUnresolvedReferences
from requests.compat import (
is_windows,
bytes,
str,
is_py3,
is_py26,
)
try:
from urllib.parse import urlsplit
except ImportError:
from urlparse import urlsplit

View File

@ -3,7 +3,7 @@ import json
import errno
from . import __version__
from .compat import is_windows
from requests.compat import is_windows
DEFAULT_CONFIG_DIR = os.environ.get(
@ -17,9 +17,7 @@ class BaseConfigDict(dict):
name = None
help = None
about = None
directory = DEFAULT_CONFIG_DIR
directory=DEFAULT_CONFIG_DIR
def __init__(self, directory=None, *args, **kwargs):
super(BaseConfigDict, self).__init__(*args, **kwargs)
@ -58,15 +56,7 @@ class BaseConfigDict(dict):
raise
def save(self):
self['__meta__'] = {
'httpie': __version__
}
if self.help:
self['__meta__']['help'] = self.help
if self.about:
self['__meta__']['about'] = self.about
self['__version__'] = __version__
with open(self.path, 'w') as f:
json.dump(self, f, indent=4, sort_keys=True, ensure_ascii=True)
f.write('\n')
@ -82,8 +72,6 @@ class BaseConfigDict(dict):
class Config(BaseConfigDict):
name = 'config'
help = 'https://github.com/jkbr/httpie#config'
about = 'HTTPie configuration file'
DEFAULTS = {
'implicit_content_type': 'json',

View File

@ -14,31 +14,31 @@ import sys
import errno
import requests
from requests.compat import str, is_py3
from httpie import __version__ as httpie_version
from requests import __version__ as requests_version
from pygments import __version__ as pygments_version
from .cli import parser
from .compat import str, is_py3
from .client import get_response
from .models import Environment
from .output import build_output_stream, write, write_with_colors_win_p3k
from . import ExitStatus
from .output import output_stream, write, write_with_colors_win_p3k
from . import exit
def get_exit_status(http_status, follow=False):
"""Translate HTTP status code to exit status code."""
if 300 <= http_status <= 399 and not follow:
def get_exist_status(code, follow=False):
"""Translate HTTP status code to exit status."""
if 300 <= code <= 399 and not follow:
# Redirect
return ExitStatus.ERROR_HTTP_3XX
elif 400 <= http_status <= 499:
return exit.ERROR_HTTP_3XX
elif 400 <= code <= 499:
# Client Error
return ExitStatus.ERROR_HTTP_4XX
elif 500 <= http_status <= 599:
return exit.ERROR_HTTP_4XX
elif 500 <= code <= 599:
# Server Error
return ExitStatus.ERROR_HTTP_5XX
return exit.ERROR_HTTP_5XX
else:
return ExitStatus.OK
return exit.OK
def print_debug_info(env):
@ -54,25 +54,24 @@ def print_debug_info(env):
def main(args=sys.argv[1:], env=Environment()):
"""Run the main program and write the output to ``env.stdout``.
Return exit status code.
Return exit status.
"""
if env.config.default_options:
args = env.config.default_options + args
def error(msg, *args, **kwargs):
def error(msg, *args):
msg = msg % args
level = kwargs.get('level', 'error')
env.stderr.write('http: %s: %s\n' % (level, msg))
env.stderr.write('\nhttp: error: %s\n' % msg)
debug = '--debug' in args
traceback = debug or '--traceback' in args
exit_status = ExitStatus.OK
status = exit.OK
if debug:
print_debug_info(env)
if args == ['--debug']:
return exit_status
sys.exit(exit.OK)
try:
args = parser.parse_args(args=args, env=env)
@ -80,18 +79,15 @@ def main(args=sys.argv[1:], env=Environment()):
response = get_response(args, config_dir=env.config.directory)
if args.check_status:
exit_status = get_exit_status(response.status_code, args.follow)
status = get_exist_status(response.status_code,
args.follow)
if status and not env.stdout_isatty:
error('%s %s', response.raw.status, response.raw.reason)
if not env.stdout_isatty and exit_status != ExitStatus.OK:
error('HTTP %s %s',
response.raw.status,
response.raw.reason,
level='warning')
stream = output_stream(args, env, response.request, response)
write_kwargs = {
'stream': build_output_stream(args, env,
response.request,
response),
'stream': stream,
'outfile': env.stdout,
'flush': env.stdout_isatty or args.stream
}
@ -112,18 +108,16 @@ def main(args=sys.argv[1:], env=Environment()):
if traceback:
raise
env.stderr.write('\n')
exit_status = ExitStatus.ERROR
status = exit.ERROR
except requests.Timeout:
exit_status = ExitStatus.ERROR_TIMEOUT
status = exit.ERROR_TIMEOUT
error('Request timed out (%ss).', args.timeout)
except Exception as e:
# TODO: Better distinction between expected and unexpected errors.
# Network errors vs. bugs, etc.
# TODO: distinguish between expected and unexpected errors.
# network errors vs. bugs, etc.
if traceback:
raise
error('%s: %s', type(e).__name__, str(e))
exit_status = ExitStatus.ERROR
status = exit.ERROR
return exit_status
return status

View File

@ -8,18 +8,15 @@ import json
import mimetypes
import getpass
from io import BytesIO
from argparse import ArgumentParser, ArgumentTypeError, ArgumentError
from argparse import ArgumentParser, ArgumentTypeError
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
# TODO: Use MultiDict for headers once added to `requests`.
# https://github.com/jkbr/httpie/issues/130
from requests.structures import CaseInsensitiveDict
from .compat import urlsplit, str
from requests.compat import str, urlparse
HTTP_POST = 'POST'
@ -99,10 +96,7 @@ class Parser(ArgumentParser):
self.env = env
args, no_options = super(Parser, self).parse_known_args(args,
namespace)
self._apply_no_options(args, no_options)
args = super(Parser, self).parse_args(args, namespace)
if not args.json and env.config.implicit_content_type == 'form':
args.form = True
@ -126,54 +120,12 @@ class Parser(ArgumentParser):
scheme = HTTPS if env.progname == 'https' else HTTP
args.url = scheme + args.url
self._process_auth(args)
if args.auth and not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(urlparse(args.url).netloc)
return args
def _process_auth(self, args):
url = urlsplit(args.url)
if args.auth:
if not args.auth.has_password():
# Stdin already read (if not a tty) so it's save to prompt.
args.auth.prompt_password(url.netloc)
elif url.username is not None:
# Handle http://username:password@hostname/
username, password = url.username, url.password
args.auth = AuthCredentials(
key=username,
value=password,
sep=SEP_CREDENTIALS,
orig=SEP_CREDENTIALS.join([username, password])
)
def _apply_no_options(self, args, no_options):
"""For every `--no-OPTION` in `no_options`, set `args.OPTION` to
its default value. This allows for un-setting of options, e.g.,
specified in config.
"""
invalid = []
for option in no_options:
if not option.startswith('--no-'):
invalid.append(option)
continue
# --no-option => --option
inverted = '--' + option[5:]
for action in self._actions:
if inverted in action.option_strings:
setattr(args, action.dest, action.default)
break
else:
invalid.append(option)
if invalid:
msg = 'unrecognized arguments: %s'
self.error(msg % ' '.join(invalid))
def _print_message(self, message, file=None):
# Sneak in our stderr/stdout.
file = {
@ -310,38 +262,6 @@ class KeyValue(object):
return self.__dict__ == other.__dict__
def session_name_arg_type(name):
from .sessions import Session
if not Session.is_valid_name(name):
raise ArgumentTypeError(
'special characters and spaces are not'
' allowed in session names: "%s"'
% name)
return name
def host_name_arg_type(name):
from .sessions import Host
if not Host.is_valid_name(name):
raise ArgumentTypeError(
'special characters and spaces are not'
' allowed in host names: "%s"'
% name)
return name
class RegexValidator(object):
def __init__(self, pattern, error_message):
self.pattern = re.compile(pattern)
self.error_message = error_message
def __call__(self, value):
if not self.pattern.search(value):
raise ArgumentError(None, self.error_message)
return value
class KeyValueArgType(object):
"""A key-value pair argument type used with `argparse`.
@ -483,6 +403,9 @@ class ParamDict(OrderedDict):
data and URL params.
"""
# NOTE: Won't work when used for form data with multiple values
# for a field and a file field is present:
# https://github.com/kennethreitz/requests/issues/737
if key not in self:
super(ParamDict, self).__setitem__(key, value)
else:

30
httpie/manage.py Normal file
View File

@ -0,0 +1,30 @@
"""
Provides the `httpie' management command.
Note that the main `http' command points to `httpie.__main__.main()`.
"""
import argparse
from . import sessions
from . import __version__
parser = argparse.ArgumentParser(
description='The HTTPie management command.',
version=__version__
)
subparsers = parser.add_subparsers()
# Only sessions as of now.
sessions.add_commands(subparsers)
def main():
args = parser.parse_args()
args.command(args)
if __name__ == '__main__':
main()

View File

@ -1,8 +1,8 @@
import os
import sys
from requests.compat import urlparse, is_windows, bytes, str
from .config import DEFAULT_CONFIG_DIR, Config
from .compat import urlsplit, is_windows, bytes, str
class Environment(object):
@ -13,6 +13,7 @@ class Environment(object):
"""
#noinspection PyUnresolvedReferences
is_windows = is_windows
progname = os.path.basename(sys.argv[0])
@ -142,18 +143,33 @@ class HTTPRequest(HTTPMessage):
@property
def headers(self):
url = urlsplit(self._orig.url)
"""Return Request-Line"""
url = urlparse(self._orig.url)
# Querystring
qs = ''
if url.query or self._orig.params:
qs = '?'
if url.query:
qs += url.query
# Requests doesn't make params part of ``request.url``.
if self._orig.params:
if url.query:
qs += '&'
#noinspection PyUnresolvedReferences
qs += type(self._orig)._encode_params(self._orig.params)
# Request-Line
request_line = '{method} {path}{query} HTTP/1.1'.format(
method=self._orig.method,
path=url.path or '/',
query='?' + url.query if url.query else ''
query=qs
)
headers = dict(self._orig.headers)
if 'Host' not in headers:
headers['Host'] = url.netloc
headers['Host'] = urlparse(self._orig.url).netloc
headers = ['%s: %s' % (name, value)
for name, value in headers.items()]
@ -168,8 +184,26 @@ class HTTPRequest(HTTPMessage):
@property
def body(self):
body = self._orig.body
if isinstance(body, str):
# Happens with JSON/form request data parsed from the command line.
body = body.encode('utf8')
return body or b''
"""Reconstruct and return the original request body bytes."""
if self._orig.files:
# TODO: would be nice if we didn't need to encode the files again
# FIXME: Also the boundary header doesn't match the one used.
for fn, fd in self._orig.files.values():
# Rewind the files as they have already been read before.
fd.seek(0)
body, _ = self._orig._encode_files(self._orig.files)
else:
try:
body = self._orig.data
except AttributeError:
# requests < 0.12.1
body = self._orig._enc_data
if isinstance(body, dict):
#noinspection PyUnresolvedReferences
body = type(self._orig)._encode_params(body)
if isinstance(body, str):
body = body.encode('utf8')
return body

View File

@ -12,8 +12,8 @@ from pygments.lexers import get_lexer_for_mimetype, get_lexer_by_name
from pygments.formatters.terminal import TerminalFormatter
from pygments.formatters.terminal256 import Terminal256Formatter
from pygments.util import ClassNotFound
from requests.compat import is_windows
from .compat import is_windows
from .solarized import Solarized256Style
from .models import HTTPRequest, HTTPResponse, Environment
from .input import (OUT_REQ_BODY, OUT_REQ_HEAD,
@ -78,21 +78,23 @@ def write_with_colors_win_p3k(stream, outfile, flush):
outfile.flush()
def build_output_stream(args, env, request, response):
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 = []
Stream = get_stream_type(env, args)
if req:
output.append(Stream(
@ -118,7 +120,7 @@ def build_output_stream(args, env, request, response):
return chain(*output)
def get_stream_type(env, args):
def make_stream(env, args):
"""Pick the right stream type based on `env` and `args`.
Wrap it in a partial with the type-specific args so that
we don't need to think what stream we are dealing with.
@ -145,7 +147,7 @@ def get_stream_type(env, args):
class BaseStream(object):
"""Base HTTP message output stream class."""
"""Base HTTP message stream class."""
def __init__(self, msg, with_headers=True, with_body=True):
"""
@ -154,28 +156,27 @@ class BaseStream(object):
:param with_body: if `True`, body will be included
"""
assert with_headers or with_body
self.msg = msg
self.with_headers = with_headers
self.with_body = with_body
def _get_headers(self):
def _headers(self):
"""Return the headers' bytes."""
return self.msg.headers.encode('ascii')
def _iter_body(self):
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._get_headers()
yield self._headers()
yield b'\r\n\r\n'
if self.with_body:
try:
for chunk in self._iter_body():
for chunk in self._body():
yield chunk
except BinarySuppressedError as e:
if self.with_headers:
@ -193,7 +194,7 @@ class RawStream(BaseStream):
super(RawStream, self).__init__(**kwargs)
self.chunk_size = chunk_size
def _iter_body(self):
def _body(self):
return self.msg.iter_body(self.chunk_size)
@ -221,7 +222,7 @@ class EncodedStream(BaseStream):
# Default to utf8 when unsure.
self.output_encoding = output_encoding or 'utf8'
def _iter_body(self):
def _body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
@ -247,11 +248,11 @@ class PrettyStream(EncodedStream):
super(PrettyStream, self).__init__(**kwargs)
self.processor = processor
def _get_headers(self):
def _headers(self):
return self.processor.process_headers(
self.msg.headers).encode(self.output_encoding)
def _iter_body(self):
def _body(self):
for line, lf in self.msg.iter_lines(self.CHUNK_SIZE):
if b'\0' in line:
raise BinarySuppressedError()
@ -275,8 +276,9 @@ class BufferedPrettyStream(PrettyStream):
CHUNK_SIZE = 1024 * 10
def _iter_body(self):
def _body(self):
#noinspection PyArgumentList
# Read the whole body before prettifying it,
# but bail out immediately if the body is binary.
body = bytearray()

View File

@ -1,18 +1,22 @@
"""Persistent, JSON-serialized sessions.
"""
import re
import os
import sys
import glob
import errno
import codecs
import shutil
import subprocess
import requests
from requests.compat import urlparse
from requests.cookies import RequestsCookieJar, create_cookie
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
from argparse import OPTIONAL
from .compat import urlsplit
from .config import BaseConfigDict, DEFAULT_CONFIG_DIR
from .output import PygmentsProcessor
SESSIONS_DIR_NAME = 'sessions'
@ -28,8 +32,9 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
host = Host(
root_dir=sessions_dir,
name=request_kwargs['headers'].get('Host', None)
or urlsplit(request_kwargs['url']).netloc.split('@')[-1]
or urlparse(request_kwargs['url']).netloc.split('@')[-1]
)
session = Session(host, name)
session.load()
@ -44,17 +49,15 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
elif session.auth:
request_kwargs['auth'] = session.auth
requests_session = requests.Session()
requests_session.cookies = session.cookies
rsession = requests.Session(cookies=session.cookies)
try:
response = requests_session.request(**request_kwargs)
response = rsession.request(**request_kwargs)
except Exception:
raise
else:
# Existing sessions with `read_only=True` don't get updated.
if session.is_new or not read_only:
session.cookies = requests_session.cookies
session.cookies = rsession.cookies
session.save()
return response
@ -62,46 +65,24 @@ def get_response(name, request_kwargs, config_dir, read_only=False):
class Host(object):
"""A host is a per-host directory on the disk containing sessions files."""
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.:-]+$')
def __init__(self, name, root_dir=DEFAULT_SESSIONS_DIR):
assert self.VALID_NAME_PATTERN.match(name)
def __init__(self, name, root_dir=DEFAULT_CONFIG_DIR):
self.name = name
self.root_dir = root_dir
def __iter__(self):
"""Return an iterator yielding `Session` instances."""
"""Return a iterator yielding `(session_name, session_path)`."""
for fn in sorted(glob.glob1(self.path, '*.json')):
session_name = os.path.splitext(fn)[0]
yield Session(host=self, name=session_name)
@staticmethod
def _quote_name(name):
"""host:port => host_port"""
return name.replace(':', '_')
@staticmethod
def _unquote_name(name):
"""host_port => host:port"""
return re.sub(r'_(\d+)$', r':\1', name)
@classmethod
def all(cls, root_dir=DEFAULT_SESSIONS_DIR):
"""Return a generator yielding a host at a time."""
for name in sorted(glob.glob1(root_dir, '*')):
if os.path.isdir(os.path.join(root_dir, name)):
yield Host(cls._unquote_name(name), root_dir=root_dir)
@property
def verbose_name(self):
return '%s %s' % (self.name, self.path)
yield os.path.splitext(fn)[0], os.path.join(self.path, fn)
def delete(self):
shutil.rmtree(self.path)
@property
def path(self):
path = os.path.join(self.root_dir, self._quote_name(self.name))
# Name will include ':' if a port is specified, which is invalid
# on windows. DNS does not allow '_' in a domain, or for it to end
# in a number (I think?)
path = os.path.join(self.root_dir, self.name.replace(':', '_'))
try:
os.makedirs(path, mode=0o700)
except OSError as e:
@ -109,35 +90,28 @@ class Host(object):
raise
return path
@classmethod
def all(cls):
"""Return a generator yielding a host at a time."""
for name in sorted(glob.glob1(DEFAULT_SESSIONS_DIR, '*')):
if os.path.isdir(os.path.join(DEFAULT_SESSIONS_DIR, name)):
yield Host(name)
class Session(BaseConfigDict):
help = 'https://github.com/jkbr/httpie#sessions'
about = 'HTTPie session file'
VALID_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$')
""""""
def __init__(self, host, name, *args, **kwargs):
assert self.VALID_NAME_PATTERN.match(name)
super(Session, self).__init__(*args, **kwargs)
self.host = host
self.name = name
self['headers'] = {}
self['cookies'] = {}
self['auth'] = {
'type': None,
'username': None,
'password': None
}
@property
def directory(self):
return self.host.path
@property
def verbose_name(self):
return '%s %s %s' % (self.host.name, self.name, self.path)
@property
def cookies(self):
jar = RequestsCookieJar()
@ -149,22 +123,26 @@ class Session(BaseConfigDict):
@cookies.setter
def cookies(self, jar):
# http://docs.python.org/2/library/cookielib.html#cookie-objects
stored_attrs = ['value', 'path', 'secure', 'expires']
excluded = [
'_rest', 'name', 'port_specified',
'domain_specified', 'domain_initial_dot',
'path_specified', 'comment', 'comment_url'
]
self['cookies'] = {}
for host in jar._cookies.values():
for path in host.values():
for name, cookie in path.items():
self['cookies'][name] = dict(
(attname, getattr(cookie, attname))
for attname in stored_attrs
)
cookie_dict = {}
for k, v in cookie.__dict__.items():
if k not in excluded:
cookie_dict[k] = v
self['cookies'][name] = cookie_dict
@property
def auth(self):
auth = self.get('auth', None)
if not auth or not auth['type']:
return
if not auth:
return None
Auth = {'basic': HTTPBasicAuth,
'digest': HTTPDigestAuth}[auth['type']]
return Auth(auth['username'], auth['password'])
@ -177,3 +155,79 @@ class Session(BaseConfigDict):
'username': cred.username,
'password': cred.password,
}
# The commands are disabled for now.
# TODO: write tests for the commands.
def list_command(args):
if args.host:
for name, path in Host(args.host):
print(name + ' [' + path + ']')
else:
for host in Host.all():
print(host.name)
for name, path in host:
print(' ' + name + ' [' + path + ']')
def show_command(args):
path = Session(Host(args.host), args.name).path
if not os.path.exists(path):
sys.stderr.write('Session "%s" does not exist [%s].\n'
% (args.name, path))
sys.exit(1)
with codecs.open(path, encoding='utf8') as f:
print(path + ':\n')
proc = PygmentsProcessor()
print(proc.process_body(f.read(), 'application/json', 'json'))
print('')
def delete_command(args):
host = Host(args.host)
if not args.name:
host.delete()
else:
Session(host, args.name).delete()
def edit_command(args):
editor = os.environ.get('EDITOR', None)
if not editor:
sys.stderr.write(
'You need to configure the environment variable EDITOR.\n')
sys.exit(1)
command = editor.split()
command.append(Session(Host(args.host), args.name).path)
subprocess.call(command)
def add_commands(subparsers):
# List
list_ = subparsers.add_parser('session-list', help='list sessions')
list_.set_defaults(command=list_command)
list_.add_argument('host', nargs=OPTIONAL)
# Show
show = subparsers.add_parser('session-show', help='show a session')
show.set_defaults(command=show_command)
show.add_argument('host')
show.add_argument('name')
# Edit
edit = subparsers.add_parser(
'session-edit', help='edit a session in $EDITOR')
edit.set_defaults(command=edit_command)
edit.add_argument('host')
edit.add_argument('name')
# Delete
delete = subparsers.add_parser('session-delete', help='delete a session')
delete.set_defaults(command=delete_command)
delete.add_argument('host')
delete.add_argument('name', nargs=OPTIONAL,
help='The name of the session to be deleted.'
' If not specified, all host sessions are deleted.')

View File

@ -1 +0,0 @@
#

View File

@ -12,7 +12,8 @@ if sys.argv[-1] == 'test':
requirements = [
'requests>=1.0.4',
# Debian has only requests==0.10.1 and httpie.deb depends on that.
'requests>=0.10.1,<1.0',
'Pygments>=1.5'
]
if sys.version_info[:2] in ((2, 6), (3, 1)):
@ -46,6 +47,8 @@ setup(
entry_points={
'console_scripts': [
'http = httpie.__main__:main',
# Not ready yet.
# 'httpie = httpie.manage:main',
],
},
install_requires=requirements,

View File

@ -19,6 +19,7 @@ To make it run faster and offline you can::
HTTPBIN_URL=http://localhost:5000 tox
"""
from functools import partial
import subprocess
import os
import sys
@ -28,6 +29,7 @@ import tempfile
import unittest
import shutil
from requests.compat import urlparse
try:
from urllib.request import urlopen
except ImportError:
@ -36,7 +38,6 @@ try:
from unittest import skipIf, skip
except ImportError:
skip = lambda msg: lambda self: None
def skipIf(cond, reason):
def decorator(test_method):
if cond:
@ -45,6 +46,7 @@ except ImportError:
return decorator
from requests import __version__ as requests_version
from requests.compat import is_windows, is_py26, bytes, str
#################################################################
@ -55,13 +57,12 @@ from requests import __version__ as requests_version
TESTS_ROOT = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import ExitStatus
from httpie import exit
from httpie import input
from httpie.models import Environment
from httpie.core import main
from httpie.output import BINARY_SUPPRESSED_NOTICE
from httpie.input import ParseError
from httpie.compat import is_windows, is_py26, bytes, str
CRLF = '\r\n'
@ -131,7 +132,6 @@ class TestEnvironment(Environment):
if self.delete_config_dir:
self._shutil.rmtree(self.config_dir)
def has_docutils():
try:
#noinspection PyUnresolvedReferences
@ -140,7 +140,6 @@ def has_docutils():
except ImportError:
return False
def get_readme_errors():
p = subprocess.Popen([
'rst2pseudoxml.py',
@ -155,8 +154,6 @@ def get_readme_errors():
class BytesResponse(bytes):
stderr = json = exit_status = None
class StrResponse(str):
stderr = json = exit_status = None
@ -188,7 +185,7 @@ def http(*args, **kwargs):
sys.stderr.write(env.stderr.read())
raise
except SystemExit:
exit_status = ExitStatus.ERROR
exit_status = exit.ERROR
env.stdout.seek(0)
env.stderr.seek(0)
@ -227,8 +224,6 @@ def http(*args, **kwargs):
class BaseTestCase(unittest.TestCase):
maxDiff = 100000
if is_py26:
def assertIn(self, member, container, msg=None):
self.assertTrue(member in container, msg)
@ -863,31 +858,6 @@ class AuthTest(BaseTestCase):
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_credentials_in_url(self):
url = httpbin('/basic-auth/user/password')
url = 'http://user:password@' + url.split('http://', 1)[1]
r = http(
'GET',
url
)
self.assertIn(OK, r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_credentials_in_url_auth_flag_has_priority(self):
"""When credentials are passed in URL and via -a at the same time,
then the ones from -a are used."""
url = httpbin('/basic-auth/user/password')
url = 'http://user:wrong_password@' + url.split('http://', 1)[1]
r = http(
'--auth=user:password',
'GET',
url
)
self.assertIn(OK, r)
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
class ExitStatusTest(BaseTestCase):
@ -897,7 +867,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/200')
)
self.assertIn(OK, r)
self.assertEqual(r.exit_status, ExitStatus.OK)
self.assertEqual(r.exit_status, exit.OK)
def test_error_response_exits_0_without_check_status(self):
r = http(
@ -905,7 +875,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, ExitStatus.OK)
self.assertEqual(r.exit_status, exit.OK)
self.assertTrue(not r.stderr)
def test_timeout_exit_status(self):
@ -914,7 +884,7 @@ class ExitStatusTest(BaseTestCase):
'GET',
httpbin('/delay/1')
)
self.assertEqual(r.exit_status, ExitStatus.ERROR_TIMEOUT)
self.assertEqual(r.exit_status, exit.ERROR_TIMEOUT)
def test_3xx_check_status_exits_3_and_stderr_when_stdout_redirected(self):
r = http(
@ -925,7 +895,7 @@ class ExitStatusTest(BaseTestCase):
env=TestEnvironment(stdout_isatty=False,)
)
self.assertIn('HTTP/1.1 301', r)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_3XX)
self.assertEqual(r.exit_status, exit.ERROR_HTTP_3XX)
self.assertIn('301 moved permanently', r.stderr.lower())
@skipIf(requests_version == '0.13.6',
@ -939,7 +909,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, ExitStatus.OK)
self.assertEqual(r.exit_status, exit.OK)
def test_4xx_check_status_exits_4(self):
r = http(
@ -948,7 +918,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/401')
)
self.assertIn('HTTP/1.1 401', r)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_4XX)
self.assertEqual(r.exit_status, exit.ERROR_HTTP_4XX)
# Also stderr should be empty since stdout isn't redirected.
self.assertTrue(not r.stderr)
@ -959,7 +929,7 @@ class ExitStatusTest(BaseTestCase):
httpbin('/status/500')
)
self.assertIn('HTTP/1.1 500', r)
self.assertEqual(r.exit_status, ExitStatus.ERROR_HTTP_5XX)
self.assertEqual(r.exit_status, exit.ERROR_HTTP_5XX)
class WindowsOnlyTests(BaseTestCase):
@ -1012,8 +982,7 @@ class StreamTest(BaseTestCase):
#self.assertIn(OK_COLOR, r)
def test_encoded_stream(self):
"""Test that --stream works with non-prettified
redirected terminal output."""
"""Test that --stream works with non-prettified redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--pretty=none',
@ -1031,8 +1000,7 @@ class StreamTest(BaseTestCase):
#self.assertIn(OK, r)
def test_redirected_stream(self):
"""Test that --stream works with non-prettified
redirected terminal output."""
"""Test that --stream works with non-prettified redirected terminal output."""
with open(BIN_FILE_PATH, 'rb') as f:
r = http(
'--pretty=none',
@ -1056,6 +1024,7 @@ class LineEndingsTest(BaseTestCase):
as the headers/body separator."""
def _validate_crlf(self, msg):
#noinspection PyUnresolvedReferences
lines = iter(msg.splitlines(True))
for header in lines:
if header == CRLF:
@ -1090,7 +1059,7 @@ class LineEndingsTest(BaseTestCase):
'GET',
httpbin('/get')
)
self.assertEqual(r.exit_status, 0)
self.assertEqual(r.exit_status,0)
self._validate_crlf(r)
def test_CRLF_ugly_request(self):
@ -1262,33 +1231,11 @@ class ArgumentParserTestCase(unittest.TestCase):
self.assertEqual(args.items, [
input.KeyValue(
key='new_item', value='a', sep='=', orig='new_item=a'),
input.KeyValue(
key='old_item', value='b', sep='=', orig='old_item=b'),
input.KeyValue(key
='old_item', value='b', sep='=', orig='old_item=b'),
])
class TestNoOptions(BaseTestCase):
def test_valid_no_options(self):
r = http(
'--verbose',
'--no-verbose',
'GET',
httpbin('/get')
)
self.assertNotIn('GET /get HTTP/1.1', r)
def test_invalid_no_options(self):
r = http(
'--no-war',
'GET',
httpbin('/get')
)
self.assertEqual(r.exit_status, 1)
self.assertIn('unrecognized arguments: --no-war', r.stderr)
self.assertNotIn('GET /get HTTP/1.1', r)
class READMETest(BaseTestCase):
@skipIf(not has_docutils(), 'docutils not installed')
@ -1403,12 +1350,10 @@ class SessionTest(BaseTestCase):
)
self.assertIn(OK, r3)
# Origin can differ on Travis.
del r1.json['origin'], r3.json['origin']
# Should be the same as before r2.
self.assertDictEqual(r1.json, r3.json)
if __name__ == '__main__':
#noinspection PyCallingNonCallable
unittest.main()

View File

@ -4,7 +4,7 @@
# and then run "tox" from this directory.
[tox]
envlist = py26, py27, py33, pypy
envlist = py26, py27, py32, pypy
[testenv]
commands = {envpython} setup.py test