Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proper JSON handling for :=/:=@ #1213

Merged
merged 3 commits into from
Nov 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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')