瀏覽代碼

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

Dan Helfman 2 月之前
父節點
當前提交
d2c3ed26a9

+ 15 - 11
borgmatic/commands/arguments.py

@@ -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()}"'
-
-    description = description.replace('%', '%%')
+        pieces.append(f'Example value: "{example_buffer.getvalue().strip()}"')
 
-    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.

+ 6 - 6
borgmatic/config/arguments.py

@@ -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
+        try:
+            if len(keys) == 1:
+                config[list_key][list_index] = value
 
-            return
+                return
 
-        if list_key not in config:
-            config[list_key] = []
+            if list_key not in config:
+                config[list_key] = []
 
-        try:
             set_values(config[list_key][list_index], keys[1:], value)
         except IndexError:
             raise ValueError(f'Argument list index {first_key} is out of range')

+ 57 - 10
tests/unit/commands/test_arguments.py

@@ -577,20 +577,28 @@ 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():
+def test_make_argument_description_with_object_adds_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={
-                'description': None,
-                'type': 'not yours',
+                'description': 'Thing.',
+                'type': 'object',
+                'example': {'foo': 'example'},
             },
             flag_name='flag',
         )
-        is None
+        == 'Thing. Example value: "{foo: example}"'
     )
 
 
-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)
@@ -601,13 +609,12 @@ def test_make_argument_description_with_object_adds_example():
     assert (
         module.make_argument_description(
             schema={
-                'description': 'Thing.',
                 'type': 'object',
                 'example': {'foo': 'example'},
             },
             flag_name='flag',
         )
-        == 'Thing. Example value: "{foo: example}"'
+        == 'Example value: "{foo: example}"'
     )
 
 
@@ -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'

+ 7 - 0
tests/unit/config/test_arguments.py

@@ -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'}