Skip to content

Commit

Permalink
Allow multiple fields with the same name.
Browse files Browse the repository at this point in the history
Applies to form data and URL params:

    http -f url a=1 a=2
    http url a==1 a==2
  • Loading branch information
jkbrzt committed Jul 24, 2012
1 parent 9944def commit 7af08b6
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 20 deletions.
2 changes: 2 additions & 0 deletions README.rst
Expand Up @@ -328,6 +328,8 @@ Changelog
---------

* `0.2.6dev <https://github.com/jkbr/httpie/compare/0.2.5...master>`_
* 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,
Expand Down
2 changes: 1 addition & 1 deletion httpie/cli.py
Expand Up @@ -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".
Expand Down
33 changes: 25 additions & 8 deletions httpie/cliparse.py
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`,
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions httpie/core.py
Expand Up @@ -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(),
Expand Down
43 changes: 36 additions & 7 deletions tests/tests.py
Expand Up @@ -7,14 +7,17 @@
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
# 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
Expand Down Expand Up @@ -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):
Expand All @@ -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)
Expand All @@ -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


Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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')
)
Expand All @@ -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')
)
Expand Down

0 comments on commit 7af08b6

Please sign in to comment.