arguments.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import io
  2. import re
  3. import ruamel.yaml
  4. import borgmatic.config.schema
  5. LIST_INDEX_KEY_PATTERN = re.compile(r'^(?P<list_name>[a-zA-z-]+)\[(?P<index>\d+)\]$')
  6. def set_values(config, keys, value):
  7. '''
  8. Given a configuration dict, a sequence of parsed key strings, and a string value, descend into
  9. the configuration hierarchy based on the given keys and set the value into the right place.
  10. For example, consider these keys:
  11. ('foo', 'bar', 'baz')
  12. This looks up "foo" in the given configuration dict. And within that, it looks up "bar". And
  13. then within that, it looks up "baz" and sets it to the given value. Another example:
  14. ('mylist[0]', 'foo')
  15. This looks for the zeroth element of "mylist" in the given configuration. And within that, it
  16. looks up "foo" and sets it to the given value.
  17. '''
  18. if not keys:
  19. return
  20. first_key = keys[0]
  21. # Support "mylist[0]" list index syntax.
  22. match = LIST_INDEX_KEY_PATTERN.match(first_key)
  23. if match:
  24. list_key = match.group('list_name')
  25. list_index = int(match.group('index'))
  26. try:
  27. if len(keys) == 1:
  28. config[list_key][list_index] = value
  29. return
  30. if list_key not in config:
  31. config[list_key] = []
  32. set_values(config[list_key][list_index], keys[1:], value)
  33. except (IndexError, KeyError):
  34. raise ValueError(f'Argument list index {first_key} is out of range')
  35. return
  36. if len(keys) == 1:
  37. config[first_key] = value
  38. return
  39. if first_key not in config:
  40. config[first_key] = {}
  41. set_values(config[first_key], keys[1:], value)
  42. def type_for_option(schema, option_keys):
  43. '''
  44. Given a configuration schema dict and a sequence of keys identifying a potentially nested
  45. option, e.g. ('extra_borg_options', 'create'), return the schema type of that option as a
  46. string.
  47. Return None if the option or its type cannot be found in the schema.
  48. '''
  49. option_schema = schema
  50. for key in option_keys:
  51. # Support "name[0]"-style list index syntax.
  52. match = LIST_INDEX_KEY_PATTERN.match(key)
  53. properties = borgmatic.config.schema.get_properties(option_schema)
  54. try:
  55. if match:
  56. option_schema = properties[match.group('list_name')]['items']
  57. else:
  58. option_schema = properties[key]
  59. except KeyError:
  60. return None
  61. try:
  62. return option_schema['type']
  63. except KeyError:
  64. return None
  65. def convert_value_type(value, option_type):
  66. '''
  67. Given a string value and its schema type as a string, determine its logical type (string,
  68. boolean, integer, etc.), and return it converted to that type.
  69. If the destination option type is a string, then leave the value as-is so that special
  70. characters in it don't get interpreted as YAML during conversion.
  71. And if the source value isn't a string, return it as-is.
  72. Raise ruamel.yaml.error.YAMLError if there's a parse issue with the YAML.
  73. Raise ValueError if the parsed value doesn't match the option type.
  74. '''
  75. if not isinstance(value, str):
  76. return value
  77. if option_type == 'string':
  78. return value
  79. try:
  80. parsed_value = ruamel.yaml.YAML(typ='safe').load(io.StringIO(value))
  81. except ruamel.yaml.error.YAMLError as error:
  82. raise ValueError(f'Argument value "{value}" is invalid: {error.problem}')
  83. if not isinstance(parsed_value, borgmatic.config.schema.parse_type(option_type)):
  84. raise ValueError(f'Argument value "{value}" is not of the expected type: {option_type}')
  85. return parsed_value
  86. def prepare_arguments_for_config(global_arguments, schema):
  87. '''
  88. Given global arguments as an argparse.Namespace and a configuration schema dict, parse each
  89. argument that corresponds to an option in the schema and return a sequence of tuples (keys,
  90. values) for that option, where keys is a sequence of strings. For instance, given the following
  91. arguments:
  92. argparse.Namespace(**{'my_option.sub_option': 'value1', 'other_option': 'value2'})
  93. ... return this:
  94. (
  95. (('my_option', 'sub_option'), 'value1'),
  96. (('other_option',), 'value2'),
  97. )
  98. '''
  99. prepared_values = []
  100. for argument_name, value in global_arguments.__dict__.items():
  101. if value is None:
  102. continue
  103. keys = tuple(argument_name.split('.'))
  104. option_type = type_for_option(schema, keys)
  105. # The argument doesn't correspond to any option in the schema, or it is a complex argument, so ignore it.
  106. # It's probably a flag that borgmatic has on the command-line but not in configuration.
  107. if option_type in {'object', None}:
  108. continue
  109. prepared_values.append(
  110. (
  111. keys,
  112. convert_value_type(value, option_type),
  113. ),
  114. )
  115. return tuple(prepared_values)
  116. def apply_arguments_to_config(config, schema, arguments):
  117. '''
  118. Given a configuration dict, a corresponding configuration schema dict, and arguments as a dict
  119. from action name to argparse.Namespace, set those given argument values into their corresponding
  120. configuration options in the configuration dict.
  121. This supports argument flags of the from "--foo.bar.baz" where each dotted component is a nested
  122. configuration object. Additionally, flags like "--foo.bar[0].baz" are supported to update a list
  123. element in the configuration.
  124. '''
  125. for action_arguments in arguments.values():
  126. for keys, value in prepare_arguments_for_config(action_arguments, schema):
  127. set_values(config, keys, value)