Skip to content

Commit 254a70f

Browse files
Pass input JSON value parameter as-is to the API call (#807)
1 parent 6e87a01 commit 254a70f

File tree

4 files changed

+32
-53
lines changed

4 files changed

+32
-53
lines changed

linodecli/api_request.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from linodecli.helpers import API_CA_PATH, API_VERSION_OVERRIDE
1919

2020
from .baked.operation import (
21-
ExplicitEmptyDictValue,
2221
ExplicitEmptyListValue,
22+
ExplicitJsonValue,
2323
ExplicitNullValue,
2424
OpenAPIOperation,
2525
)
@@ -314,14 +314,14 @@ def _traverse_request_body(o: Any) -> Any:
314314
result[k] = []
315315
continue
316316

317-
if isinstance(v, ExplicitEmptyDictValue):
318-
result[k] = {}
319-
continue
320-
321317
if isinstance(v, ExplicitNullValue):
322318
result[k] = None
323319
continue
324320

321+
if isinstance(v, ExplicitJsonValue):
322+
result[k] = v.json_value
323+
continue
324+
325325
value = _traverse_request_body(v)
326326

327327
# We should exclude implicit empty lists

linodecli/baked/operation.py

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
import re
1111
import sys
1212
from collections import defaultdict
13+
from collections.abc import Callable
14+
from dataclasses import dataclass
1315
from getpass import getpass
1416
from os import environ, path
15-
from typing import Any, Dict, List, Optional, Tuple, Union
17+
from typing import Any, Dict, List, Optional, Tuple
1618
from urllib.parse import urlparse
1719

1820
import openapi3.paths
@@ -49,46 +51,12 @@ def parse_boolean(value: str) -> bool:
4951
raise argparse.ArgumentTypeError("Expected a boolean value")
5052

5153

52-
def parse_dict(
53-
value: str,
54-
) -> Union[Dict[str, Any], "ExplicitEmptyDictValue", "ExplicitEmptyListValue"]:
55-
"""
56-
A helper function to decode incoming JSON data as python dicts. This is
57-
intended to be passed to the `type=` kwarg for ArgumentParaser.add_argument.
58-
59-
:param value: The json string to be parsed into dict.
60-
:type value: str
61-
62-
:returns: The dict value of the input.
63-
:rtype: dict, ExplicitEmptyDictValue, or ExplicitEmptyListValue
64-
"""
65-
if not isinstance(value, str):
66-
raise argparse.ArgumentTypeError("Expected a JSON string")
67-
68-
try:
69-
result = json.loads(value)
70-
except Exception as e:
71-
raise argparse.ArgumentTypeError("Expected a JSON string") from e
72-
73-
# This is necessary because empty dicts and lists are excluded from requests
74-
# by default, but we still want to support user-defined empty dict
75-
# strings. This is particularly helpful when updating LKE node pool
76-
# labels and taints.
77-
if isinstance(result, dict) and result == {}:
78-
return ExplicitEmptyDictValue()
79-
80-
if isinstance(result, list) and result == []:
81-
return ExplicitEmptyListValue()
82-
83-
return result
84-
85-
8654
TYPES = {
8755
"string": str,
8856
"integer": int,
8957
"boolean": parse_boolean,
9058
"array": list,
91-
"object": parse_dict,
59+
"object": lambda value: ExplicitJsonValue(json_value=json.loads(value)),
9260
"number": float,
9361
}
9462

@@ -106,13 +74,16 @@ class ExplicitEmptyListValue:
10674
"""
10775

10876

109-
class ExplicitEmptyDictValue:
77+
@dataclass
78+
class ExplicitJsonValue:
11079
"""
111-
A special type used to explicitly pass empty dictionaries to the API.
80+
A special type used to explicitly pass raw JSON from user input as is.
11281
"""
11382

83+
json_value: Any
84+
11485

115-
def wrap_parse_nullable_value(arg_type: str) -> TYPES:
86+
def wrap_parse_nullable_value(arg_type: str) -> Callable[[Any], Any]:
11687
"""
11788
A helper function to parse `null` as None for nullable CLI args.
11889
This is intended to be called and passed to the `type=` kwarg for ArgumentParser.add_argument.

tests/unit/test_api_request.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414

1515
from linodecli import ExitCodes, api_request
1616
from linodecli.baked.operation import (
17-
ExplicitEmptyDictValue,
1817
ExplicitEmptyListValue,
18+
ExplicitJsonValue,
1919
ExplicitNullValue,
2020
)
2121

@@ -667,7 +667,7 @@ def test_traverse_request_body(self):
667667
"baz": ExplicitNullValue(),
668668
},
669669
"cool": [],
670-
"pretty_cool": ExplicitEmptyDictValue(),
670+
"pretty_cool": ExplicitJsonValue(json_value={}),
671671
"cooler": ExplicitEmptyListValue(),
672672
"coolest": ExplicitNullValue(),
673673
}

tests/unit/test_operation.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from linodecli.baked import operation
77
from linodecli.baked.operation import (
88
TYPES,
9-
ExplicitEmptyDictValue,
109
ExplicitEmptyListValue,
10+
ExplicitJsonValue,
1111
ExplicitNullValue,
1212
OpenAPIOperation,
1313
)
@@ -195,7 +195,7 @@ def test_parse_args_object_list(self, create_operation):
195195
"field_string": "test1",
196196
"field_int": 123,
197197
"field_dict": {"nested_string": "test2", "nested_int": 789},
198-
"field_array": ["foo", "bar"],
198+
"field_array": ExplicitJsonValue(json_value=["foo", "bar"]),
199199
"nullable_string": None, # We expect this to be filtered out later
200200
},
201201
{"field_int": 456, "field_dict": {"nested_string": "test3"}},
@@ -216,7 +216,7 @@ def test_parse_args_object_list_json(self, create_operation):
216216
["--object_list", json.dumps(expected)]
217217
)
218218

219-
assert result.object_list == expected
219+
assert result.object_list.json_value == expected
220220

221221
def test_parse_args_conflicting_parent_child(self, create_operation):
222222
stderr_buf = io.StringIO()
@@ -296,19 +296,27 @@ def test_object_arg_action_basic(self):
296296

297297
# User specifies a normal object (dict)
298298
result = parser.parse_args(["--foo", '{"test-key": "test-value"}'])
299-
assert getattr(result, "foo") == {"test-key": "test-value"}
299+
foo = getattr(result, "foo")
300+
assert isinstance(foo, ExplicitJsonValue)
301+
assert foo.json_value == {"test-key": "test-value"}
300302

301303
# User specifies a normal object (list)
302304
result = parser.parse_args(["--foo", '[{"test-key": "test-value"}]'])
303-
assert getattr(result, "foo") == [{"test-key": "test-value"}]
305+
foo = getattr(result, "foo")
306+
assert isinstance(foo, ExplicitJsonValue)
307+
assert foo.json_value == [{"test-key": "test-value"}]
304308

305309
# User wants an explicitly empty object (dict)
306310
result = parser.parse_args(["--foo", "{}"])
307-
assert isinstance(getattr(result, "foo"), ExplicitEmptyDictValue)
311+
foo = getattr(result, "foo")
312+
assert isinstance(foo, ExplicitJsonValue)
313+
assert foo.json_value == {}
308314

309315
# User wants an explicitly empty object (list)
310316
result = parser.parse_args(["--foo", "[]"])
311-
assert isinstance(getattr(result, "foo"), ExplicitEmptyListValue)
317+
foo = getattr(result, "foo")
318+
assert isinstance(foo, ExplicitJsonValue)
319+
assert foo.json_value == []
312320

313321
# User doesn't specify the list
314322
result = parser.parse_args([])

0 commit comments

Comments
 (0)