Fixed multiple uploads with the same field name

Closes #267
This commit is contained in:
Jakub Roztocil 2014-10-20 14:40:55 +02:00
parent c301305a59
commit 0481957715
7 changed files with 85 additions and 30 deletions

View File

@ -1277,6 +1277,7 @@ Changelog
* Added `CONTRIBUTING`_. * Added `CONTRIBUTING`_.
* Fixed ``User-Agent`` overwriting when used within a session. * Fixed ``User-Agent`` overwriting when used within a session.
* Fixed handling of empty passwords in URL credentials. * Fixed handling of empty passwords in URL credentials.
* Fixed multiple file uploads with the same form field name.
* To make it easier to deal with Windows paths in request items, ``\`` * To make it easier to deal with Windows paths in request items, ``\``
now only escapes special characters (the ones that are used as key-value now only escapes special characters (the ones that are used as key-value
separators). separators).

View File

@ -48,7 +48,7 @@ class BaseConfigDict(dict):
except ValueError as e: except ValueError as e:
raise ValueError( raise ValueError(
'Invalid %s JSON: %s [%s]' % 'Invalid %s JSON: %s [%s]' %
(type(self).__name__, e.message, self.path) (type(self).__name__, str(e), self.path)
) )
self.update(data) self.update(data)
except IOError as e: except IOError as e:

View File

@ -25,13 +25,16 @@ class Environment(object):
stdout_encoding = None stdout_encoding = None
stderr = sys.stderr stderr = sys.stderr
stderr_isatty = stderr.isatty() stderr_isatty = stderr.isatty()
colors = 256
if not is_windows: if not is_windows:
import curses import curses
curses.setupterm() try:
colors = curses.tigetnum('colors') curses.setupterm()
colors = curses.tigetnum('colors')
except curses.error:
pass
del curses del curses
else: else:
colors = 256
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import colorama.initialise import colorama.initialise
stdout = colorama.initialise.wrap_stream( stdout = colorama.initialise.wrap_stream(

View File

@ -309,21 +309,20 @@ class Parser(ArgumentParser):
and `args.files`. and `args.files`.
""" """
self.args.headers = CaseInsensitiveDict()
self.args.data = ParamDict() if self.args.form else OrderedDict()
self.args.files = OrderedDict()
self.args.params = ParamDict()
try: try:
parse_items(items=self.args.items, items = parse_items(
headers=self.args.headers, items=self.args.items,
data=self.args.data, data_class=ParamsDict if self.args.form else OrderedDict
files=self.args.files, )
params=self.args.params)
except ParseError as e: except ParseError as e:
if self.args.traceback: if self.args.traceback:
raise raise
self.error(e.args[0]) self.error(e.args[0])
else:
self.args.headers = items.headers
self.args.data = items.data
self.args.files = items.files
self.args.params = items.params
if self.args.files and not self.args.form: if self.args.files and not self.args.form:
# `http url @/path/to/file` # `http url @/path/to/file`
@ -555,7 +554,7 @@ class AuthCredentialsArgType(KeyValueArgType):
) )
class ParamDict(OrderedDict): class RequestItemsDict(OrderedDict):
"""Multi-value dict for URL parameters and form data.""" """Multi-value dict for URL parameters and form data."""
#noinspection PyMethodOverriding #noinspection PyMethodOverriding
@ -567,31 +566,46 @@ class ParamDict(OrderedDict):
data and URL params. data and URL params.
""" """
assert not isinstance(value, list)
if key not in self: if key not in self:
super(ParamDict, self).__setitem__(key, value) super(RequestItemsDict, self).__setitem__(key, value)
else: else:
if not isinstance(self[key], list): if not isinstance(self[key], list):
super(ParamDict, self).__setitem__(key, [self[key]]) super(RequestItemsDict, self).__setitem__(key, [self[key]])
self[key].append(value) self[key].append(value)
class ParamsDict(RequestItemsDict):
pass
class DataDict(RequestItemsDict):
def items(self):
for key, values in super(RequestItemsDict, self).items():
if not isinstance(values, list):
values = [values]
for value in values:
yield key, value
RequestItems = namedtuple('RequestItems', RequestItems = namedtuple('RequestItems',
['headers', 'data', 'files', 'params']) ['headers', 'data', 'files', 'params'])
def parse_items(items, data=None, headers=None, files=None, params=None): def parse_items(items,
headers_class=CaseInsensitiveDict,
data_class=OrderedDict,
files_class=DataDict,
params_class=ParamsDict):
"""Parse `KeyValue` `items` into `data`, `headers`, `files`, """Parse `KeyValue` `items` into `data`, `headers`, `files`,
and `params`. and `params`.
""" """
if headers is None: headers = []
headers = CaseInsensitiveDict() data = []
if data is None: files = []
data = OrderedDict() params = []
if files is None:
files = OrderedDict()
if params is None:
params = ParamDict()
for item in items: for item in items:
value = item.value value = item.value
@ -634,9 +648,12 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
else: else:
raise TypeError(item) raise TypeError(item)
target[item.key] = value target.append((item.key, value))
return RequestItems(headers, data, files, params) return RequestItems(headers_class(headers),
data_class(data),
files_class(files),
params_class(params))
def readable_file_arg(filename): def readable_file_arg(filename):

View File

@ -2,11 +2,12 @@
import json import json
# noinspection PyCompatibility # noinspection PyCompatibility
import argparse import argparse
import os
import pytest import pytest
from httpie import input from httpie import input
from httpie.input import KeyValue, KeyValueArgType from httpie.input import KeyValue, KeyValueArgType, DataDict
from httpie import ExitStatus from httpie import ExitStatus
from httpie.cli import parser from httpie.cli import parser
from utils import TestEnvironment, http, HTTP_OK from utils import TestEnvironment, http, HTTP_OK
@ -105,6 +106,25 @@ class TestItemParsing:
assert (items.files['file'][1].read().strip().decode('utf8') assert (items.files['file'][1].read().strip().decode('utf8')
== FILE_CONTENT) == FILE_CONTENT)
def test_multiple_file_fields_with_same_field_name(self):
items = input.parse_items([
self.key_value('file_field@' + FILE_PATH_ARG),
self.key_value('file_field@' + FILE_PATH_ARG),
])
assert len(items.files['file_field']) == 2
def test_multiple_text_fields_with_same_field_name(self):
items = input.parse_items(
[self.key_value('text_field=a'),
self.key_value('text_field=b')],
data_class=DataDict
)
assert items.data['text_field'] == ['a', 'b']
assert list(items.data.items()) == [
('text_field', 'a'),
('text_field', 'b'),
]
class TestQuerystring: class TestQuerystring:
def test_query_string_params_in_url(self, httpbin): def test_query_string_params_in_url(self, httpbin):
@ -134,7 +154,7 @@ class TestQuerystring:
assert '"url": "%s"' % url in r assert '"url": "%s"' % url in r
class TestCLIParser: class TestURLshorthand:
def test_expand_localhost_shorthand(self): def test_expand_localhost_shorthand(self):
args = parser.parse_args(args=[':'], env=TestEnvironment()) args = parser.parse_args(args=[':'], env=TestEnvironment())
assert args.url == 'http://localhost' assert args.url == 'http://localhost'

View File

@ -24,6 +24,17 @@ class TestMultipartFormDataFileUpload:
assert FILE_CONTENT in r assert FILE_CONTENT in r
assert '"foo": "bar"' in r assert '"foo": "bar"' in r
def test_upload_multiple_fields_with_the_same_name(self, httpbin):
r = http('--form', '--verbose', 'POST', httpbin.url + '/post',
'test-file@%s' % FILE_PATH_ARG,
'test-file@%s' % FILE_PATH_ARG)
assert HTTP_OK in r
assert r.count('Content-Disposition: form-data; name="test-file";'
' filename="%s"' % os.path.basename(FILE_PATH)) == 2
# Should be 4, but is 3 because httpbin
# doesn't seem to support filed field lists
assert r.count(FILE_CONTENT) in [3, 4]
class TestRequestBodyFromFilePath: class TestRequestBodyFromFilePath:
""" """

View File

@ -16,3 +16,6 @@ deps =
commands = commands =
py.test --verbose --doctest-modules --basetemp={envtmpdir} {posargs:./tests ./httpie} py.test --verbose --doctest-modules --basetemp={envtmpdir} {posargs:./tests ./httpie}
[pytest]
addopts = --tb=native