mirror of
https://github.com/httpie/cli.git
synced 2024-11-22 15:53:13 +01:00
parent
c301305a59
commit
0481957715
@ -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).
|
||||||
|
@ -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:
|
||||||
|
@ -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(
|
||||||
|
@ -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):
|
||||||
|
@ -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'
|
||||||
|
@ -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:
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user