Organizational refactoring (#303).

This commit is contained in:
Dan Helfman
2025-03-22 22:52:23 -07:00
parent dc9da3832d
commit f222bf2c1a
3 changed files with 98 additions and 69 deletions
+88 -64
View File
@@ -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):
+4 -4
View File
@@ -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
+6 -1
View File
@@ -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,