Switched to "==" a the separator for URL params.

Also refactored item escaping.
This commit is contained in:
Jakub Roztocil 2012-07-24 14:56:53 +02:00
parent 728a1a195b
commit 9944def703
5 changed files with 114 additions and 78 deletions

View File

@ -83,8 +83,8 @@ File fields (``field@/path/to/file``)
``screenshot@/path/to/file.png``. The presence of a file field results into
a ``multipart/form-data`` request.
Query string parameters (``name=:value``)
Appends the given name/value pair as a query string parameter to the URL.
Query string parameters (``name==value``)
Appends the given name/value pair as a query string parameter to the URL.
Examples
@ -127,11 +127,12 @@ The above will send the same request as if the following HTML form were submitte
<input type="file" name="cv" />
</form>
Query string parameters can be added to any request::
**Query string parameters** can be added to any request without having to quote
the ``&`` characters::
http GET example.com/ search=:donuts
http GET example.com/ search==donuts in==fridge
Will GET the URL "example.com/?search=donuts".
Will ``GET` the URL ``http://example.com/?search=donuts&in=fridge``.
A whole request body can be passed in via **``stdin``** instead, in which
case it will be used with no further processing::
@ -151,7 +152,7 @@ the second one does via ``stdin``::
http https://api.github.com/repos/jkbr/httpie | http httpbin.org/post
Note that when the output is redirected (like the examples above), HTTPie
Note that when the **output is redirected** (like the examples above), HTTPie
applies a different set of defaults then for console output. Namely colors
aren't used (can be forced with ``--pretty``) and only the response body
gets printed (can be overwritten with ``--print``).
@ -165,7 +166,7 @@ request will send the verbatim contents of the file with
http PUT httpbin.org/put @/data/file.xml
When using HTTPie from shell scripts you might want to use the
When using HTTPie from **shell scripts**, you might want to use the
``--check-status`` flag. It instructs HTTPie to exit with an error if the
HTTP status is one of ``3xx``, ``4xx``, or ``5xx``. The exit status will
be ``3`` (unless ``--allow-redirects`` is set), ``4``, or ``5``
@ -213,7 +214,7 @@ See ``http -h`` for more details::
separator used. It can be an HTTP header
(header:value), a data field to be used in the request
body (field_name=value), a raw JSON data field
(field_name:=value), a query parameter (name=:value),
(field_name:=value), a query parameter (name=value),
or a file field (field_name@/path/to/file). You can
use a backslash to escape a colliding separator in the
field name.
@ -333,7 +334,7 @@ Changelog
the new default behaviour is to only print the response body.
(It can still be overriden via the ``--print`` flag.)
* Improved highlighing of HTTP headers.
* Added query string parameters (param=:value).
* Added query string parameters (param==value).
* Added support for terminal colors under Windows.
* `0.2.5 <https://github.com/jkbr/httpie/compare/0.2.2...0.2.5>`_ (2012-07-17)
* Unicode characters in prettified JSON now don't get escaped for

View File

@ -146,7 +146,7 @@ parser.add_argument(
# ``requests.request`` keyword arguments.
parser.add_argument(
'--auth', '-a', type=cliparse.AuthCredentialsType(cliparse.SEP_COMMON),
'--auth', '-a', type=cliparse.AuthCredentialsArgType(cliparse.SEP_COMMON),
help=_('''
username:password.
If only the username is provided (-a username),
@ -174,7 +174,7 @@ parser.add_argument(
)
parser.add_argument(
'--proxy', default=[], action='append',
type=cliparse.KeyValueType(cliparse.SEP_COMMON),
type=cliparse.KeyValueArgType(cliparse.SEP_COMMON),
help=_('''
String mapping protocol to the URL of the proxy
(e.g. http:foo.bar:3128).
@ -221,7 +221,7 @@ parser.add_argument(
parser.add_argument(
'items', nargs='*',
metavar='ITEM',
type=cliparse.KeyValueType(
type=cliparse.KeyValueArgType(
cliparse.SEP_COMMON,
cliparse.SEP_QUERY,
cliparse.SEP_DATA,
@ -233,7 +233,7 @@ parser.add_argument(
separator used. It can be an HTTP header (header:value),
a data field to be used in the request body (field_name=value),
a raw JSON data field (field_name:=value),
a query parameter (name=:value),
a query parameter (name==value),
or a file field (field_name@/path/to/file).
You can use a backslash to escape a colliding
separator in the field name.

View File

@ -16,6 +16,7 @@ except ImportError:
OrderedDict = dict
from requests.structures import CaseInsensitiveDict
from requests.compat import str
from . import __version__
@ -25,7 +26,7 @@ SEP_HEADERS = SEP_COMMON
SEP_DATA = '='
SEP_DATA_RAW_JSON = ':='
SEP_FILES = '@'
SEP_QUERY = '=:'
SEP_QUERY = '=='
DATA_ITEM_SEPARATORS = [
SEP_DATA,
SEP_DATA_RAW_JSON,
@ -61,7 +62,6 @@ class Parser(argparse.ArgumentParser):
if not env.stdin_isatty:
self._body_from_file(args, env.stdin)
if args.auth and not args.auth.has_password():
# stdin has already been read (if not a tty) so
# it's save to prompt now.
@ -99,7 +99,7 @@ class Parser(argparse.ArgumentParser):
# - Set `args.url` correctly.
# - Parse the first item and move it to `args.items[0]`.
item = KeyValueType(
item = KeyValueArgType(
SEP_COMMON,
SEP_QUERY,
SEP_DATA,
@ -119,20 +119,20 @@ class Parser(argparse.ArgumentParser):
def _parse_items(self, args):
"""
Parse `args.items` into `args.headers`,
`args.data`, `args.queries`, and `args.files`.
`args.data`, `args.`, and `args.files`.
"""
args.headers = CaseInsensitiveDict()
args.headers['User-Agent'] = DEFAULT_UA
args.data = OrderedDict()
args.files = OrderedDict()
args.queries = CaseInsensitiveDict()
args.params = OrderedDict()
try:
parse_items(items=args.items,
headers=args.headers,
data=args.data,
files=args.files,
queries=args.queries)
params=args.params)
except ParseError as e:
if args.traceback:
raise
@ -195,49 +195,91 @@ class KeyValue(object):
return self.__dict__ == other.__dict__
class KeyValueType(object):
"""A type used with `argparse`."""
class KeyValueArgType(object):
"""
A key-value pair argument type used with `argparse`.
Parses a key-value arg and constructs a `KeyValue` instance.
Used for headers, form data, and other key-value pair types.
"""
key_value_class = KeyValue
def __init__(self, *separators):
self.separators = separators
self.escapes = ['\\\\' + sep for sep in separators]
def __call__(self, string):
found = {}
found_escapes = []
for esc in self.escapes:
found_escapes += [m.span() for m in re.finditer(esc, string)]
for sep in self.separators:
matches = re.finditer(sep, string)
for match in matches:
start, end = match.span()
inside_escape = False
for estart, eend in found_escapes:
if start >= estart and end <= eend:
inside_escape = True
break
if start in found and len(found[start]) > len(sep):
break
if not inside_escape:
found[start] = sep
"""
Parse `string` and return `self.key_value_class()` instance.
if not found:
The best of `self.separators` is determined (first found, longest).
Back slash escaped characters aren't considered as separators
(or parts thereof). Literal back slash characters have to be escaped
as well (r'\\').
"""
class Escaped(str):
pass
def tokenize(s):
"""
r'foo\=bar\\baz'
=> ['foo', Escaped('='), 'bar', Escaped('\'), 'baz']
"""
tokens = ['']
esc = False
for c in s:
if esc:
tokens.extend([Escaped(c), ''])
esc = False
else:
if c == '\\':
esc = True
else:
tokens[-1] += c
return tokens
tokens = tokenize(string)
# Sorting by length ensures that the longest one will be
# chosen as it will overwrite any shorter ones starting
# at the same position in the `found` dictionary.
separators = sorted(self.separators, key=len)
for i, token in enumerate(tokens):
if isinstance(token, Escaped):
continue
found = {}
for sep in separators:
pos = token.find(sep)
if pos != -1:
found[pos] = sep
if found:
# Starting first, longest separator found.
sep = found[min(found.keys())]
key, value = token.split(sep, 1)
# Any preceding tokens are part of the key.
key = ''.join(tokens[:i]) + key
# Any following tokens are part of the value.
value += ''.join(tokens[i + 1:])
break
else:
raise argparse.ArgumentTypeError(
'"%s" is not a valid value' % string)
# split the string at the earliest non-escaped separator.
seploc = min(found.keys())
sep = found[seploc]
key = string[:seploc]
value = string[seploc + len(sep):]
# remove escape chars
for sepstr in self.separators:
key = key.replace('\\' + sepstr, sepstr)
value = value.replace('\\' + sepstr, sepstr)
return self.key_value_class(key=key, value=value, sep=sep, orig=string)
return self.key_value_class(
key=key, value=value, sep=sep, orig=string)
class AuthCredentials(KeyValue):
@ -260,13 +302,13 @@ class AuthCredentials(KeyValue):
sys.exit(0)
class AuthCredentialsType(KeyValueType):
class AuthCredentialsArgType(KeyValueArgType):
key_value_class = AuthCredentials
def __call__(self, string):
try:
return super(AuthCredentialsType, self).__call__(string)
return super(AuthCredentialsArgType, self).__call__(string)
except argparse.ArgumentTypeError:
# No password provided, will prompt for it later.
return self.key_value_class(
@ -277,10 +319,10 @@ class AuthCredentialsType(KeyValueType):
)
def parse_items(items, data=None, headers=None, files=None, queries=None):
def parse_items(items, data=None, headers=None, files=None, params=None):
"""
Parse `KeyValueType` `items` into `data`, `headers`, `files`,
and `queries`.
Parse `KeyValue` `items` into `data`, `headers`, `files`,
and `params`.
"""
if headers is None:
@ -289,15 +331,15 @@ def parse_items(items, data=None, headers=None, files=None, queries=None):
data = {}
if files is None:
files = {}
if queries is None:
queries = {}
if params is None:
params = {}
for item in items:
value = item.value
key = item.key
if item.sep == SEP_HEADERS:
target = headers
elif item.sep == SEP_QUERY:
target = queries
target = params
elif item.sep == SEP_FILES:
try:
value = open(os.path.expanduser(item.value), 'r')
@ -322,4 +364,4 @@ def parse_items(items, data=None, headers=None, files=None, queries=None):
target[key] = value
return headers, data, files, queries
return headers, data, files, params

View File

@ -53,7 +53,7 @@ def get_response(args):
proxies=dict((p.key, p.value) for p in args.proxy),
files=args.files,
allow_redirects=args.allow_redirects,
params=args.queries,
params=args.params,
)
except (KeyboardInterrupt, SystemExit):

View File

@ -1,3 +1,4 @@
#!/usr/bin/env python
# coding=utf8
"""
@ -598,7 +599,7 @@ class ExitStatusTest(BaseTestCase):
class ItemParsingTest(BaseTestCase):
def setUp(self):
self.key_value_type = cliparse.KeyValueType(
self.key_value_type = cliparse.KeyValueArgType(
cliparse.SEP_HEADERS,
cliparse.SEP_QUERY,
cliparse.SEP_DATA,
@ -613,7 +614,7 @@ class ItemParsingTest(BaseTestCase):
lambda: self.key_value_type(item))
def test_escape(self):
headers, data, files, queries = cliparse.parse_items([
headers, data, files, params = cliparse.parse_items([
# headers
self.key_value_type('foo\\:bar:baz'),
self.key_value_type('jack\\@jill:hill'),
@ -632,15 +633,15 @@ class ItemParsingTest(BaseTestCase):
self.assertIn('bar@baz', files)
def test_escape_longsep(self):
headers, data, files, queries = cliparse.parse_items([
headers, data, files, params = cliparse.parse_items([
self.key_value_type('bob\\:==foo'),
])
self.assertDictEqual(data, {
'bob:=': 'foo',
self.assertDictEqual(params, {
'bob:': 'foo',
})
def test_valid_items(self):
headers, data, files, queries = cliparse.parse_items([
headers, data, files, params = cliparse.parse_items([
self.key_value_type('string=value'),
self.key_value_type('header:value'),
self.key_value_type('list:=["a", 1, {}, false]'),
@ -649,7 +650,7 @@ class ItemParsingTest(BaseTestCase):
self.key_value_type('ed='),
self.key_value_type('bool:=true'),
self.key_value_type('test-file@%s' % TEST_FILE_PATH),
self.key_value_type('query=:value'),
self.key_value_type('query==value'),
])
self.assertDictEqual(headers, {
'header': 'value',
@ -660,21 +661,13 @@ class ItemParsingTest(BaseTestCase):
"string": "value",
"bool": True,
"list": ["a", 1, {}, False],
"obj": {"a": "b"}
"obj": {"a": "b"},
})
self.assertDictEqual(queries, {
self.assertDictEqual(params, {
'query': 'value',
})
self.assertIn('test-file', files)
def test_query_string(self):
headers, data, files, queries = cliparse.parse_items([
self.key_value_type('query=:value'),
])
self.assertDictEqual(queries, {
'query': 'value',
})
class ArgumentParserTestCase(unittest.TestCase):