diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a79a76..a48efcbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/docs/README.md b/docs/README.md index b6ced336..a887e937 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. diff --git a/httpie/cli/argparser.py b/httpie/cli/argparser.py index 54c89965..1a3d7589 100644 --- a/httpie/cli/argparser.py +++ b/httpie/cli/argparser.py @@ -364,7 +364,7 @@ class HTTPieArgumentParser(argparse.ArgumentParser): 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: diff --git a/httpie/cli/requestitems.py b/httpie/cli/requestitems.py index c1c2568e..c9101943 100644 --- a/httpie/cli/requestitems.py +++ b/httpie/cli/requestitems.py @@ -1,4 +1,5 @@ import os +import functools from typing import Callable, Dict, IO, List, Optional, Tuple, Union from .argtypes import KeyValueArg @@ -7,7 +8,7 @@ from .constants import ( 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, @@ -20,9 +21,11 @@ from ..utils import get_content_type, load_json_preserve_order_and_dupe_keys 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. @@ -32,9 +35,9 @@ class RequestItems: 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, @@ -61,11 +64,11 @@ class RequestItems: 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, ), } @@ -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) diff --git a/tests/test_cli.py b/tests/test_cli.py index 09a39c11..fd3f0dd0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -132,7 +132,7 @@ class TestItemParsing: 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()) == [ diff --git a/tests/test_json.py b/tests/test_json.py index 9b0f17ce..f19aa7e1 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -4,6 +4,7 @@ import pytest 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 @@ -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')