Tests, docs, clean-up.

Closes #54.
This commit is contained in:
Jakub Roztocil 2012-06-24 03:43:08 +02:00
parent 4613d947a8
commit 926d3f5caf
7 changed files with 260 additions and 233 deletions

View File

@ -5,8 +5,6 @@ python:
- pypy
- 3.1
- 3.2
script: python tests/tests.py
script: python setup.py test
install:
- pip install requests pygments
- "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]] || [[ $TRAVIS_PYTHON_VERSION == '3.1' ]]; then pip install argparse; fi"
- pip install . --use-mirrors

View File

@ -104,28 +104,28 @@ Flags
Most of the flags mirror the arguments understood by ``requests.request``. See ``http -h`` for more details::
usage: http [-h] [--version] [--json | --form] [--traceback]
[--pretty | --ugly]
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
[--style STYLE] [--auth AUTH] [--auth-type {basic,digest}]
[--verify VERIFY] [--proxy PROXY] [--allow-redirects]
[--timeout TIMEOUT]
[METHOD] URL [ITEM [ITEM ...]]
[--pretty | --ugly]
[--print OUTPUT_OPTIONS | --verbose | --headers | --body]
[--style STYLE] [--auth AUTH] [--auth-type {basic,digest}]
[--verify VERIFY] [--proxy PROXY] [--allow-redirects]
[--timeout TIMEOUT]
[METHOD] URL [ITEM [ITEM ...]]
HTTPie - cURL for humans. <http://httpie.org>
positional arguments:
METHOD The HTTP method to be used for the request (GET, POST,
PUT, DELETE, PATCH, ...). If this argument is omitted
then httpie will guess HTTP method. If there is either
simple data field or JSON data field or file field
presents then method is POST otherwise it is GET.
PUT, DELETE, PATCH, ...). If this argument is omitted,
then HTTPie will guess the HTTP method. If there is
some data to be sent, then it will be POST, otherwise
GET.
URL The protocol defaults to http:// if the URL does not
include one.
ITEM A key-value pair whose type is defined by the
separator used. It can be an HTTP header
(header:value), a data field to be used in the request
body (field_name=value), a raw JSON data field
(field_name:=value) or a file field
(field_name:=value), or a file field
(field_name@/path/to/file). You can use a backslash to
escape a colliding separator in the field name.
@ -159,12 +159,12 @@ Most of the flags mirror the arguments understood by ``requests.request``. See `
--style STYLE, -s STYLE
Output coloring style, one of autumn, borland, bw,
colorful, default, emacs, friendly, fruity, manni,
monokai, murphy, native, pastie, perldoc, solarized,
tango, trac, vim, vs. Defaults to solarized. For this
option to work properly, please make sure that the
$TERM environment variable is set to "xterm-256color"
or similar (e.g., via `export TERM=xterm-256color' in
your ~/.bashrc).
monokai, murphy, native, pastie, perldoc, rrt,
solarized, tango, trac, vim, vs. Defaults to
solarized. For this option to work properly, please
make sure that the $TERM environment variable is set
to "xterm-256color" or similar (e.g., via `export TERM
=xterm-256color' in your ~/.bashrc).
--auth AUTH, -a AUTH username:password
--auth-type {basic,digest}
The authentication mechanism to be used. Defaults to
@ -189,7 +189,7 @@ Contribute
If you have found a bug or have a feature request, the `issue tracker <https://github.com/jkbr/httpie/issues?state=open>`_ is the place to start a discussion about it.
To contribute code or documentation, please first browse the exsiting issues to see if the feature/bug has previously been discussed. Then fork `the repository <https://github.com/jkbr/httpie>`_, make changes in your develop branch and submit a pull request. Note: Pull requests with tests and documentation are 53.6% more awesome :)
To contribute code or documentation, please first browse the existing issues to see if the feature/bug has previously been discussed. Then fork `the repository <https://github.com/jkbr/httpie>`_, make changes in your develop branch and submit a pull request. Note: Pull requests with tests and documentation are 53.6% more awesome :)
Before a pull requests is submitted, it's a good idea to run the existing suite of tests::
@ -206,7 +206,9 @@ Before a pull requests is submitted, it's a good idea to run the existing suite
Changelog
---------
* `New in development version <https://github.com/jkbr/httpie/compare/0.2.1...master>`_
* `0.2.2dev <https://github.com/jkbr/httpie/compare/0.2.1...master>`_
* The ``METHOD`` positional argument can now be omitted (defaults to ``GET``, or to ``POST`` with data).
* Fixed --verbose --form.
* `0.2.1 <https://github.com/jkbr/httpie/compare/0.2.0...0.2.1>`_ (2012-06-13)
* Added compatibility with ``requests-0.12.1``.
* Dropped custom JSON and HTTP lexers in favor of the ones newly included in ``pygments-1.5``.

View File

@ -14,7 +14,7 @@ def _(text):
desc = '%s <http://httpie.org>'
parser = cliparse.HTTPieArgumentParser(description=desc % __doc__.strip(),)
parser = cliparse.Parser(description=desc % __doc__.strip(),)
parser.add_argument('--version', action='version', version=__version__)
@ -176,8 +176,8 @@ parser.add_argument(
help=_('''
The HTTP method to be used for the request
(GET, POST, PUT, DELETE, PATCH, ...).
If this argument is omitted then httpie will guess the HTTP method.
If there is any data to be sent then method is POST otherwise it is GET.
If this argument is omitted, then HTTPie will guess the HTTP method.
If there is some data to be sent, then it will be POST, otherwise GET.
''')
)
parser.add_argument(
@ -200,7 +200,7 @@ parser.add_argument(
A key-value pair whose type is defined by the separator used. It can be an
HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value)
a raw JSON data field (field_name:=value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding separator in the field name.
''')

View File

@ -25,11 +25,11 @@ SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
DATA_ITEM_SEPARATORS = {
DATA_ITEM_SEPARATORS = [
SEP_DATA,
SEP_DATA_RAW_JSON,
SEP_FILES
}
]
OUT_REQ_HEADERS = 'H'
@ -47,12 +47,12 @@ PRETTIFY_STDOUT_TTY_ONLY = object()
DEFAULT_UA = 'HTTPie/%s' % __version__
class HTTPieArgumentParser(argparse.ArgumentParser):
class Parser(argparse.ArgumentParser):
def parse_args(self, args=None, namespace=None,
stdin=sys.stdin,
stdin_isatty=sys.stdin.isatty()):
args = super(HTTPieArgumentParser, self).parse_args(args, namespace)
args = super(Parser, self).parse_args(args, namespace)
self._validate_output_options(args)
self._validate_auth_options(args)
self._guess_method(args, stdin_isatty)
@ -68,28 +68,9 @@ class HTTPieArgumentParser(argparse.ArgumentParser):
args.data = stdin.read()
def _guess_method(self, args, stdin_isatty=sys.stdin.isatty()):
"""Suggests HTTP method by positional argument values.
In following description by data item it means one of:
* simple data item (key=value)
* JSON raw item (key:=value)
* file item (key@value)
If METHOD argument is omitted and no data ITEM is given then method is GET:
http http://example.com/
- is shortcut for -
http GET http://example.com.
If METHOD argument is omitted but at least one data ITEM
is present then method is POST:
http http://example.com/ hello=world
- is shortcut for -
http POST http://example.com hello=world.
If METHOD is specified then http behaves as it is now.
The first argument should be treated as method
if it matches ^[a-zA-Z]+$ regexp. Otherwise it is url.
"""
Set `args.method`, if not specified, to either POST or GET
based on whether the request has data or not.
"""
if args.method is None:

View File

@ -5,12 +5,14 @@ import httpie
if sys.argv[-1] == 'test':
os.system('python tests/tests.py')
sys.exit()
sys.exit(os.system('python tests/tests.py'))
# Debian has only requests==0.10.1 and httpie.deb depends on that.
requirements = ['requests>=0.10.1', 'Pygments>=1.5']
requirements = [
# Debian has only requests==0.10.1 and httpie.deb depends on that.
'requests>=0.10.1',
'Pygments>=1.5'
]
if sys.version_info[:2] in ((2, 6), (3, 1)):
# argparse has been added in Python 3.2 / 2.7
requirements.append('argparse>=1.2.1')

View File

@ -1,75 +0,0 @@
import unittest
from argparse import Namespace
from httpie.cliparse import HTTPieArgumentParser, KeyValue
__author__ = 'vladimir'
class HTTPieArgumentParserTestCase(unittest.TestCase):
def setUp(self):
self.HTTPieArgumentParserStub = type(HTTPieArgumentParser.__name__, (HTTPieArgumentParser,), {})
self.HTTPieArgumentParserStub.__init__ = lambda self: None
self.parser = HTTPieArgumentParser()
def test_guess_when_method_set_and_valid(self):
args = Namespace()
args.method = 'GET'
args.url = 'http://example.com/'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_guess_when_method_not_set(self):
args = Namespace()
args.method = None
args.url = 'http://example.com/'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_guess_when_method_set_but_invalid_and_data_field(self):
args = Namespace()
args.method = 'http://example.com/'
args.url = 'data=field'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'POST')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [KeyValue(key='data', value='field', sep='=', orig='data=field')])
def test_guess_when_method_set_but_invalid_and_header_field(self):
args = Namespace()
args.method = 'http://example.com/'
args.url = 'test:header'
args.items = []
self.parser._guess_method(args)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [KeyValue(key='test', value='header', sep=':', orig='test:header')])
def test_guess_when_method_set_but_invalid_and_item_exists(self):
args = Namespace()
args.method = 'http://example.com/'
args.url = 'new_item=a'
args.items = [KeyValue(key='old_item', value='b', sep='=', orig='old_item=b')]
self.parser._guess_method(args)
self.assertEquals(args.items, [
KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'),
KeyValue(key='old_item', value='b', sep='=', orig='old_item=b'),
])

View File

@ -1,24 +1,36 @@
# coding:utf-8
import os
import sys
"""
High-level tests.
"""
import unittest
import argparse
from requests.compat import is_py26
import os
import sys
import tempfile
from requests.compat import is_py26
#################################################################
# Utils/setup
#################################################################
# HACK: Prepend ../ to PYTHONPATH so that we can import httpie form there.
TESTS_ROOT = os.path.dirname(__file__)
sys.path.insert(0, os.path.realpath(os.path.join(TESTS_ROOT, '..')))
from httpie import __main__
from httpie import cliparse
from httpie import __main__, cliparse
TEST_FILE = os.path.join(TESTS_ROOT, 'file.txt')
TEST_FILE_PATH = os.path.join(TESTS_ROOT, 'file.txt')
TERMINAL_COLOR_PRESENCE_CHECK = '\x1b['
def http(*args, **kwargs):
"""
Invoke `httpie.__main__.main` with `args` and `kwargs`,
and return a unicode response.
"""
http_kwargs = {
'stdin_isatty': True,
'stdout_isatty': False
@ -32,7 +44,7 @@ def http(*args, **kwargs):
return response
class BaseTest(unittest.TestCase):
class BaseTestCase(unittest.TestCase):
if is_py26:
def assertIn(self, member, container, msg=None):
@ -46,7 +58,149 @@ class BaseTest(unittest.TestCase):
self.assertEqual(sorted(d1.values()), sorted(d2.values()), msg)
class TestItemParsing(BaseTest):
#################################################################
# High-level tests using httpbin.org.
#################################################################
class HTTPieTest(BaseTestCase):
def test_GET(self):
r = http('GET', 'http://httpbin.org/get')
self.assertIn('HTTP/1.1 200', r)
def test_DELETE(self):
r = http('DELETE', 'http://httpbin.org/delete')
self.assertIn('HTTP/1.1 200', r)
def test_PUT(self):
r = http('PUT', 'http://httpbin.org/put', 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_POST_JSON_data(self):
r = http('POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"foo": "bar"', r)
def test_GET_JSON_implicit_accept(self):
r = http('-j', 'GET', 'http://httpbin.org/headers')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/json"', r)
def test_GET_JSON_explicit_accept(self):
r = http('-j', 'GET', 'http://httpbin.org/headers', 'Accept:application/xml')
self.assertIn('HTTP/1.1 200', r)
self.assertIn('"Accept": "application/xml"', r)
def test_POST_form(self):
response = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
def test_POST_stdin(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
self.assertIn('HTTP/1.1 200', r)
def test_headers(self):
response = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
self.assertIn('"User-Agent": "HTTPie', response)
self.assertIn('"Foo": "bar"', response)
class ImplicitHTTPMethodTest(BaseTestCase):
def test_implicit_GET(self):
r = http('http://httpbin.org/get')
self.assertIn('HTTP/1.1 200', r)
def test_implicit_GET_with_headers(self):
r = http('http://httpbin.org/headers', 'Foo:bar')
self.assertIn('"Foo": "bar"', r)
self.assertIn('HTTP/1.1 200', r)
def test_implicit_POST_json(self):
r = http('http://httpbin.org/post', 'hello=world')
self.assertIn('"hello": "world"', r)
self.assertIn('HTTP/1.1 200', r)
def test_implicit_POST_form(self):
r = http('--form', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', r)
self.assertIn('HTTP/1.1 200', r)
def test_implicit_POST_stdin(self):
r = http('--form', 'http://httpbin.org/post',
stdin=open(TEST_FILE_PATH), stdin_isatty=False)
self.assertIn('HTTP/1.1 200', r)
class PrettyFlagTest(BaseTestCase):
"""Test the --pretty / --ugly flag handling."""
def test_pretty_enabled_by_default(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_pretty(self):
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_ugly(self):
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
class VerboseFlagTest(BaseTestCase):
def test_verbose(self):
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
self.assertEqual(r.count('__test__'), 2)
def test_verbose_form(self):
# https://github.com/jkbr/httpie/issues/53
r = http('--verbose', '--form', 'POST', 'http://httpbin.org/post', 'foo=bar', 'baz=bar')
self.assertIn('foo=bar&baz=bar', r)
class MultipartFormDataFileUploadTest(BaseTestCase):
def test_non_existent_file_raises_parse_error(self):
self.assertRaises(cliparse.ParseError, http,
'--form', '--traceback',
'POST', 'http://httpbin.org/post',
'foo@/__does_not_exist__')
def test_upload_ok(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
'test-file@%s' % TEST_FILE_PATH, 'foo=bar')
self.assertIn('"test-file": "__test_file_content__', r)
self.assertIn('"foo": "bar"', r)
class AuthTest(BaseTestCase):
def test_basic_auth(self):
r = http('--auth', 'user:password',
'GET', 'httpbin.org/basic-auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_digest_auth(self):
r = http('--auth-type=digest', '--auth', 'user:password',
'GET', 'httpbin.org/digest-auth/auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
#################################################################
# CLI argument parsing related tests.
#################################################################
class ItemParsingTest(BaseTestCase):
def setUp(self):
self.key_value_type = cliparse.KeyValueType(
@ -70,7 +224,7 @@ class TestItemParsing(BaseTest):
# data
self.key_value_type('baz\\=bar=foo'),
# files
self.key_value_type('bar\\@baz@%s' % TEST_FILE)
self.key_value_type('bar\\@baz@%s' % TEST_FILE_PATH)
])
self.assertDictEqual(headers, {
'foo:bar': 'baz',
@ -98,7 +252,7 @@ class TestItemParsing(BaseTest):
self.key_value_type('eh:'),
self.key_value_type('ed='),
self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % TEST_FILE),
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
])
self.assertDictEqual(headers, {
'header': 'value',
@ -114,112 +268,77 @@ class TestItemParsing(BaseTest):
self.assertIn('test-file', files)
class TestHTTPie(BaseTest):
class HTTPieArgumentParserTestCase(unittest.TestCase):
def test_get(self):
http('GET', 'http://httpbin.org/get')
def setUp(self):
self.parser = cliparse.Parser()
def test_verbose(self):
r = http('--verbose', 'GET', 'http://httpbin.org/get', 'test-header:__test__')
self.assertEqual(r.count('__test__'), 2)
def test_guess_when_method_set_and_valid(self):
args = argparse.Namespace()
args.method = 'GET'
args.url = 'http://example.com/'
args.items = []
def test_verbose_form(self):
# https://github.com/jkbr/httpie/issues/53
r = http('--verbose', '--form', 'POST', 'http://httpbin.org/post', 'foo=bar', 'baz=bar')
self.assertIn('foo=bar&baz=bar', r)
self.parser._guess_method(args)
def test_json(self):
response = http('POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
response2 = http('-j', 'GET', 'http://httpbin.org/headers')
self.assertIn('"Accept": "application/json"', response2)
response3 = http('-j', 'GET', 'http://httpbin.org/headers', 'Accept:application/xml')
self.assertIn('"Accept": "application/xml"', response3)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
def test_form(self):
response = http('--form', 'POST', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', response)
def test_guess_when_method_not_set(self):
args = argparse.Namespace()
args.method = None
args.url = 'http://example.com/'
args.items = []
def test_headers(self):
response = http('GET', 'http://httpbin.org/headers', 'Foo:bar')
self.assertIn('"User-Agent": "HTTPie', response)
self.assertIn('"Foo": "bar"', response)
self.parser._guess_method(args)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(args.items, [])
class TestImplicitHTTPMethod(BaseTest):
def test_guess_when_method_set_but_invalid_and_data_field(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'data=field'
args.items = []
def test_implicit_GET(self):
r = http('http://httpbin.org/get')
self.assertIn('HTTP/1.1 200', r)
self.parser._guess_method(args)
def test_implicit_GET_with_headers(self):
r = http('http://httpbin.org/headers', 'Foo:bar')
self.assertIn('"Foo": "bar"', r)
self.assertIn('HTTP/1.1 200', r)
self.assertEquals(args.method, 'POST')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(
args.items,
[cliparse.KeyValue(key='data', value='field', sep='=', orig='data=field')])
def test_implicit_POST_json(self):
r = http('http://httpbin.org/post', 'hello=world')
self.assertIn('"hello": "world"', r)
self.assertIn('HTTP/1.1 200', r)
def test_guess_when_method_set_but_invalid_and_header_field(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'test:header'
args.items = []
def test_implicit_POST_form(self):
r = http('--form', 'http://httpbin.org/post', 'foo=bar')
self.assertIn('"foo": "bar"', r)
self.assertIn('HTTP/1.1 200', r)
self.parser._guess_method(args)
def test_implicit_POST_stdin(self):
r = http('--form', 'http://httpbin.org/post',
stdin=open(TEST_FILE), stdin_isatty=False)
self.assertIn('HTTP/1.1 200', r)
self.assertEquals(args.method, 'GET')
self.assertEquals(args.url, 'http://example.com/')
self.assertEquals(
args.items,
[cliparse.KeyValue(key='test', value='header', sep=':', orig='test:header')])
def test_guess_when_method_set_but_invalid_and_item_exists(self):
args = argparse.Namespace()
args.method = 'http://example.com/'
args.url = 'new_item=a'
args.items = [
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b')
]
class TestPrettyFlag(BaseTest):
"""Test the --pretty / --ugly flag handling."""
self.parser._guess_method(args)
def test_pretty_enabled_by_default(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_pretty_enabled_by_default_unless_stdin_redirected(self):
r = http('GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_pretty(self):
r = http('--pretty', 'GET', 'http://httpbin.org/get', stdout_isatty=False)
self.assertIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
def test_force_ugly(self):
r = http('--ugly', 'GET', 'http://httpbin.org/get', stdout_isatty=True)
self.assertNotIn(TERMINAL_COLOR_PRESENCE_CHECK, r)
class TestFileUpload(BaseTest):
def test_non_existent_file_raises_parse_error(self):
self.assertRaises(cliparse.ParseError, http,
'--form', '--traceback',
'POST', 'http://httpbin.org/post',
'foo@/__does_not_exist__')
def test_upload_ok(self):
r = http('--form', 'POST', 'http://httpbin.org/post',
'test-file@%s' % TEST_FILE)
self.assertIn('"test-file": "__test_file_content__', r)
class TestAuth(BaseTest):
def test_basic_auth(self):
r = http('--auth', 'user:password',
'GET', 'httpbin.org/basic-auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
def test_digest_auth(self):
r = http('--auth-type=digest', '--auth', 'user:password',
'GET', 'httpbin.org/digest-auth/auth/user/password')
self.assertIn('"authenticated": true', r)
self.assertIn('"user": "user"', r)
self.assertEquals(args.items, [
cliparse.KeyValue(key='new_item', value='a', sep='=', orig='new_item=a'),
cliparse.KeyValue(key='old_item', value='b', sep='=', orig='old_item=b'),
])
if __name__ == '__main__':