You've already forked httpie-cli
							
							
				mirror of
				https://github.com/httpie/cli.git
				synced 2025-10-30 23:47:52 +02:00 
			
		
		
		
	Proper JSON handling for :=/:=@ (#1213)
* Proper JSON handling for :=/:=@ * document the behavior * fixup docs
This commit is contained in:
		| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -710,6 +710,10 @@ Host: pie.dev | ||||
|  | ||||
| Furthermore, the structure syntax only allows you to send an object as the JSON document, but not an array, etc. | ||||
| Here, again, the solution is to use [redirected input](#redirected-input). | ||||
|  | ||||
| ## Forms | ||||
|  | ||||
| Submitting forms is very similar to sending [JSON](#json) requests. | ||||
| Often the only difference is in adding the `--form, -f` option, which ensures that data fields are serialized as, and `Content-Type` is set to `application/x-www-form-urlencoded; charset=utf-8`. | ||||
| It is possible to make form data the implicit content type instead of JSON via the [config](#config) file. | ||||
|  | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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()) == [ | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
		Reference in New Issue
	
	Block a user