Browse Source

Expose propertyless YAML objects from configuration (e.g. "constants") as command-line flags (#303).

Dan Helfman 2 months ago
parent
commit
affe7cdc1b

+ 6 - 4
borgmatic/commands/arguments.py

@@ -307,7 +307,7 @@ def make_argument_description(schema, flag_name):
     if '[0]' in flag_name:
         description += ' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).'
 
-    if example and schema_type == 'array':
+    if example and schema_type in ('array', 'object'):
         example_buffer = io.StringIO()
         yaml = ruamel.yaml.YAML(typ='safe')
         yaml.default_flow_style = True
@@ -453,13 +453,15 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
     if schema_type == 'object':
         properties = schema.get('properties')
 
+        # If there are child properties, recurse for each one. But if there are no child properties,
+        # fall through so that a flag gets added below for the (empty) object.
         if properties:
             for name, child in properties.items():
                 add_arguments_from_schema(
                     arguments_group, child, unparsed_arguments, names + (name,)
                 )
 
-        return
+            return
 
     # 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.
@@ -485,9 +487,9 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
     metavar = names[-1].upper()
     description = make_argument_description(schema, flag_name)
 
-    # array=str instead of list here to support specifying a list as a YAML string on the
+    # The ...=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, array=str)
+    argument_type = borgmatic.config.schema.parse_type(schema_type, object=str, array=str)
     full_flag_name = f"--{flag_name.replace('_', '-')}"
 
     # As a UX nicety, allow boolean options that have a default of false to have command-line flags

+ 16 - 0
tests/integration/commands/test_arguments.py

@@ -4,6 +4,22 @@ from flexmock import flexmock
 from borgmatic.commands import arguments as module
 
 
+def test_make_argument_description_with_object_adds_example():
+    assert (
+        module.make_argument_description(
+            schema={
+                'description': 'Thing.',
+                'type': 'object',
+                'example': {'bar': 'baz'},
+            },
+            flag_name='flag',
+        )
+        # Apparently different versions of ruamel.yaml serialize this
+        # differently.
+        in ('Thing. Example value: "bar: baz"' 'Thing. Example value: "{bar: baz}"')
+    )
+
+
 def test_make_argument_description_with_array_adds_example():
     assert (
         module.make_argument_description(

+ 47 - 8
tests/unit/commands/test_arguments.py

@@ -590,6 +590,42 @@ 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': 'Thing.',
+                'type': 'object',
+                'example': {'foo': 'example'},
+            },
+            flag_name='flag',
+        )
+        == 'Thing. Example value: "{foo: example}"'
+    )
+
+
+def test_make_argument_description_with_object_skips_missing_example():
+    flexmock(module.ruamel.yaml).should_receive('YAML').never()
+
+    assert (
+        module.make_argument_description(
+            schema={
+                'description': 'Thing.',
+                'type': 'object',
+            },
+            flag_name='flag',
+        )
+        == 'Thing.'
+    )
+
+
 def test_make_argument_description_with_array_adds_example():
     buffer = flexmock()
     buffer.should_receive('getvalue').and_return('[example]')
@@ -612,9 +648,7 @@ def test_make_argument_description_with_array_adds_example():
 
 
 def test_make_argument_description_with_array_skips_missing_example():
-    yaml = flexmock()
-    yaml.should_receive('dump').and_return('[example]')
-    flexmock(module.ruamel.yaml).should_receive('YAML').and_return(yaml)
+    flexmock(module.ruamel.yaml).should_receive('YAML').never()
 
     assert (
         module.make_argument_description(
@@ -951,12 +985,17 @@ def test_add_arguments_from_schema_with_empty_multi_type_raises():
         )
 
 
-def test_add_arguments_from_schema_with_propertyless_option_does_not_add_flag():
+def test_add_arguments_from_schema_with_propertyless_option_adds_flag():
     arguments_group = flexmock()
-    flexmock(module).should_receive('make_argument_description').never()
-    flexmock(module.borgmatic.config.schema).should_receive('parse_type').never()
-    arguments_group.should_receive('add_argument').never()
-    flexmock(module).should_receive('add_array_element_arguments').never()
+    flexmock(module).should_receive('make_argument_description').and_return('help')
+    flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(str)
+    arguments_group.should_receive('add_argument').with_args(
+        '--foo',
+        type=str,
+        metavar='FOO',
+        help='help',
+    ).once()
+    flexmock(module).should_receive('add_array_element_arguments')
 
     module.add_arguments_from_schema(
         arguments_group=arguments_group,