diff --git a/README.rst b/README.rst index 768963f9d7..d46cc0bdf4 100644 --- a/README.rst +++ b/README.rst @@ -328,6 +328,8 @@ Changelog --------- * `0.2.6dev `_ + * Form data and URL params can now have mutiple fields with the same name + (e.g.,``http -f url a=1 a=2``). * Added ``--check-status`` to exit with an error for HTTP 3xx, 4xx and 5xx (3, 4, 5). * If the output is piped to another program or redirected to a file, diff --git a/httpie/cli.py b/httpie/cli.py index e6932cfb4e..f7e544a459 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -155,7 +155,7 @@ def _(text): ) parser.add_argument( - '--auth-type', choices=['basic', 'digest'], + '--auth-type', choices=['basic', 'digest'], default='basic', help=_(''' The authentication mechanism to be used. Defaults to "basic". diff --git a/httpie/cliparse.py b/httpie/cliparse.py index 736b02e764..3fbd236419 100644 --- a/httpie/cliparse.py +++ b/httpie/cliparse.py @@ -56,7 +56,6 @@ def parse_args(self, env, args=None, namespace=None): args = super(Parser, self).parse_args(args, namespace) self._process_output_options(args, env) - self._validate_auth_options(args) self._guess_method(args, env) self._parse_items(args) @@ -124,9 +123,9 @@ def _parse_items(self, args): """ args.headers = CaseInsensitiveDict() args.headers['User-Agent'] = DEFAULT_UA - args.data = OrderedDict() + args.data = ParamDict() if args.form else OrderedDict() args.files = OrderedDict() - args.params = OrderedDict() + args.params = ParamDict() try: parse_items(items=args.items, headers=args.headers, @@ -173,10 +172,6 @@ def _process_output_options(self, args, env): ','.join(unknown) ) - def _validate_auth_options(self, args): - if args.auth_type and not args.auth: - self.error('--auth-type can only be used with --auth') - class ParseError(Exception): pass @@ -319,6 +314,28 @@ def __call__(self, string): ) +class ParamDict(OrderedDict): + + def __setitem__(self, key, value): + """ + If `key` is assigned more than once, `self[key]` holds a + `list` of all the values. + + This allows having multiple fields with the same name in form + 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: + if not isinstance(self[key], list): + super(ParamDict, self).__setitem__(key, [self[key]]) + self[key].append(value) + + def parse_items(items, data=None, headers=None, files=None, params=None): """ Parse `KeyValue` `items` into `data`, `headers`, `files`, @@ -332,7 +349,7 @@ def parse_items(items, data=None, headers=None, files=None, params=None): if files is None: files = {} if params is None: - params = {} + params = ParamDict() for item in items: value = item.value key = item.key diff --git a/httpie/core.py b/httpie/core.py index 4cda53a8ef..fecce1b760 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -37,10 +37,10 @@ def get_response(args): try: credentials = None if args.auth: - auth_type = (requests.auth.HTTPDigestAuth - if args.auth_type == 'digest' - else requests.auth.HTTPBasicAuth) - credentials = auth_type(args.auth.key, args.auth.value) + credentials = { + 'basic': requests.auth.HTTPBasicAuth, + 'digest': requests.auth.HTTPDigestAuth, + }[args.auth_type](args.auth.key, args.auth.value) return requests.request( method=args.method.lower(), diff --git a/tests/tests.py b/tests/tests.py index 0ef67e7338..e5d238de37 100755 --- a/tests/tests.py +++ b/tests/tests.py @@ -7,7 +7,7 @@ To make it run faster and offline you can:: # Install `httpbin` locally - pip install httpbin + pip install git+https://github.com/kennethreitz/httpbin.git # Run it httpbin @@ -15,6 +15,9 @@ # Run the tests against it HTTPBIN_URL=http://localhost:5000 python setup.py test + # Test all Python environments + HTTPBIN_URL=http://localhost:5000 tox + """ import os import sys @@ -55,11 +58,13 @@ def httpbin(path): class Response(str): """ A unicode subclass holding the output of `main()`, and also - the exit status and contents of ``stderr``. + the exit status, the contents of ``stderr``, and de-serialized + JSON response (if possible). """ exit_status = None stderr = None + json = None def http(*args, **kwargs): @@ -80,7 +85,7 @@ def http(*args, **kwargs): stdout = kwargs['env'].stdout = tempfile.TemporaryFile() stderr = kwargs['env'].stderr = tempfile.TemporaryFile() - exit_status = main(args=args, **kwargs) + exit_status = main(args=['--traceback'] + list(args), **kwargs) stdout.seek(0) stderr.seek(0) @@ -92,6 +97,19 @@ def http(*args, **kwargs): stdout.close() stderr.close() + if TERMINAL_COLOR_PRESENCE_CHECK not in r: + # De-serialize JSON body if possible. + if r.strip().startswith('{'): + r.json = json.loads(r) + elif r.count('Content-Type:') == 1 and 'application/json' in r: + try: + j = r.strip()[r.strip().rindex('\n\n'):] + except ValueError: + pass + else: + r.strip().index('\n') + r.json = json.loads(j) + return r @@ -157,6 +175,19 @@ def test_POST_form(self): self.assertIn('HTTP/1.1 200', r) self.assertIn('"foo": "bar"', r) + def test_POST_form_multiple_values(self): + r = http( + '--form', + 'POST', + httpbin('/post'), + 'foo=bar', + 'foo=baz', + ) + self.assertIn('HTTP/1.1 200', r) + self.assertDictEqual(r.json['form'], { + 'foo': ['bar', 'baz'] + }) + def test_POST_stdin(self): env = Environment( @@ -490,8 +521,7 @@ class AuthTest(BaseTestCase): def test_basic_auth(self): r = http( - '--auth', - 'user:password', + '--auth=user:password', 'GET', httpbin('/basic-auth/user/password') ) @@ -502,8 +532,7 @@ def test_basic_auth(self): def test_digest_auth(self): r = http( '--auth-type=digest', - '--auth', - 'user:password', + '--auth=user:password', 'GET', httpbin('/digest-auth/auth/user/password') )