From f222bf2c1a5fb6d8a54d73672503259599d29b32 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Sat, 22 Mar 2025 22:52:23 -0700 Subject: [PATCH] Organizational refactoring (#303). --- borgmatic/commands/arguments.py | 152 ++++++++++++++++++-------------- borgmatic/config/arguments.py | 8 +- borgmatic/config/schema.py | 7 +- 3 files changed, 98 insertions(+), 69 deletions(-) diff --git a/borgmatic/commands/arguments.py b/borgmatic/commands/arguments.py index f00f49bb..32249f8f 100644 --- a/borgmatic/commands/arguments.py +++ b/borgmatic/commands/arguments.py @@ -291,6 +291,91 @@ def parse_arguments_for_actions(unparsed_arguments, action_parsers, global_parse OMITTED_FLAG_NAMES = {'match_archives', 'progress', 'stats', 'list'} +def make_argument_description(schema, flag_name): + ''' + Given a configuration schema dict and a flag name for it, extend the schema's description with + an example or additional information as appropriate based on its type. Return the updated + description for use in a command-line argument. + ''' + description = schema.get('description') + schema_type = schema.get('type') + + if not description: + return None + + if schema_type == 'array': + example_buffer = io.StringIO() + yaml = ruamel.yaml.YAML(typ='safe') + yaml.default_flow_style = True + yaml.dump(schema.get('example'), example_buffer) + + description += f' Example value: "{example_buffer.getvalue().strip()}"' + + if '[0]' in flag_name: + description += ' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).' + + description = description.replace('%', '%%') + + +def add_array_element_arguments_from_schema(arguments_group, schema, unparsed_arguments, flag_name): + ''' + Given an argparse._ArgumentGroup instance, a configuration schema dict, a sequence of unparsed + argument strings, and a dotted flag name, convert the schema into corresponding command-line + array element flags that correspond to the given unparsed arguments. + + Here's the background. We want to support flags that can have arbitrary indices like: + + --foo.bar[1].baz + + But argparse doesn't support that natively because the index can be an arbitrary number. We + won't let that stop us though, will we? + + If the current flag name has an array component in it (e.g. a name with "[0]"), then make a + pattern that would match the flag name regardless of the number that's in it. The idea is that + we want to look for unparsed arguments that appear like the flag name, but instead of "[0]" they + have, say, "[1]" or "[123]". + + Next, we check each unparsed argument against that pattern. If one of them matches, add an + argument flag for it to the argument parser group. Example: + + Let's say flag_name is: + + --foo.bar[0].baz + + ... then the regular expression pattern will be: + + ^--foo\.bar\[\d+\]\.baz + + ... and, if that matches an unparsed argument of: + + --foo.bar[1].baz + + ... then an argument flag will get added equal to that unparsed argument. And the unparsed + argument will match it when parsing is performed! In this manner, we're using the actual user + CLI input to inform what exact flags we support! + ''' + if '[0]' not in flag_name or '--help' in unparsed_arguments: + return + + pattern = re.compile(f'^--{flag_name.replace("[0]", r"\[\d+\]").replace(".", r"\.")}$') + existing_flags = set( + itertools.chain( + *(group_action.option_strings for group_action in arguments_group._group_actions) + ) + ) + + for unparsed in unparsed_arguments: + unparsed_flag_name = unparsed.split('=', 1)[0] + + if pattern.match(unparsed_flag_name) and unparsed_flag_name not in existing_flags: + arguments_group.add_argument( + unparsed_flag_name, + type=argument_type, + metavar=metavar, + help=description, + ) + + def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names=None): ''' Given an argparse._ArgumentGroup instance, a configuration schema dict, and a sequence of @@ -362,28 +447,14 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names ) flag_name = '.'.join(names) - description = schema.get('description') metavar = names[-1].upper() - if description: - if schema_type == 'array': - example_buffer = io.StringIO() - yaml = ruamel.yaml.YAML(typ='safe') - yaml.default_flow_style = True - yaml.dump(schema.get('example'), example_buffer) - - description += f' Example value: "{example_buffer.getvalue().strip()}"' - - if '[0]' in flag_name: - description += ' To specify a different list element, replace the "[0]" with another array index ("[1]", "[2]", etc.).' - - description = description.replace('%', '%%') - - # These options already have corresponding flags on individual actions (like "create + # Certain options already have corresponding flags on individual actions (like "create # --progress"), so don't bother adding them to the global flags. if flag_name in OMITTED_FLAG_NAMES: return + description = make_argument_description(schema, flag_name) argument_type = borgmatic.config.schema.parse_type(schema_type) full_flag_name = f"--{flag_name.replace('_', '-')}" @@ -404,54 +475,7 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names help=description, ) - # We want to support flags that can have arbitrary indices like: - # - # --foo.bar[1].baz - # - # But argparse doesn't support that natively because the index can be an arbitrary number. We - # won't let that stop us though, will we? So, if the current flag name has an array component in - # it (e.g. a name with "[0]"), then make a pattern that would match the flag name regardless of - # the number that's in it. The idea is that we want to look for unparsed arguments that appear - # like the flag name, but instead of "[0]" they have, say, "[1]" or "[123]". - # - # Next, we check each unparsed argument against that pattern. If one of them matches, add an - # argument flag for it to the argument parser group. Example: - # - # Let's say flag_name is: - # - # --foo.bar[0].baz - # - # ... then the regular expression pattern will be: - # - # ^--foo\.bar\[\d+\]\.baz - # - # ... and, if that matches an unparsed argument of: - # - # --foo.bar[1].baz - # - # ... then an argument flag will get added equal to that unparsed argument. And the unparsed - # argument will match it when parsing is performed! In this manner, we're using the actual user - # CLI input to inform what exact flags we support! - if '[0]' not in flag_name or '--help' in unparsed_arguments: - return - - pattern = re.compile(f'^--{flag_name.replace("[0]", r"\[\d+\]").replace(".", r"\.")}$') - existing_flags = set( - itertools.chain( - *(group_action.option_strings for group_action in arguments_group._group_actions) - ) - ) - - for unparsed in unparsed_arguments: - unparsed_flag_name = unparsed.split('=', 1)[0] - - if pattern.match(unparsed_flag_name) and unparsed_flag_name not in existing_flags: - arguments_group.add_argument( - unparsed_flag_name, - type=argument_type, - metavar=metavar, - help=description, - ) + add_array_element_arguments_from_schema(arguments_group, schema, unparsed_arguments, flag_name) def make_parsers(schema, unparsed_arguments): diff --git a/borgmatic/config/arguments.py b/borgmatic/config/arguments.py index 9aa83998..8f7023cd 100644 --- a/borgmatic/config/arguments.py +++ b/borgmatic/config/arguments.py @@ -14,13 +14,13 @@ def set_values(config, keys, value): ('foo', 'bar', 'baz') - This looks up "foo" in the given configuration. And within that value, it looks up "bar". And - then within that value, it looks up "baz" and sets it to the given value. Another example: + This looks up "foo" in the given configuration dict. And within that, it looks up "bar". And + then within that, it looks up "baz" and sets it to the given value. Another example: ('mylist[0]', 'foo') - This looks for the zeroth element of "mylist" in the given configuration. And within that value, - it looks up "foo" and sets it to the given value. + This looks for the zeroth element of "mylist" in the given configuration. And within that, it + looks up "foo" and sets it to the given value. ''' if not keys: return diff --git a/borgmatic/config/schema.py b/borgmatic/config/schema.py index 118e9437..4442010f 100644 --- a/borgmatic/config/schema.py +++ b/borgmatic/config/schema.py @@ -5,7 +5,7 @@ import itertools def get_properties(schema): ''' Given a schema dict, return its properties. But if it's got sub-schemas with multiple different - potential properties, returned their merged properties instead (interleaved so the first + potential properties, return their merged properties instead (interleaved so the first properties of each sub-schema come first). The idea is that the user should see all possible options even if they're not all possible together. ''' @@ -24,6 +24,11 @@ def get_properties(schema): def parse_type(schema_type): + ''' + Given a schema type as a string, return the corresponding Python type. + + Raise ValueError if the schema type is unknown. + ''' try: return { 'string': str,