For boolean configuration options, add separate "--foo" and "--no-foo" CLI flags (#303).

This commit is contained in:
Dan Helfman
2025-04-02 17:08:04 -07:00
parent 9301ab13cc
commit bbf6f27715
6 changed files with 86 additions and 117 deletions
+12 -9
View File
@@ -500,20 +500,26 @@ def add_arguments_from_schema(arguments_group, schema, unparsed_arguments, names
# 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, object=str, array=str)
full_flag_name = f"--{flag_name}"
# As a UX nicety, allow boolean options that have a default of false to have command-line flags
# without values.
if schema_type == 'boolean' and schema.get('default') is False:
# As a UX nicety, add separate true and false flags for boolean options.
if schema_type == 'boolean':
arguments_group.add_argument(
full_flag_name,
f'--{flag_name}',
action='store_true',
default=None,
help=description,
)
no_flag_name = '.'.join(names[:-1] + ('no-' + names[-1],)).replace('_', '-')
arguments_group.add_argument(
f'--{no_flag_name}',
dest=flag_name.replace('-', '_'),
action='store_false',
default=None,
help=f'Set the --{flag_name} value to false.',
)
else:
arguments_group.add_argument(
full_flag_name,
f'--{flag_name}',
type=argument_type,
metavar=metavar,
help=description,
@@ -553,9 +559,6 @@ def make_parsers(schema, unparsed_arguments):
action='store_true',
help='Go through the motions, but do not actually write to any repositories',
)
global_group.add_argument(
'-nc', '--no-color', dest='no_color', action='store_true', help='Disable colored output'
)
global_group.add_argument(
'-v',
'--verbosity',
+1 -1
View File
@@ -1025,7 +1025,7 @@ def main(extra_summary_logs=[]): # pragma: no cover
any_json_flags = any(
getattr(sub_arguments, 'json', False) for sub_arguments in arguments.values()
)
color_enabled = should_do_markup(global_arguments.no_color or any_json_flags, configs)
color_enabled = should_do_markup(configs, any_json_flags)
try:
configure_logging(
+3 -33
View File
@@ -59,7 +59,6 @@ properties:
description: |
Whether the repository should be created append-only,
only used for the repo-create action. Defaults to false.
default: false
example: true
storage_quota:
type: string
@@ -74,7 +73,6 @@ properties:
Whether any missing parent directories of the repository
path should be created, only used for the repo-create
action. Defaults to false.
default: false
example: true
description: |
A required list of local or remote repositories with paths and
@@ -104,14 +102,12 @@ properties:
description: |
Stay in same file system; do not cross mount points beyond the given
source directories. Defaults to false.
default: false
example: true
numeric_ids:
type: boolean
description: |
Only store/extract numeric user and group identifiers. Defaults to
false.
default: false
example: true
atime:
type: boolean
@@ -122,13 +118,11 @@ properties:
ctime:
type: boolean
description: Store ctime into archive. Defaults to true.
default: true
example: false
birthtime:
type: boolean
description: |
Store birthtime (creation date) into archive. Defaults to true.
default: true
example: false
read_special:
type: boolean
@@ -138,14 +132,12 @@ properties:
used when backing up special devices such as /dev/zero. Defaults to
false. But when a database hook is used, the setting here is ignored
and read_special is considered true.
default: false
example: true
flags:
type: boolean
description: |
Record filesystem flags (e.g. NODUMP, IMMUTABLE) in archive.
Defaults to true.
default: true
example: false
files_cache:
type: string
@@ -219,7 +211,6 @@ properties:
Exclude directories that contain a CACHEDIR.TAG file. See
http://www.brynosaurus.com/cachedir/spec.html for details. Defaults
to false.
default: false
example: true
exclude_if_present:
type: array
@@ -236,13 +227,11 @@ properties:
If true, the exclude_if_present filename is included in backups.
Defaults to false, meaning that the exclude_if_present filename is
omitted from backups.
default: false
example: true
exclude_nodump:
type: boolean
description: |
Exclude files with the NODUMP flag. Defaults to false.
default: false
example: true
borgmatic_source_directory:
type: string
@@ -274,7 +263,6 @@ properties:
description: |
If true, then source directories (and root pattern paths) must
exist. If they don't, an error is raised. Defaults to false.
default: false
example: true
encryption_passcommand:
type: string
@@ -488,21 +476,18 @@ properties:
description: |
Bypass Borg error about a repository that has been moved. Defaults
to false.
default: false
example: true
unknown_unencrypted_repo_access_is_ok:
type: boolean
description: |
Bypass Borg error about a previously unknown unencrypted repository.
Defaults to false.
default: false
example: true
check_i_know_what_i_am_doing:
type: boolean
description: |
Bypass Borg confirmation about check with repair option. Defaults to
false and an interactive prompt from Borg.
default: false
example: true
extra_borg_options:
type: object
@@ -828,9 +813,7 @@ properties:
color:
type: boolean
description: |
Apply color to console output. Can be overridden with --no-color
command-line flag. Defaults to true.
default: true
Apply color to console output. Defaults to true.
example: false
progress:
type: boolean
@@ -838,7 +821,6 @@ properties:
Display progress as each file or archive is processed when running
supported actions. Corresponds to the "--progress" flag on those
actions. Defaults to false.
default: false
example: true
statistics:
type: boolean
@@ -846,7 +828,6 @@ properties:
Display statistics for an archive when running supported actions.
Corresponds to the "--stats" flag on those actions. Defaults to
false.
default: false
example: true
list_details:
type: boolean
@@ -854,7 +835,6 @@ properties:
Display details for each file or archive as it is processed when
running supported actions. Corresponds to the "--list" flag on those
actions. Defaults to false.
default: false
example: true
skip_actions:
type: array
@@ -1197,7 +1177,6 @@ properties:
backup itself. Defaults to true. Changing this to false
prevents "borgmatic bootstrap" from extracting configuration
files from the backup.
default: true
example: false
description: |
Support for the "borgmatic bootstrap" action, used to extract
@@ -1282,7 +1261,6 @@ properties:
schema elements. These statements will fail unless the
initial connection to the database is made by a
superuser.
default: false
example: true
format:
type: string
@@ -1521,7 +1499,6 @@ properties:
Use the "--add-drop-database" flag with mariadb-dump,
causing the database to be dropped right before restore.
Defaults to true.
default: true
example: false
options:
type: string
@@ -1669,7 +1646,6 @@ properties:
Use the "--add-drop-database" flag with mysqldump,
causing the database to be dropped right before restore.
Defaults to true.
default: true
example: false
options:
type: string
@@ -2397,9 +2373,8 @@ properties:
send_logs:
type: boolean
description: |
Send borgmatic logs to Apprise services as part the
Send borgmatic logs to Apprise services as part of the
"finish", "fail", and "log" states. Defaults to true.
default: true
example: false
logs_size_limit:
type: integer
@@ -2509,14 +2484,12 @@ properties:
description: |
Verify the TLS certificate of the ping URL host. Defaults to
true.
default: true
example: false
send_logs:
type: boolean
description: |
Send borgmatic logs to Healthchecks as part the "finish",
Send borgmatic logs to Healthchecks as part of the "finish",
"fail", and "log" states. Defaults to true.
default: true
example: false
ping_body_limit:
type: integer
@@ -2549,7 +2522,6 @@ properties:
the slug URL scheme (https://hc-ping.com/<ping-key>/<slug>
as opposed to https://hc-ping.com/<uuid>).
Defaults to false.
default: false
example: true
description: |
Configuration for a monitoring integration with Healthchecks. Create
@@ -2589,7 +2561,6 @@ properties:
description: |
Verify the TLS certificate of the push URL host. Defaults to
true.
default: true
example: false
description: |
Configuration for a monitoring integration with Uptime Kuma using
@@ -2626,7 +2597,6 @@ properties:
description: |
Send borgmatic logs to PagerDuty when a backup errors.
Defaults to true.
default: true
example: false
description: |
Configuration for a monitoring integration with PagerDuty. Create an
+5 -4
View File
@@ -29,12 +29,13 @@ def interactive_console():
return sys.stderr.isatty() and os.environ.get('TERM') != 'dumb'
def should_do_markup(no_color, configs):
def should_do_markup(configs, json_enabled):
'''
Given the value of the command-line no-color argument, and a dict of configuration filename to
corresponding parsed configuration, determine if we should enable color marking up.
Given a dict of configuration filename to corresponding parsed configuration (which already have
any command-line overrides applied) and whether json is enabled, determine if we should enable
color marking up.
'''
if no_color:
if json_enabled:
return False
if any(config.get('color', True) is False for config in configs.values()):
+40 -53
View File
@@ -1121,7 +1121,7 @@ def test_add_arguments_from_schema_with_array_and_nested_object_adds_multiple_fl
)
def test_add_arguments_from_schema_with_default_false_boolean_adds_valueless_flag():
def test_add_arguments_from_schema_with_boolean_adds_two_valueless_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(bool)
@@ -1131,59 +1131,12 @@ def test_add_arguments_from_schema_with_default_false_boolean_adds_valueless_fla
default=None,
help='help',
).once()
flexmock(module).should_receive('add_array_element_arguments')
module.add_arguments_from_schema(
arguments_group=arguments_group,
schema={
'type': 'object',
'properties': {
'foo': {
'type': 'boolean',
'default': False,
}
},
},
unparsed_arguments=(),
)
def test_add_arguments_from_schema_with_default_true_boolean_adds_value_flag():
arguments_group = flexmock()
flexmock(module).should_receive('make_argument_description').and_return('help')
flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(bool)
arguments_group.should_receive('add_argument').with_args(
'--foo',
type=bool,
metavar='FOO',
help='help',
).once()
flexmock(module).should_receive('add_array_element_arguments')
module.add_arguments_from_schema(
arguments_group=arguments_group,
schema={
'type': 'object',
'properties': {
'foo': {
'type': 'boolean',
'default': True,
}
},
},
unparsed_arguments=(),
)
def test_add_arguments_from_schema_with_defaultless_boolean_adds_value_flag():
arguments_group = flexmock()
flexmock(module).should_receive('make_argument_description').and_return('help')
flexmock(module.borgmatic.config.schema).should_receive('parse_type').and_return(bool)
arguments_group.should_receive('add_argument').with_args(
'--foo',
type=bool,
metavar='FOO',
help='help',
'--no-foo',
dest='foo',
action='store_false',
default=None,
help=object,
).once()
flexmock(module).should_receive('add_array_element_arguments')
@@ -1201,6 +1154,40 @@ def test_add_arguments_from_schema_with_defaultless_boolean_adds_value_flag():
)
def test_add_arguments_from_schema_with_nested_boolean_adds_two_valueless_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(bool)
arguments_group.should_receive('add_argument').with_args(
'--foo.bar.baz-quux',
action='store_true',
default=None,
help='help',
).once()
arguments_group.should_receive('add_argument').with_args(
'--foo.bar.no-baz-quux',
dest='foo.bar.baz_quux',
action='store_false',
default=None,
help=object,
).once()
flexmock(module).should_receive('add_array_element_arguments')
module.add_arguments_from_schema(
arguments_group=arguments_group,
schema={
'type': 'object',
'properties': {
'baz_quux': {
'type': 'boolean',
}
},
},
unparsed_arguments=(),
names=('foo', 'bar'),
)
def test_add_arguments_from_schema_skips_omitted_flag_name():
arguments_group = flexmock()
flexmock(module).should_receive('make_argument_description').and_return('help')
+25 -17
View File
@@ -44,19 +44,23 @@ def test_interactive_console_true_when_isatty_and_TERM_is_not_dumb(capsys):
assert module.interactive_console() is True
def test_should_do_markup_respects_no_color_value():
flexmock(module.os.environ).should_receive('get').and_return(None)
def test_should_do_markup_respects_json_enabled_value():
flexmock(module.os.environ).should_receive('get').never()
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=True, configs={}) is False
assert module.should_do_markup(configs={}, json_enabled=True) is False
def test_should_do_markup_respects_config_value():
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=False, configs={'foo.yaml': {'color': False}}) is False
assert (
module.should_do_markup(configs={'foo.yaml': {'color': False}}, json_enabled=False) is False
)
flexmock(module).should_receive('interactive_console').and_return(True).once()
assert module.should_do_markup(no_color=False, configs={'foo.yaml': {'color': True}}) is True
assert (
module.should_do_markup(configs={'foo.yaml': {'color': True}}, json_enabled=False) is True
)
def test_should_do_markup_prefers_any_false_config_value():
@@ -65,11 +69,11 @@ def test_should_do_markup_prefers_any_false_config_value():
assert (
module.should_do_markup(
no_color=False,
configs={
'foo.yaml': {'color': True},
'bar.yaml': {'color': False},
},
json_enabled=False,
)
is False
)
@@ -83,14 +87,16 @@ def test_should_do_markup_respects_PY_COLORS_environment_variable():
flexmock(module).should_receive('to_bool').and_return(True)
assert module.should_do_markup(no_color=False, configs={}) is True
assert module.should_do_markup(configs={}, json_enabled=False) is True
def test_should_do_markup_prefers_no_color_value_to_config_value():
def test_should_do_markup_prefers_json_enabled_value_to_config_value():
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=True, configs={'foo.yaml': {'color': True}}) is False
assert (
module.should_do_markup(configs={'foo.yaml': {'color': True}}, json_enabled=True) is False
)
def test_should_do_markup_prefers_config_value_to_environment_variables():
@@ -98,7 +104,9 @@ def test_should_do_markup_prefers_config_value_to_environment_variables():
flexmock(module).should_receive('to_bool').and_return(True)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=False, configs={'foo.yaml': {'color': False}}) is False
assert (
module.should_do_markup(configs={'foo.yaml': {'color': False}}, json_enabled=False) is False
)
def test_should_do_markup_prefers_no_color_value_to_environment_variables():
@@ -106,14 +114,14 @@ def test_should_do_markup_prefers_no_color_value_to_environment_variables():
flexmock(module).should_receive('to_bool').and_return(True)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=True, configs={}) is False
assert module.should_do_markup(configs={}, json_enabled=False) is False
def test_should_do_markup_respects_interactive_console_value():
flexmock(module.os.environ).should_receive('get').and_return(None)
flexmock(module).should_receive('interactive_console').and_return(True)
assert module.should_do_markup(no_color=False, configs={}) is True
assert module.should_do_markup(configs={}, json_enabled=False) is True
def test_should_do_markup_prefers_PY_COLORS_to_interactive_console_value():
@@ -124,7 +132,7 @@ def test_should_do_markup_prefers_PY_COLORS_to_interactive_console_value():
flexmock(module).should_receive('to_bool').and_return(True)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=False, configs={}) is True
assert module.should_do_markup(configs={}, json_enabled=False) is True
def test_should_do_markup_prefers_NO_COLOR_to_interactive_console_value():
@@ -132,7 +140,7 @@ def test_should_do_markup_prefers_NO_COLOR_to_interactive_console_value():
flexmock(module.os.environ).should_receive('get').with_args('NO_COLOR', None).and_return('True')
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=False, configs={}) is False
assert module.should_do_markup(configs={}, json_enabled=False) is False
def test_should_do_markup_respects_NO_COLOR_environment_variable():
@@ -140,7 +148,7 @@ def test_should_do_markup_respects_NO_COLOR_environment_variable():
flexmock(module.os.environ).should_receive('get').with_args('PY_COLORS', None).and_return(None)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=False, configs={}) is False
assert module.should_do_markup(configs={}, json_enabled=False) is False
def test_should_do_markup_ignores_empty_NO_COLOR_environment_variable():
@@ -148,7 +156,7 @@ def test_should_do_markup_ignores_empty_NO_COLOR_environment_variable():
flexmock(module.os.environ).should_receive('get').with_args('PY_COLORS', None).and_return(None)
flexmock(module).should_receive('interactive_console').and_return(True)
assert module.should_do_markup(no_color=False, configs={}) is True
assert module.should_do_markup(configs={}, json_enabled=False) is True
def test_should_do_markup_prefers_NO_COLOR_to_PY_COLORS():
@@ -160,7 +168,7 @@ def test_should_do_markup_prefers_NO_COLOR_to_PY_COLORS():
)
flexmock(module).should_receive('interactive_console').never()
assert module.should_do_markup(no_color=False, configs={}) is False
assert module.should_do_markup(configs={}, json_enabled=False) is False
def test_multi_stream_handler_logs_to_handler_for_log_level():