Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions linodecli/api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
from linodecli.helpers import API_CA_PATH, API_VERSION_OVERRIDE

from .baked.operation import (
ExplicitEmptyDictValue,
ExplicitEmptyListValue,
ExplicitJsonValue,
ExplicitNullValue,
OpenAPIOperation,
)
Expand Down Expand Up @@ -314,14 +314,14 @@ def _traverse_request_body(o: Any) -> Any:
result[k] = []
continue

if isinstance(v, ExplicitEmptyDictValue):
result[k] = {}
continue

if isinstance(v, ExplicitNullValue):
result[k] = None
continue

if isinstance(v, ExplicitJsonValue):
result[k] = v.json_value
continue

value = _traverse_request_body(v)

# We should exclude implicit empty lists
Expand Down
49 changes: 10 additions & 39 deletions linodecli/baked/operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import re
import sys
from collections import defaultdict
from collections.abc import Callable
from dataclasses import dataclass
from getpass import getpass
from os import environ, path
from typing import Any, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import urlparse

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


def parse_dict(
value: str,
) -> Union[Dict[str, Any], "ExplicitEmptyDictValue", "ExplicitEmptyListValue"]:
"""
A helper function to decode incoming JSON data as python dicts. This is
intended to be passed to the `type=` kwarg for ArgumentParaser.add_argument.

:param value: The json string to be parsed into dict.
:type value: str

:returns: The dict value of the input.
:rtype: dict, ExplicitEmptyDictValue, or ExplicitEmptyListValue
"""
if not isinstance(value, str):
raise argparse.ArgumentTypeError("Expected a JSON string")

try:
result = json.loads(value)
except Exception as e:
raise argparse.ArgumentTypeError("Expected a JSON string") from e

# This is necessary because empty dicts and lists are excluded from requests
# by default, but we still want to support user-defined empty dict
# strings. This is particularly helpful when updating LKE node pool
# labels and taints.
if isinstance(result, dict) and result == {}:
return ExplicitEmptyDictValue()

if isinstance(result, list) and result == []:
return ExplicitEmptyListValue()

return result


TYPES = {
"string": str,
"integer": int,
"boolean": parse_boolean,
"array": list,
"object": parse_dict,
"object": lambda value: ExplicitJsonValue(json_value=json.loads(value)),
"number": float,
}

Expand All @@ -106,13 +74,16 @@ class ExplicitEmptyListValue:
"""


class ExplicitEmptyDictValue:
@dataclass
class ExplicitJsonValue:
"""
A special type used to explicitly pass empty dictionaries to the API.
A special type used to explicitly pass raw JSON from user input as is.
"""

json_value: Any


def wrap_parse_nullable_value(arg_type: str) -> TYPES:
def wrap_parse_nullable_value(arg_type: str) -> Callable[[Any], Any]:
"""
A helper function to parse `null` as None for nullable CLI args.
This is intended to be called and passed to the `type=` kwarg for ArgumentParser.add_argument.
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_api_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@

from linodecli import ExitCodes, api_request
from linodecli.baked.operation import (
ExplicitEmptyDictValue,
ExplicitEmptyListValue,
ExplicitJsonValue,
ExplicitNullValue,
)

Expand Down Expand Up @@ -667,7 +667,7 @@ def test_traverse_request_body(self):
"baz": ExplicitNullValue(),
},
"cool": [],
"pretty_cool": ExplicitEmptyDictValue(),
"pretty_cool": ExplicitJsonValue(json_value={}),
"cooler": ExplicitEmptyListValue(),
"coolest": ExplicitNullValue(),
}
Expand Down
22 changes: 15 additions & 7 deletions tests/unit/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from linodecli.baked import operation
from linodecli.baked.operation import (
TYPES,
ExplicitEmptyDictValue,
ExplicitEmptyListValue,
ExplicitJsonValue,
ExplicitNullValue,
OpenAPIOperation,
)
Expand Down Expand Up @@ -195,7 +195,7 @@ def test_parse_args_object_list(self, create_operation):
"field_string": "test1",
"field_int": 123,
"field_dict": {"nested_string": "test2", "nested_int": 789},
"field_array": ["foo", "bar"],
"field_array": ExplicitJsonValue(json_value=["foo", "bar"]),
"nullable_string": None, # We expect this to be filtered out later
},
{"field_int": 456, "field_dict": {"nested_string": "test3"}},
Expand All @@ -216,7 +216,7 @@ def test_parse_args_object_list_json(self, create_operation):
["--object_list", json.dumps(expected)]
)

assert result.object_list == expected
assert result.object_list.json_value == expected

def test_parse_args_conflicting_parent_child(self, create_operation):
stderr_buf = io.StringIO()
Expand Down Expand Up @@ -296,19 +296,27 @@ def test_object_arg_action_basic(self):

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

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

# User wants an explicitly empty object (dict)
result = parser.parse_args(["--foo", "{}"])
assert isinstance(getattr(result, "foo"), ExplicitEmptyDictValue)
foo = getattr(result, "foo")
assert isinstance(foo, ExplicitJsonValue)
assert foo.json_value == {}

# User wants an explicitly empty object (list)
result = parser.parse_args(["--foo", "[]"])
assert isinstance(getattr(result, "foo"), ExplicitEmptyListValue)
foo = getattr(result, "foo")
assert isinstance(foo, ExplicitJsonValue)
assert foo.json_value == []

# User doesn't specify the list
result = parser.parse_args([])
Expand Down
Loading