Skip to content

Commit c2aba48

Browse files
Fix support for deeply nested oneOfs; group nested oneOf options on help pages (#785)
1 parent d5136db commit c2aba48

File tree

7 files changed

+252
-51
lines changed

7 files changed

+252
-51
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ test-unit:
6464
@mkdir -p /tmp/linode/.config
6565
@orig_xdg_config_home=$${XDG_CONFIG_HOME:-}; \
6666
export LINODE_CLI_TEST_MODE=1 XDG_CONFIG_HOME=/tmp/linode/.config; \
67-
pytest -v tests/unit; \
67+
pytest -vv tests/unit; \
6868
exit_code=$$?; \
6969
export XDG_CONFIG_HOME=$$orig_xdg_config_home; \
7070
exit $$exit_code

linodecli/baked/request.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ def __init__( # pylint: disable=too-many-arguments
4848
:type parent: Optional[str]
4949
:param depth: The depth of this argument, or how many parent arguments this argument has.
5050
:type depth: int
51+
:param option_variants: A mapping of options, defined using oneOf in the to spec,
52+
to a variant of this argument.
5153
"""
5254
#: The name of this argument, mostly used for display and docs
5355
self.name = name
@@ -147,6 +149,7 @@ def _parse_request_model(
147149
:returns: The flattened request model, as a list
148150
:rtype: list[OpenAPIRequestArg]
149151
"""
152+
150153
args = []
151154

152155
properties, required = _aggregate_schema_properties(schema)

linodecli/baked/util.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,39 @@ def _aggregate_schema_properties(
2424
properties = {}
2525
required = defaultdict(lambda: 0)
2626

27-
def _handle_schema(_schema: Schema):
28-
if _schema.properties is None:
27+
def __inner(
28+
path: List[str],
29+
entry: Schema,
30+
):
31+
if isinstance(entry, dict):
32+
# TODO: Figure out why this happens (openapi3 package bug?)
33+
# pylint: disable=protected-access
34+
entry = Schema(path, entry, schema._root)
35+
36+
if entry.properties is None:
37+
# If there aren't any properties, this might be a
38+
# composite schema
39+
for composition_field in ["oneOf", "allOf", "anyOf"]:
40+
for i, nested_entry in enumerate(
41+
getattr(entry, composition_field) or []
42+
):
43+
__inner(
44+
schema.path + [composition_field, str(i)],
45+
nested_entry,
46+
)
47+
2948
return
3049

50+
# This is a valid option
51+
properties.update(entry.properties)
52+
3153
nonlocal schema_count
3254
schema_count += 1
3355

34-
properties.update(dict(_schema.properties))
35-
36-
# Aggregate required keys and their number of usages.
37-
if _schema.required is not None:
38-
for key in _schema.required:
39-
required[key] += 1
40-
41-
_handle_schema(schema)
42-
43-
one_of = schema.oneOf or []
44-
any_of = schema.anyOf or []
56+
for key in entry.required or []:
57+
required[key] += 1
4558

46-
for entry in one_of + any_of:
47-
# pylint: disable=protected-access
48-
_handle_schema(Schema(schema.path, entry, schema._root))
59+
__inner(schema.path, schema)
4960

5061
return (
5162
properties,

linodecli/help_pages.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -298,50 +298,58 @@ def _help_group_arguments(
298298
"""
299299
Returns help page groupings for a list of POST/PUT arguments.
300300
"""
301+
args = [arg for arg in args if not arg.read_only]
301302
args_sorted = sorted(args, key=lambda a: a.path)
302303

303-
groups_tmp = defaultdict(list)
304+
paths = {tuple(arg.path.split(".")) for arg in args_sorted}
305+
path_to_args = defaultdict(list)
304306

305-
# Initial grouping by root parent
306307
for arg in args_sorted:
307-
if arg.read_only:
308-
continue
308+
arg_path = tuple(arg.path.split("."))
309+
310+
if not arg.is_parent:
311+
# Parent arguments are grouped in with their children
312+
arg_path = arg_path[:-1]
313+
314+
# Find first common parent
315+
while len(arg_path) > 1 and arg_path not in paths:
316+
arg_path = arg_path[:-1]
309317

310-
groups_tmp[arg.path.split(".", 1)[0]].append(arg)
318+
path_to_args[arg_path].append(arg)
311319

312320
group_required = []
313321
groups = []
314322
ungrouped = []
315323

316-
for group in groups_tmp.values():
317-
# If the group has more than one element,
318-
# leave it as is in the result
319-
if len(group) > 1:
324+
for k, group in sorted(
325+
path_to_args.items(), key=lambda a: (len(a[0]), a[0], len(a[1]))
326+
):
327+
if len(k) > 0 and len(group) > 1:
328+
# This is a named subgroup
320329
groups.append(
321330
# Args should be ordered by least depth -> required -> path
322331
sorted(group, key=lambda v: (v.depth, not v.required, v.path)),
323332
)
324333
continue
325334

326-
target_arg = group[0]
327-
328335
# If the group's argument is required,
329-
# add it to the required group
330-
if target_arg.required:
331-
group_required.append(target_arg)
332-
continue
336+
# add it to the top-level required group
337+
for arg in group:
338+
if arg.required:
339+
group_required.append(arg)
340+
continue
333341

334-
# Add ungrouped arguments (single value groups) to the
335-
# "ungrouped" group.
336-
ungrouped.append(target_arg)
342+
# Add ungrouped arguments (single value groups) to the
343+
# "ungrouped" group.
344+
ungrouped.append(arg)
337345

338346
result = []
339347

340348
if len(group_required) > 0:
341-
result.append(group_required)
349+
result.append(sorted(group_required, key=lambda v: v.path))
342350

343351
if len(ungrouped) > 0:
344-
result.append(ungrouped)
352+
result.append(sorted(ungrouped, key=lambda v: v.path))
345353

346354
result += groups
347355

tests/integration/linodes/fixtures.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,60 @@ def linode_with_vpc_interface_as_json(linode_cloud_firewall):
311311
delete_target_id(target="vpcs", id=vpc_id)
312312

313313

314+
@pytest.fixture
315+
def linode_with_vpc_interface_as_args(linode_cloud_firewall):
316+
"""
317+
NOTE: This is fixture exists to accommodate a regression test.
318+
For new tests, use linode_with_vpc_interface_as_json.
319+
"""
320+
321+
vpc_json = create_vpc_w_subnet()
322+
323+
vpc_region = vpc_json["region"]
324+
vpc_id = str(vpc_json["id"])
325+
subnet_id = str(vpc_json["subnets"][0]["id"])
326+
327+
linode_json = json.loads(
328+
exec_test_command(
329+
BASE_CMDS["linodes"]
330+
+ [
331+
"create",
332+
"--type",
333+
"g6-nanode-1",
334+
"--region",
335+
vpc_region,
336+
"--image",
337+
DEFAULT_TEST_IMAGE,
338+
"--root_pass",
339+
DEFAULT_RANDOM_PASS,
340+
"--firewall_id",
341+
linode_cloud_firewall,
342+
"--interfaces.purpose",
343+
"vpc",
344+
"--interfaces.primary",
345+
"true",
346+
"--interfaces.subnet_id",
347+
subnet_id,
348+
"--interfaces.ipv4.nat_1_1",
349+
"any",
350+
"--interfaces.ipv4.vpc",
351+
"10.0.0.5",
352+
"--interfaces.ip_ranges",
353+
json.dumps(["10.0.0.6/32"]),
354+
"--interfaces.purpose",
355+
"public",
356+
"--json",
357+
"--suppress-warnings",
358+
]
359+
)
360+
)[0]
361+
362+
yield linode_json, vpc_json
363+
364+
delete_target_id(target="linodes", id=str(linode_json["id"]))
365+
delete_target_id(target="vpcs", id=vpc_id)
366+
367+
314368
# Interfaces new
315369
@pytest.fixture(scope="module")
316370
def linode_interface_public(linode_cloud_firewall):

tests/integration/linodes/test_interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
exec_test_command,
77
)
88
from tests.integration.linodes.fixtures import ( # noqa: F401
9+
linode_with_vpc_interface_as_args,
910
linode_with_vpc_interface_as_json,
1011
)
1112

@@ -40,5 +41,9 @@ def assert_interface_configuration(
4041
assert public_interface["purpose"] == "public"
4142

4243

44+
def test_with_vpc_interface_as_args(linode_with_vpc_interface_as_args):
45+
assert_interface_configuration(*linode_with_vpc_interface_as_args)
46+
47+
4348
def test_with_vpc_interface_as_json(linode_with_vpc_interface_as_json):
4449
assert_interface_configuration(*linode_with_vpc_interface_as_json)

0 commit comments

Comments
 (0)