Make a CLI flag for any config option that's a list of scalars (#303).

This commit is contained in:
2025-04-02 23:15:21 -07:00
parent bbf6f27715
commit d2c3ed26a9
4 changed files with 93 additions and 35 deletions

View File

@@ -300,12 +300,12 @@ def make_argument_description(schema, flag_name):
description = schema.get('description')
schema_type = schema.get('type')
example = schema.get('example')
if not description:
return None
pieces = [description] if description else []
if '[0]' in flag_name:
description += ' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).'
pieces.append(
' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).'
)
if example and schema_type in ('array', 'object'):
example_buffer = io.StringIO()
@@ -313,11 +313,9 @@ def make_argument_description(schema, flag_name):
yaml.default_flow_style = True
yaml.dump(example, example_buffer)
description += f' Example value: "{example_buffer.getvalue().strip()}"'
pieces.append(f'Example value: "{example_buffer.getvalue().strip()}"')
description = description.replace('%', '%%')
return description
return ' '.join(pieces).replace('%', '%%')
def add_array_element_arguments(arguments_group, unparsed_arguments, flag_name):
@@ -476,7 +474,8 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
# If this is an "array" type, recurse for each items type child option. Don't return yet so that
# a flag also gets added below for the array itself.
if schema_type == 'array':
properties = borgmatic.config.schema.get_properties(schema.get('items', {}))
items = schema.get('items', {})
properties = borgmatic.config.schema.get_properties(items)
if properties:
for name, child in properties.items():
@@ -486,6 +485,11 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
unparsed_arguments,
names[:-1] + (f'{names[-1]}[0]',) + (name,),
)
# If there aren't any children, then this is an array of scalars. Recurse accordingly.
else:
add_arguments_from_schema(
arguments_group, items, unparsed_arguments, names[:-1] + (f'{names[-1]}[0]',)
)
flag_name = '.'.join(names).replace('_', '-')
@@ -497,8 +501,8 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
metavar = names[-1].upper()
description = make_argument_description(schema, flag_name)
# The ...=str given here is to support specifying an object or an array as a YAML string on the
# command-line.
# The object=str and array=str given here is to support specifying an object or an array as a
# YAML string on the command-line.
argument_type = borgmatic.config.schema.parse_type(schema_type, object=str, array=str)
# As a UX nicety, add separate true and false flags for boolean options.

View File

@@ -36,15 +36,15 @@ def set_values(config, keys, value):
list_key = match.group('list_name')
list_index = int(match.group('index'))
if len(keys) == 1:
config[list_key][list_index] = value
return
if list_key not in config:
config[list_key] = []
try:
if len(keys) == 1:
config[list_key][list_index] = value
return
if list_key not in config:
config[list_key] = []
set_values(config[list_key][list_index], keys[1:], value)
except IndexError:
raise ValueError(f'Argument list index {first_key} is out of range')

View File

@@ -577,19 +577,6 @@ def test_parse_arguments_for_actions_raises_error_when_no_action_is_specified():
module.parse_arguments_for_actions(('config',), action_parsers, global_parser)
def test_make_argument_description_without_description_bails():
assert (
module.make_argument_description(
schema={
'description': None,
'type': 'not yours',
},
flag_name='flag',
)
is None
)
def test_make_argument_description_with_object_adds_example():
buffer = flexmock()
buffer.should_receive('getvalue').and_return('{foo: example}')
@@ -611,6 +598,26 @@ def test_make_argument_description_with_object_adds_example():
)
def test_make_argument_description_without_description_and_with_object_sets_example():
buffer = flexmock()
buffer.should_receive('getvalue').and_return('{foo: example}')
flexmock(module.io).should_receive('StringIO').and_return(buffer)
yaml = flexmock()
yaml.should_receive('dump')
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(yaml)
assert (
module.make_argument_description(
schema={
'type': 'object',
'example': {'foo': 'example'},
},
flag_name='flag',
)
== 'Example value: "{foo: example}"'
)
def test_make_argument_description_with_object_skips_missing_example():
flexmock(module.ruamel.yaml).should_receive('YAML').never()
@@ -647,6 +654,26 @@ def test_make_argument_description_with_array_adds_example():
)
def test_make_argument_description_without_description_and_with_array_sets_example():
buffer = flexmock()
buffer.should_receive('getvalue').and_return('[example]')
flexmock(module.io).should_receive('StringIO').and_return(buffer)
yaml = flexmock()
yaml.should_receive('dump')
flexmock(module.ruamel.yaml).should_receive('YAML').and_return(yaml)
assert (
module.make_argument_description(
schema={
'type': 'array',
'example': ['example'],
},
flag_name='flag',
)
== 'Example value: "[example]"'
)
def test_make_argument_description_with_array_skips_missing_example():
flexmock(module.ruamel.yaml).should_receive('YAML').never()
@@ -672,6 +699,15 @@ def test_make_argument_description_with_array_index_in_flag_name_adds_to_descrip
)
def test_make_argument_description_without_description_and_with_array_index_in_flag_name_sets_description():
assert 'list element' in module.make_argument_description(
schema={
'type': 'something',
},
flag_name='flag[0]',
)
def test_make_argument_description_escapes_percent_character():
assert (
module.make_argument_description(
@@ -1043,10 +1079,21 @@ def test_add_arguments_from_schema_with_propertyless_option_adds_flag():
)
def test_add_arguments_from_schema_with_array_adds_flag():
def test_add_arguments_from_schema_with_array_of_scalars_adds_multiple_flags():
arguments_group = flexmock()
flexmock(module).should_receive('make_argument_description').and_return('help')
flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(str)
flexmock(module.borgmatic.config.schema).should_receive('parse_type').with_args(
'integer', object=str, array=str
).and_return(int)
flexmock(module.borgmatic.config.schema).should_receive('parse_type').with_args(
'array', object=str, array=str
).and_return(str)
arguments_group.should_receive('add_argument').with_args(
'--foo[0]',
type=int,
metavar='FOO[0]',
help='help',
).once()
arguments_group.should_receive('add_argument').with_args(
'--foo',
type=str,
@@ -1072,7 +1119,7 @@ def test_add_arguments_from_schema_with_array_adds_flag():
)
def test_add_arguments_from_schema_with_array_and_nested_object_adds_multiple_flags():
def test_add_arguments_from_schema_with_array_of_objects_adds_multiple_flags():
arguments_group = flexmock()
flexmock(module).should_receive('make_argument_description').and_return('help 1').and_return(
'help 2'

View File

@@ -50,6 +50,13 @@ def test_set_values_with_list_index_key_out_of_range_raises():
module.set_values(config=config, keys=('foo', 'bar[1]', 'baz'), value=5)
def test_set_values_with_final_list_index_key_out_of_range_raises():
config = {'foo': {'bar': [{'option': 'value'}]}}
with pytest.raises(ValueError):
module.set_values(config=config, keys=('foo', 'bar[1]'), value=5)
def test_set_values_with_list_index_key_missing_list_and_out_of_range_raises():
config = {'other': 'value'}