Skip to content

Fix support for deeply nested oneOfs; group nested oneOf options on help pages #785

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ test-unit:
@mkdir -p /tmp/linode/.config
@orig_xdg_config_home=$${XDG_CONFIG_HOME:-}; \
export LINODE_CLI_TEST_MODE=1 XDG_CONFIG_HOME=/tmp/linode/.config; \
pytest -v tests/unit; \
pytest -vv tests/unit; \
exit_code=$$?; \
export XDG_CONFIG_HOME=$$orig_xdg_config_home; \
exit $$exit_code
Expand Down
3 changes: 3 additions & 0 deletions linodecli/baked/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def __init__( # pylint: disable=too-many-arguments
:type parent: Optional[str]
:param depth: The depth of this argument, or how many parent arguments this argument has.
:type depth: int
:param option_variants: A mapping of options, defined using oneOf in the to spec,
to a variant of this argument.
"""
#: The name of this argument, mostly used for display and docs
self.name = name
Expand Down Expand Up @@ -144,6 +146,7 @@ def _parse_request_model(
:returns: The flattened request model, as a list
:rtype: list[OpenAPIRequestArg]
"""

args = []

properties, required = _aggregate_schema_properties(schema)
Expand Down
45 changes: 28 additions & 17 deletions linodecli/baked/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from collections import defaultdict
from typing import Any, Dict, Set, Tuple
from typing import Any, Dict, List, Set, Tuple

from openapi3.schemas import Schema

Expand All @@ -23,28 +23,39 @@ def _aggregate_schema_properties(
properties = {}
required = defaultdict(lambda: 0)

def _handle_schema(_schema: Schema):
if _schema.properties is None:
return
def __inner(
path: List[str],
entry: Schema,
):
if isinstance(entry, dict):
# TODO: Figure out why this happens (openapi3 package bug?)
# pylint: disable=protected-access
entry = Schema(path, entry, schema._root)

nonlocal schema_count
schema_count += 1
if entry.properties is None:
# If there aren't any properties, this might be a
# composite schema
for composition_field in ["oneOf", "allOf", "anyOf"]:
for i, nested_entry in enumerate(
getattr(entry, composition_field) or []
):
__inner(
schema.path + [composition_field, str(i)],
nested_entry,
)

properties.update(dict(_schema.properties))
return

# Aggregate required keys and their number of usages.
if _schema.required is not None:
for key in _schema.required:
required[key] += 1
# This is a valid option
properties.update(entry.properties)

_handle_schema(schema)
nonlocal schema_count
schema_count += 1

one_of = schema.oneOf or []
any_of = schema.anyOf or []
for key in entry.required or []:
required[key] += 1

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

return (
properties,
Expand Down
48 changes: 28 additions & 20 deletions linodecli/help_pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,50 +298,58 @@ def _help_group_arguments(
"""
Returns help page groupings for a list of POST/PUT arguments.
"""
args = [arg for arg in args if not arg.read_only]
args_sorted = sorted(args, key=lambda a: a.path)

groups_tmp = defaultdict(list)
paths = {tuple(arg.path.split(".")) for arg in args_sorted}
path_to_args = defaultdict(list)

# Initial grouping by root parent
for arg in args_sorted:
if arg.read_only:
continue
arg_path = tuple(arg.path.split("."))

if not arg.is_parent:
# Parent arguments are grouped in with their children
arg_path = arg_path[:-1]

# Find first common parent
while len(arg_path) > 1 and arg_path not in paths:
arg_path = arg_path[:-1]

groups_tmp[arg.path.split(".", 1)[0]].append(arg)
path_to_args[arg_path].append(arg)

group_required = []
groups = []
ungrouped = []

for group in groups_tmp.values():
# If the group has more than one element,
# leave it as is in the result
if len(group) > 1:
for k, group in sorted(
path_to_args.items(), key=lambda a: (len(a[0]), a[0], len(a[1]))
):
if len(k) > 0 and len(group) > 1:
# This is a named subgroup
groups.append(
# Args should be ordered by least depth -> required -> path
sorted(group, key=lambda v: (v.depth, not v.required, v.path)),
)
continue

target_arg = group[0]

# If the group's argument is required,
# add it to the required group
if target_arg.required:
group_required.append(target_arg)
continue
# add it to the top-level required group
for arg in group:
if arg.required:
group_required.append(arg)
continue

# Add ungrouped arguments (single value groups) to the
# "ungrouped" group.
ungrouped.append(target_arg)
# Add ungrouped arguments (single value groups) to the
# "ungrouped" group.
ungrouped.append(arg)

result = []

if len(group_required) > 0:
result.append(group_required)
result.append(sorted(group_required, key=lambda v: v.path))

if len(ungrouped) > 0:
result.append(ungrouped)
result.append(sorted(ungrouped, key=lambda v: v.path))

result += groups

Expand Down
60 changes: 60 additions & 0 deletions tests/integration/linodes/test_interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,62 @@
linode_label = DEFAULT_LABEL + timestamp


@pytest.fixture
def linode_with_vpc_interface_as_args(linode_cloud_firewall):
"""
NOTE: This is fixture exists to accommodate a regression test.
For new tests, use linode_with_vpc_interface_as_json.
"""

vpc_json = create_vpc_w_subnet()

vpc_region = vpc_json["region"]
vpc_id = str(vpc_json["id"])
subnet_id = str(vpc_json["subnets"][0]["id"])

linode_json = json.loads(
exec_test_command(
BASE_CMD
+ [
"create",
"--type",
"g6-nanode-1",
"--region",
vpc_region,
"--image",
DEFAULT_TEST_IMAGE,
"--root_pass",
DEFAULT_RANDOM_PASS,
"--firewall_id",
linode_cloud_firewall,
"--interfaces.purpose",
"vpc",
"--interfaces.primary",
"true",
"--interfaces.subnet_id",
subnet_id,
"--interfaces.ipv4.nat_1_1",
"any",
"--interfaces.ipv4.vpc",
"10.0.0.5",
"--interfaces.ip_ranges",
json.dumps(["10.0.0.6/32"]),
"--interfaces.purpose",
"public",
"--json",
"--suppress-warnings",
]
)
.stdout.decode()
.rstrip()
)[0]

yield linode_json, vpc_json

delete_target_id(target="linodes", id=str(linode_json["id"]))
delete_target_id(target="vpcs", id=vpc_id)


@pytest.fixture
def linode_with_vpc_interface_as_json(linode_cloud_firewall):
vpc_json = create_vpc_w_subnet()
Expand Down Expand Up @@ -99,5 +155,9 @@ def assert_interface_configuration(
assert public_interface["purpose"] == "public"


def test_with_vpc_interface_as_args(linode_with_vpc_interface_as_args):
assert_interface_configuration(*linode_with_vpc_interface_as_args)


def test_with_vpc_interface_as_json(linode_with_vpc_interface_as_json):
assert_interface_configuration(*linode_with_vpc_interface_as_json)
Loading