Compare commits

..
15 Commits
Author SHA1 Message Date
Dan Helfman 340bd72176 Fix regression of argument parsing for default actions (#220). 2019-09-22 11:30:58 -07:00
Dan Helfman 1a1bb71af1 Fix error with "borgmatic check --only" command-line flag with "extract" consistency check (#217). 2019-09-20 11:43:27 -07:00
Dan Helfman ae45dfe63a Clarify command-like help for check --only. 2019-09-19 15:20:05 -07:00
Dan Helfman d6ac7a9192 Upgrade various dependencies. 2019-09-19 13:04:59 -07:00
Dan Helfman d959fdbf8d Document new "check --only" command-line flag. 2019-09-19 11:50:29 -07:00
Dan Helfman 81739791e0 Override configured consistency checks via "borgmatic check --only" command-line flag (#210). 2019-09-19 11:43:53 -07:00
Dan Helfman 4cdff74e9b Support for Borg check --verify-data flag via borgmatic "data" consistency check (#210). 2019-09-18 16:52:27 -07:00
Dan Helfman 11e830bb1d Fix flake8 warning. 2019-09-18 14:11:56 -07:00
Dan Helfman cba00a9c4e Add NEWS entry for generate-borgmatic-config comment change. 2019-09-18 14:06:03 -07:00
Dan HelfmanandGitea f2198de151 Merge branch 'comments-white-space' of polyzen/borgmatic into master 2019-09-18 21:03:56 +00:00
Daniel M. Capella 0c439c0c02 Add space to separate comments from tokens
https://yaml.org/spec/1.2/spec.html#id2780069
2019-09-17 20:00:58 -04:00
Dan Helfman f11a9bb4aa Revert "Fix for spurious Borg traceback when initializing a repository in an empty directory (#201)."
This reverts commit 9585c8f908.
2019-09-14 16:14:20 -07:00
Dan HelfmanandGitea ee6f390910 Merge branch 'point-to-stable-docs' of polyzen/borgmatic into master 2019-09-14 21:53:34 +00:00
Daniel M. Capella 9a5117db14 Consistently point to stable Borg docs 2019-09-14 17:30:28 -04:00
Dan Helfman 9585c8f908 Fix for spurious Borg traceback when initializing a repository in an empty directory (#201). 2019-09-13 13:08:23 -07:00
15 changed files with 276 additions and 148 deletions
+12
View File
@@ -1,3 +1,15 @@
1.3.18
* #220: Fix regression of argument parsing for default actions.
1.3.17
* #217: Fix error with "borgmatic check --only" command-line flag with "extract" consistency check.
1.3.16
* #210: Support for Borg check --verify-data flag via borgmatic "data" consistency check.
* #210: Override configured consistency checks via "borgmatic check --only" command-line flag.
* When generating sample configuration with generate-borgmatic-config, add a space after each "#"
comment indicator.
1.3.15
* #208: Fix for traceback when the "checks" option has an empty value.
* #209: Bypass Borg error about a moved repository via "relocated_repo_access_is_ok" option in
+28 -15
View File
@@ -10,9 +10,10 @@ DEFAULT_PREFIX = '{hostname}-'
logger = logging.getLogger(__name__)
def _parse_checks(consistency_config):
def _parse_checks(consistency_config, only_checks=None):
'''
Given a consistency config with a "checks" list, transform it to a tuple of named checks to run.
Given a consistency config with a "checks" list, and an optional list of override checks,
transform them a tuple of named checks to run.
For example, given a retention config of:
@@ -22,16 +23,21 @@ def _parse_checks(consistency_config):
('repository', 'archives')
If no "checks" option is present, return the DEFAULT_CHECKS. If the checks value is the string
"disabled", return an empty tuple, meaning that no checks should be run.
If no "checks" option is present in the config, return the DEFAULT_CHECKS. If the checks value
is the string "disabled", return an empty tuple, meaning that no checks should be run.
If the "data" option is present, then make sure the "archives" option is included as well.
'''
checks = consistency_config.get('checks', []) or []
checks = [
check.lower() for check in (only_checks or consistency_config.get('checks', []) or [])
]
if checks == ['disabled']:
return ()
return (
tuple(check for check in checks if check.lower() not in ('disabled', '')) or DEFAULT_CHECKS
)
if 'data' in checks and 'archives' not in checks:
checks.append('archives')
return tuple(check for check in checks if check not in ('disabled', '')) or DEFAULT_CHECKS
def _make_check_flags(checks, check_last=None, prefix=None):
@@ -68,30 +74,37 @@ def _make_check_flags(checks, check_last=None, prefix=None):
'Ignoring consistency prefix option, as "archives" is not in consistency checks.'
)
common_flags = last_flags + prefix_flags + (('--verify-data',) if 'data' in checks else ())
if set(DEFAULT_CHECKS).issubset(set(checks)):
return last_flags + prefix_flags
return common_flags
return (
tuple('--{}-only'.format(check) for check in checks if check in DEFAULT_CHECKS)
+ last_flags
+ prefix_flags
+ common_flags
)
def check_archives(
repository, storage_config, consistency_config, local_path='borg', remote_path=None
repository,
storage_config,
consistency_config,
local_path='borg',
remote_path=None,
only_checks=None,
):
'''
Given a local or remote repository path, a storage config dict, a consistency config dict,
and a local/remote commands to run, check the contained Borg archives for consistency.
local/remote commands to run, and an optional list of checks to use instead of configured
checks, check the contained Borg archives for consistency.
If there are no consistency checks to run, skip running them.
'''
checks = _parse_checks(consistency_config)
checks = _parse_checks(consistency_config, only_checks)
check_last = consistency_config.get('check_last', None)
lock_wait = None
if set(checks).intersection(set(DEFAULT_CHECKS)):
if set(checks).intersection(set(DEFAULT_CHECKS + ('data',))):
remote_path_flags = ('--remote-path', remote_path) if remote_path else ()
lock_wait = storage_config.get('lock_wait', None)
lock_wait_flags = ('--lock-wait', str(lock_wait)) if lock_wait else ()
+67 -20
View File
@@ -14,14 +14,14 @@ SUBPARSER_ALIASES = {
}
def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
def parse_subparser_arguments(unparsed_arguments, subparsers):
'''
Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
object as returned by argparse.ArgumentParser().add_subparsers(), ask each subparser to parse
its own arguments and the top-level parser to parse any remaining arguments.
Given a sequence of arguments, and a subparsers object as returned by
argparse.ArgumentParser().add_subparsers(), give each requested action's subparser a shot at
parsing all arguments. This allows common arguments like "--repository" to be shared across
multiple subparsers.
Return the result as a dict mapping from subparser name (or "global") to a parsed namespace of
arguments.
Return the result as a dict mapping from subparser name to a parsed namespace of arguments.
'''
arguments = collections.OrderedDict()
remaining_arguments = list(unparsed_arguments)
@@ -31,35 +31,73 @@ def parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers):
for alias in aliases
}
# Give each requested action's subparser a shot at parsing all arguments.
for subparser_name, subparser in subparsers.choices.items():
if subparser_name not in unparsed_arguments:
if subparser_name not in remaining_arguments:
continue
remaining_arguments.remove(subparser_name)
canonical_name = alias_to_subparser_name.get(subparser_name, subparser_name)
parsed, remaining = subparser.parse_known_args(unparsed_arguments)
# If a parsed value happens to be the same as the name of a subparser, remove it from the
# remaining arguments. This prevents, for instance, "check --only extract" from triggering
# the "extract" subparser.
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
for value in vars(parsed).values():
if isinstance(value, str):
if value in subparsers.choices:
remaining_arguments.remove(value)
elif isinstance(value, list):
for item in value:
if item in subparsers.choices:
remaining_arguments.remove(item)
arguments[canonical_name] = parsed
# If no actions are explicitly requested, assume defaults: prune, create, and check.
if not arguments and '--help' not in unparsed_arguments and '-h' not in unparsed_arguments:
for subparser_name in ('prune', 'create', 'check'):
subparser = subparsers.choices[subparser_name]
parsed, remaining = subparser.parse_known_args(unparsed_arguments)
parsed, unused_remaining = subparser.parse_known_args(unparsed_arguments)
arguments[subparser_name] = parsed
# Then ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
# are global arguments.
for subparser_name in arguments.keys():
subparser = subparsers.choices[subparser_name]
parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
arguments['global'] = top_level_parser.parse_args(remaining_arguments)
return arguments
def parse_global_arguments(unparsed_arguments, top_level_parser, subparsers):
'''
Given a sequence of arguments, a top-level parser (containing subparsers), and a subparsers
object as returned by argparse.ArgumentParser().add_subparsers(), parse and return any global
arguments as a parsed argparse.Namespace instance.
'''
# Ask each subparser, one by one, to greedily consume arguments. Any arguments that remain
# are global arguments.
remaining_arguments = list(unparsed_arguments)
present_subparser_names = set()
for subparser_name, subparser in subparsers.choices.items():
if subparser_name not in remaining_arguments:
continue
present_subparser_names.add(subparser_name)
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
# If no actions are explicitly requested, assume defaults: prune, create, and check.
if (
not present_subparser_names
and '--help' not in unparsed_arguments
and '-h' not in unparsed_arguments
):
for subparser_name in ('prune', 'create', 'check'):
subparser = subparsers.choices[subparser_name]
unused_parsed, remaining_arguments = subparser.parse_known_args(remaining_arguments)
# Remove the subparser names themselves.
for subparser_name in present_subparser_names:
if subparser_name in remaining_arguments:
remaining_arguments.remove(subparser_name)
return top_level_parser.parse_args(remaining_arguments)
def parse_arguments(*unparsed_arguments):
'''
Given command-line arguments with which this script was invoked, parse the arguments and return
@@ -212,6 +250,14 @@ def parse_arguments(*unparsed_arguments):
add_help=False,
)
check_group = check_parser.add_argument_group('check arguments')
check_group.add_argument(
'--only',
metavar='CHECK',
choices=('repository', 'archives', 'data', 'extract'),
dest='only',
action='append',
help='Run a particular consistency check (repository, archives, data, or extract) instead of configured checks; can specify flag multiple times',
)
check_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
extract_parser = subparsers.add_parser(
@@ -331,7 +377,8 @@ def parse_arguments(*unparsed_arguments):
)
info_group.add_argument('-h', '--help', action='help', help='Show this help message and exit')
arguments = parse_subparser_arguments(unparsed_arguments, top_level_parser, subparsers)
arguments = parse_subparser_arguments(unparsed_arguments, subparsers)
arguments['global'] = parse_global_arguments(unparsed_arguments, top_level_parser, subparsers)
if arguments['global'].excludes_filename:
raise ValueError(
+6 -1
View File
@@ -147,7 +147,12 @@ def run_actions(
if 'check' in arguments and checks.repository_enabled_for_checks(repository, consistency):
logger.info('{}: Running consistency checks'.format(repository))
borg_check.check_archives(
repository, storage, consistency, local_path=local_path, remote_path=remote_path
repository,
storage,
consistency,
local_path=local_path,
remote_path=remote_path,
only_checks=arguments['check'].only,
)
if 'extract' in arguments:
if arguments['extract'].repository is None or repository == arguments['extract'].repository:
+2 -2
View File
@@ -45,10 +45,10 @@ def _comment_out_line(line):
# Comment out the names of optional sections.
one_indent = ' ' * INDENT
if not line.startswith(one_indent):
return '#' + line
return '# ' + line
# Otherwise, comment out the line, but insert the "#" after the first indent for aesthetics.
return '#'.join((one_indent, line[INDENT:]))
return '# '.join((one_indent, line[INDENT:]))
REQUIRED_KEYS = {'source_directories', 'repositories', 'keep_daily'}
+7 -6
View File
@@ -290,14 +290,15 @@ map:
checks:
seq:
- type: str
enum: ['repository', 'archives', 'extract', 'disabled']
enum: ['repository', 'archives', 'data', 'extract', 'disabled']
unique: true
desc: |
List of one or more consistency checks to run: "repository", "archives", and/or
"extract". Defaults to "repository" and "archives". Set to "disabled" to disable
all consistency checks. "repository" checks the consistency of the repository,
"archive" checks all of the archives, and "extract" does an extraction dry-run
of the most recent archive.
List of one or more consistency checks to run: "repository", "archives", "data",
and/or "extract". Defaults to "repository" and "archives". Set to "disabled" to
disable all consistency checks. "repository" checks the consistency of the
repository, "archives" checks all of the archives, "data" verifies the integrity
of the data within the archives, and "extract" does an extraction dry-run of the
most recent archive. Note that "data" implies "archives".
example:
- repository
- archives
+3 -3
View File
@@ -1,4 +1,4 @@
FROM python:3.7.3-alpine3.10 as borgmatic
FROM python:3.7.4-alpine3.10 as borgmatic
COPY . /app
RUN pip install --no-cache /app && generate-borgmatic-config && chmod +r /etc/borgmatic/config.yaml
@@ -7,7 +7,7 @@ RUN borgmatic --help > /command-line.txt \
echo -e "\n--------------------------------------------------------------------------------\n" >> /command-line.txt \
&& borgmatic "$action" --help >> /command-line.txt; done
FROM node:12.4.0-alpine as html
FROM node:12.10.0-alpine as html
WORKDIR /source
@@ -23,7 +23,7 @@ COPY . /source
RUN npx eleventy --input=/source/docs --output=/output/docs \
&& mv /output/docs/index.html /output/index.html
FROM nginx:1.16.0-alpine
FROM nginx:1.16.1-alpine
COPY --from=html /output /usr/share/nginx/html
COPY --from=borgmatic /etc/borgmatic/config.yaml /usr/share/nginx/html/docs/reference/config.yaml
@@ -68,6 +68,16 @@ consistency:
- path/of/repository_to_check.borg
```
Finally, you can override your configuration file's consistency checks, and
run particular checks via the command-line. For instance:
```bash
borgmatic check --only data --only extract
```
This is useful for running slow consistency checks on an infrequent basis,
separate from your regular checks.
## Troubleshooting
+4 -4
View File
@@ -4,7 +4,7 @@ title: How to set up backups with borgmatic
## Installation
To get up and running, first [install
Borg](https://borgbackup.readthedocs.io/en/latest/installation.html), at
Borg](https://borgbackup.readthedocs.io/en/stable/installation.html), at
least version 1.1.
Borgmatic consumes configurations in `/etc/borgmatic/` and `/etc/borgmatic.d/`
@@ -88,7 +88,7 @@ encrypt your Borg repository with a passphrase instead of a key file, you'll
either need to set the borgmatic `encryption_passphrase` configuration
variable or set the `BORG_PASSPHRASE` environment variable. See the
[repository encryption
section](https://borgbackup.readthedocs.io/en/latest/quickstart.html#repository-encryption)
section](https://borgbackup.readthedocs.io/en/stable/quickstart.html#repository-encryption)
of the Borg Quick Start for more info.
Alternatively, you can specify the passphrase programatically by setting
@@ -133,11 +133,11 @@ This uses the borgmatic configuration file you created above to determine
which local or remote repository to create, and encrypts it with the
encryption passphrase specified there if one is provided. Read about [Borg
encryption
modes](https://borgbackup.readthedocs.io/en/latest/usage/init.html#encryption-modes)
modes](https://borgbackup.readthedocs.io/en/stable/usage/init.html#encryption-modes)
for the menu of available encryption modes.
Also, optionally check out the [Borg Quick
Start](https://borgbackup.readthedocs.org/en/latest/quickstart.html) for more
Start](https://borgbackup.readthedocs.org/en/stable/quickstart.html) for more
background about repository initialization.
Note that borgmatic skips repository initialization if the repository already
+1 -1
View File
@@ -1,6 +1,6 @@
from setuptools import find_packages, setup
VERSION = '1.3.15'
VERSION = '1.3.18'
setup(
+7 -7
View File
@@ -4,21 +4,21 @@ attrs==19.1.0
black==19.3b0; python_version >= '3.6'
click==7.0
colorama==0.4.1
coverage==4.5.3
coverage==4.5.4
docopt==0.6.2
flake8==3.7.7
flake8==3.7.8
flexmock==0.10.4
isort==4.3.20
isort==4.3.21
mccabe==0.6.1
more-itertools==7.0.0
pluggy==0.12.0
more-itertools==7.2.0
pluggy==0.13.0
py==1.8.0
pycodestyle==2.5.0
pyflakes==2.1.1
pykwalify==1.7.0
pytest==4.6.3
pytest==5.1.2
pytest-cov==2.7.1
python-dateutil==2.8.0
PyYAML==5.1.1
PyYAML==5.1.2
ruamel.yaml>0.15.0,<0.17.0
toml==0.10.0
+30 -6
View File
@@ -78,6 +78,18 @@ def test_parse_arguments_with_no_actions_defaults_to_all_actions_enabled():
assert 'check' in arguments
def test_parse_arguments_with_no_actions_passes_argument_to_relevant_actions():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
arguments = module.parse_arguments('--stats')
assert 'prune' in arguments
assert arguments['prune'].stats
assert 'create' in arguments
assert arguments['create'].stats
assert 'check' in arguments
def test_parse_arguments_with_help_and_no_actions_shows_global_help(capsys):
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@@ -322,12 +334,6 @@ def test_parse_arguments_with_stats_flag_but_no_create_or_prune_flag_raises_valu
module.parse_arguments('--stats', 'list')
def test_parse_arguments_with_just_stats_flag_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('--stats')
def test_parse_arguments_allows_json_with_list_or_info():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
@@ -346,3 +352,21 @@ def test_parse_arguments_disallows_json_with_both_list_and_info():
with pytest.raises(ValueError):
module.parse_arguments('list', 'info', '--json')
def test_parse_arguments_check_only_extract_does_not_raise_extract_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('check', '--only', 'extract')
def test_parse_arguments_extract_archive_check_does_not_raise_check_subparser_error():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('extract', '--archive', 'check')
def test_parse_arguments_extract_with_check_only_extract_does_not_raise():
flexmock(module.collect).should_receive('get_default_config_paths').and_return(['default'])
module.parse_arguments('extract', '--archive', 'name', 'check', '--only', 'extract')
+11 -10
View File
@@ -31,17 +31,17 @@ def test_comment_out_line_skips_already_commented_out_line():
def test_comment_out_line_comments_section_name():
line = 'figgy-pudding:'
assert module._comment_out_line(line) == '#' + line
assert module._comment_out_line(line) == '# ' + line
def test_comment_out_line_comments_indented_option():
line = ' enabled: true'
assert module._comment_out_line(line) == ' #enabled: true'
assert module._comment_out_line(line) == ' # enabled: true'
def test_comment_out_optional_configuration_comments_optional_config_only():
flexmock(module)._comment_out_line = lambda line: '#' + line
flexmock(module)._comment_out_line = lambda line: '# ' + line
config = '''
foo:
bar:
@@ -56,18 +56,19 @@ location:
other: thing
'''
# flake8: noqa
expected_config = '''
#foo:
# bar:
# - baz
# - quux
#
# foo:
# bar:
# - baz
# - quux
#
location:
repositories:
- one
- two
#
# other: thing
#
# other: thing
'''
assert module._comment_out_optional_configuration(config.strip()) == expected_config.strip()
+36
View File
@@ -46,12 +46,48 @@ def test_parse_checks_with_disabled_returns_no_checks():
assert checks == ()
def test_parse_checks_with_data_check_also_injects_archives():
checks = module._parse_checks({'checks': ['data']})
assert checks == ('data', 'archives')
def test_parse_checks_with_data_check_passes_through_archives():
checks = module._parse_checks({'checks': ['data', 'archives']})
assert checks == ('data', 'archives')
def test_parse_checks_prefers_override_checks_to_configured_checks():
checks = module._parse_checks({'checks': ['archives']}, only_checks=['repository', 'extract'])
assert checks == ('repository', 'extract')
def test_parse_checks_with_override_data_check_also_injects_archives():
checks = module._parse_checks({'checks': ['extract']}, only_checks=['data'])
assert checks == ('data', 'archives')
def test_make_check_flags_with_repository_check_returns_flag():
flags = module._make_check_flags(('repository',))
assert flags == ('--repository-only',)
def test_make_check_flags_with_archives_check_returns_flag():
flags = module._make_check_flags(('archives',))
assert flags == ('--archives-only',)
def test_make_check_flags_with_data_check_returns_flag():
flags = module._make_check_flags(('data',))
assert flags == ('--verify-data',)
def test_make_check_flags_with_extract_omits_extract_flag():
flags = module._make_check_flags(('extract',))
+52 -73
View File
@@ -4,9 +4,7 @@ from borgmatic.commands import arguments as module
def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser_name():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
@@ -14,17 +12,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_before_subparser
}
)
arguments = module.parse_subparser_arguments(
('--foo', 'true', 'action'), top_level_parser, subparsers
)
arguments = module.parse_subparser_arguments(('--foo', 'true', 'action'), subparsers)
assert arguments == {'action': action_namespace, 'global': global_namespace}
assert arguments == {'action': action_namespace}
def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_name():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(parse_known_args=lambda arguments: (action_namespace, [])),
@@ -32,57 +26,13 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_after_subparser_
}
)
arguments = module.parse_subparser_arguments(
('action', '--foo', 'true'), top_level_parser, subparsers
)
arguments = module.parse_subparser_arguments(('action', '--foo', 'true'), subparsers)
assert arguments == {'action': action_namespace, 'global': global_namespace}
def test_parse_subparser_arguments_consumes_global_arguments_before_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_subparser_arguments(
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
)
assert arguments == {'action': action_namespace, 'global': global_namespace}
def test_parse_subparser_arguments_consumes_global_arguments_after_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_subparser_arguments(
('action', '--verbosity', 'lots'), top_level_parser, subparsers
)
assert arguments == {'action': action_namespace, 'global': global_namespace}
assert arguments == {'action': action_namespace}
def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
action_subparser = flexmock(parse_known_args=lambda arguments: (action_namespace, []))
subparsers = flexmock(
choices={
@@ -94,18 +44,14 @@ def test_parse_subparser_arguments_consumes_subparser_arguments_with_alias():
)
flexmock(module).SUBPARSER_ALIASES = {'action': ['-a'], 'other': ['-o']}
arguments = module.parse_subparser_arguments(
('-a', '--foo', 'true'), top_level_parser, subparsers
)
arguments = module.parse_subparser_arguments(('-a', '--foo', 'true'), subparsers)
assert arguments == {'action': action_namespace, 'global': global_namespace}
assert arguments == {'action': action_namespace}
def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
global_namespace = flexmock()
action_namespace = flexmock(foo=True)
other_namespace = flexmock(bar=3)
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
@@ -116,22 +62,16 @@ def test_parse_subparser_arguments_consumes_multiple_subparser_arguments():
)
arguments = module.parse_subparser_arguments(
('action', '--foo', 'true', 'other', '--bar', '3'), top_level_parser, subparsers
('action', '--foo', 'true', 'other', '--bar', '3'), subparsers
)
assert arguments == {
'action': action_namespace,
'other': other_namespace,
'global': global_namespace,
}
assert arguments == {'action': action_namespace, 'other': other_namespace}
def test_parse_subparser_arguments_applies_default_subparsers():
global_namespace = flexmock()
prune_namespace = flexmock()
create_namespace = flexmock(progress=True)
check_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'prune': flexmock(parse_known_args=lambda arguments: (prune_namespace, ['--progress'])),
@@ -141,17 +81,16 @@ def test_parse_subparser_arguments_applies_default_subparsers():
}
)
arguments = module.parse_subparser_arguments(('--progress'), top_level_parser, subparsers)
arguments = module.parse_subparser_arguments(('--progress'), subparsers)
assert arguments == {
'prune': prune_namespace,
'create': create_namespace,
'check': check_namespace,
'global': global_namespace,
}
def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers():
def test_parse_global_arguments_with_help_does_not_apply_default_subparsers():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
@@ -164,8 +103,48 @@ def test_parse_subparser_arguments_with_help_does_not_apply_default_subparsers()
}
)
arguments = module.parse_subparser_arguments(
arguments = module.parse_global_arguments(
('--verbosity', 'lots', '--help'), top_level_parser, subparsers
)
assert arguments == {'global': global_namespace}
assert arguments == global_namespace
def test_parse_global_arguments_consumes_global_arguments_before_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_global_arguments(
('--verbosity', 'lots', 'action'), top_level_parser, subparsers
)
assert arguments == global_namespace
def test_parse_global_arguments_consumes_global_arguments_after_subparser_name():
global_namespace = flexmock(verbosity='lots')
action_namespace = flexmock()
top_level_parser = flexmock(parse_args=lambda arguments: global_namespace)
subparsers = flexmock(
choices={
'action': flexmock(
parse_known_args=lambda arguments: (action_namespace, ['--verbosity', 'lots'])
),
'other': flexmock(),
}
)
arguments = module.parse_global_arguments(
('action', '--verbosity', 'lots'), top_level_parser, subparsers
)
assert arguments == global_namespace