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`_.
* Fixed ``User-Agent`` overwriting when used within a session.
* 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, ``\``
now only escapes special characters (the ones that are used as key-value
separators).

View File

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

View File

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

View File

@ -309,21 +309,20 @@ class Parser(ArgumentParser):
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:
parse_items(items=self.args.items,
headers=self.args.headers,
data=self.args.data,
files=self.args.files,
params=self.args.params)
items = parse_items(
items=self.args.items,
data_class=ParamsDict if self.args.form else OrderedDict
)
except ParseError as e:
if self.args.traceback:
raise
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:
# `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."""
#noinspection PyMethodOverriding
@ -567,31 +566,46 @@ class ParamDict(OrderedDict):
data and URL params.
"""
assert not isinstance(value, list)
if key not in self:
super(ParamDict, self).__setitem__(key, value)
super(RequestItemsDict, self).__setitem__(key, value)
else:
if not isinstance(self[key], list):
super(ParamDict, self).__setitem__(key, [self[key]])
super(RequestItemsDict, self).__setitem__(key, [self[key]])
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',
['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`,
and `params`.
"""
if headers is None:
headers = CaseInsensitiveDict()
if data is None:
data = OrderedDict()
if files is None:
files = OrderedDict()
if params is None:
params = ParamDict()
headers = []
data = []
files = []
params = []
for item in items:
value = item.value
@ -634,9 +648,12 @@ def parse_items(items, data=None, headers=None, files=None, params=None):
else:
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):

View File

@ -2,11 +2,12 @@
import json
# noinspection PyCompatibility
import argparse
import os
import pytest
from httpie import input
from httpie.input import KeyValue, KeyValueArgType
from httpie.input import KeyValue, KeyValueArgType, DataDict
from httpie import ExitStatus
from httpie.cli import parser
from utils import TestEnvironment, http, HTTP_OK
@ -105,6 +106,25 @@ class TestItemParsing:
assert (items.files['file'][1].read().strip().decode('utf8')
== 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:
def test_query_string_params_in_url(self, httpbin):
@ -134,7 +154,7 @@ class TestQuerystring:
assert '"url": "%s"' % url in r
class TestCLIParser:
class TestURLshorthand:
def test_expand_localhost_shorthand(self):
args = parser.parse_args(args=[':'], env=TestEnvironment())
assert args.url == 'http://localhost'

View File

@ -24,6 +24,17 @@ class TestMultipartFormDataFileUpload:
assert FILE_CONTENT 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:
"""

View File

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