Skip to content

Commit

Permalink
Proper JSON handling for :=/:=@ (#1213)
Browse files Browse the repository at this point in the history
* Proper JSON handling for :=/:=@

* document the behavior

* fixup docs
  • Loading branch information
isidentical committed Nov 26, 2021
1 parent 0fc6331 commit 6bdcdf1
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](https://semver.org/).
- Added support for sending multiple HTTP headers with the same name. ([#130](https://github.com/httpie/httpie/issues/130))
- Added support for receving multiple HTTP headers with the same name, individually. ([#1207](https://github.com/httpie/httpie/issues/1207))
- Added support for keeping `://` in the URL argument to allow quick conversions of pasted URLs into HTTPie calls just by adding a space after the protocol name (`$ https ://pie.dev``https://pie.dev`). ([#1195](https://github.com/httpie/httpie/issues/1195))
- Added support for basic JSON types on `--form`/`--multipart` when using JSON only operators (`:=`/`:=@`). ([#1212](https://github.com/httpie/httpie/issues/1212))

## [2.6.0](https://github.com/httpie/httpie/compare/2.5.0...2.6.0) (2021-10-14)

Expand Down
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,10 @@ Host: pie.dev
}
```
The `:=`/`:=@` syntax is JSON-specific. You can switch your request to `--form` or `--multipart`,
and string, float, and number values will continue to be serialized (as string form values).
Other JSON types, however, are not allowed with `--form` or `--multipart`.
### Raw and complex JSON
Please note that with the [request items](#request-items) data field syntax, commands can quickly become unwieldy when sending complex structures.
Expand Down
2 changes: 1 addition & 1 deletion httpie/cli/argparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def _parse_items(self):
try:
request_items = RequestItems.from_args(
request_item_args=self.args.request_items,
as_form=self.args.form,
request_type=self.args.request_type,
)
except ParseError as e:
if self.args.traceback:
Expand Down
40 changes: 33 additions & 7 deletions httpie/cli/requestitems.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import functools
from typing import Callable, Dict, IO, List, Optional, Tuple, Union

from .argtypes import KeyValueArg
Expand All @@ -7,7 +8,7 @@
SEPARATOR_DATA_EMBED_RAW_JSON_FILE,
SEPARATOR_DATA_RAW_JSON, SEPARATOR_DATA_STRING, SEPARATOR_FILE_UPLOAD,
SEPARATOR_FILE_UPLOAD_TYPE, SEPARATOR_HEADER, SEPARATOR_HEADER_EMPTY,
SEPARATOR_QUERY_PARAM,
SEPARATOR_QUERY_PARAM, RequestType
)
from .dicts import (
BaseMultiDict, MultipartRequestDataDict, RequestDataDict,
Expand All @@ -20,9 +21,11 @@

class RequestItems:

def __init__(self, as_form=False):
def __init__(self, request_type: Optional[RequestType] = None):
self.headers = HTTPHeadersDict()
self.data = RequestDataDict() if as_form else RequestJSONDataDict()
self.request_type = request_type
self.is_json = request_type is None or request_type is RequestType.JSON
self.data = RequestJSONDataDict() if self.is_json else RequestDataDict()
self.files = RequestFilesDict()
self.params = RequestQueryParamsDict()
# To preserve the order of fields in file upload multipart requests.
Expand All @@ -32,9 +35,9 @@ def __init__(self, as_form=False):
def from_args(
cls,
request_item_args: List[KeyValueArg],
as_form=False,
request_type: Optional[RequestType] = None,
) -> 'RequestItems':
instance = cls(as_form=as_form)
instance = cls(request_type=request_type)
rules: Dict[str, Tuple[Callable, dict]] = {
SEPARATOR_HEADER: (
process_header_arg,
Expand All @@ -61,11 +64,11 @@ def from_args(
instance.data,
),
SEPARATOR_DATA_RAW_JSON: (
process_data_raw_json_embed_arg,
json_only(instance, process_data_raw_json_embed_arg),
instance.data,
),
SEPARATOR_DATA_EMBED_RAW_JSON_FILE: (
process_data_embed_raw_json_file_arg,
json_only(instance, process_data_embed_raw_json_file_arg),
instance.data,
),
}
Expand Down Expand Up @@ -127,6 +130,29 @@ def process_data_embed_file_contents_arg(arg: KeyValueArg) -> str:
return load_text_file(arg)


def json_only(items: RequestItems, func: Callable[[KeyValueArg], JSONType]) -> str:
if items.is_json:
return func

@functools.wraps(func)
def wrapper(*args, **kwargs) -> str:
try:
ret = func(*args, **kwargs)
except ParseError:
ret = None

# If it is a basic type, then allow it
if isinstance(ret, (str, int, float)):
return str(ret)
else:
raise ParseError(
'Can\'t use complex JSON value types with '
'--form/--multipart.'
)

return wrapper


def process_data_embed_raw_json_file_arg(arg: KeyValueArg) -> JSONType:
contents = load_text_file(arg)
value = load_json(arg, contents)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def test_multiple_text_fields_with_same_field_name(self):
self.key_value_arg('text_field=a'),
self.key_value_arg('text_field=b')
],
as_form=True,
request_type=constants.RequestType.FORM,
)
assert items.data['text_field'] == ['a', 'b']
assert list(items.data.items()) == [
Expand Down
36 changes: 36 additions & 0 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import responses

from httpie.cli.constants import PRETTY_MAP
from httpie.cli.exceptions import ParseError
from httpie.compat import is_windows
from httpie.output.formatters.colors import ColorFormatter
from httpie.utils import JsonDictPreservingDuplicateKeys
Expand Down Expand Up @@ -116,3 +117,38 @@ def test_duplicate_keys_support_from_input_file():
# Check --unsorted
r = http(*args, '--unsorted')
assert JSON_WITH_DUPES_FORMATTED_UNSORTED in r


@pytest.mark.parametrize("value", [
1,
1.1,
True,
'some_value'
])
def test_simple_json_arguments_with_non_json(httpbin, value):
r = http(
'--form',
httpbin + '/post',
f'option:={json.dumps(value)}',
)
assert r.json['form'] == {'option': str(value)}


@pytest.mark.parametrize("request_type", [
"--form",
"--multipart",
])
@pytest.mark.parametrize("value", [
[1, 2, 3],
{'a': 'b'},
None
])
def test_complex_json_arguments_with_non_json(httpbin, request_type, value):
with pytest.raises(ParseError) as cm:
http(
request_type,
httpbin + '/post',
f'option:={json.dumps(value)}',
)

cm.match('Can\'t use complex JSON value types')

0 comments on commit 6bdcdf1

Please sign in to comment.