瀏覽代碼

Merge branch 'master' of https://github.com/diivi/borgmatic into feat/tag-repos

Divyansh Singh 2 年之前
父節點
當前提交
1bc003560a
共有 64 個文件被更改,包括 286 次插入305 次删除
  1. 3 0
      .eleventy.js
  2. 4 0
      NEWS
  3. 1 1
      borgmatic/actions/borg.py
  4. 1 1
      borgmatic/actions/check.py
  5. 5 1
      borgmatic/actions/compact.py
  6. 1 1
      borgmatic/actions/create.py
  7. 1 5
      borgmatic/actions/export_tar.py
  8. 1 3
      borgmatic/actions/extract.py
  9. 2 4
      borgmatic/actions/mount.py
  10. 1 1
      borgmatic/actions/prune.py
  11. 1 1
      borgmatic/actions/rcreate.py
  12. 2 5
      borgmatic/actions/restore.py
  13. 2 3
      borgmatic/actions/rinfo.py
  14. 2 1
      borgmatic/actions/rlist.py
  15. 2 2
      borgmatic/borg/check.py
  16. 1 1
      borgmatic/borg/create.py
  17. 1 1
      borgmatic/borg/export_tar.py
  18. 1 1
      borgmatic/borg/flags.py
  19. 1 1
      borgmatic/borg/list.py
  20. 1 1
      borgmatic/borg/prune.py
  21. 2 2
      borgmatic/borg/rlist.py
  22. 1 3
      borgmatic/commands/arguments.py
  23. 15 25
      borgmatic/commands/borgmatic.py
  24. 2 2
      borgmatic/commands/completion.py
  25. 7 15
      borgmatic/commands/convert_config.py
  26. 4 10
      borgmatic/commands/generate_config.py
  27. 3 7
      borgmatic/commands/validate_config.py
  28. 2 2
      borgmatic/config/collect.py
  29. 4 1
      borgmatic/config/environment.py
  30. 4 6
      borgmatic/config/generate.py
  31. 5 11
      borgmatic/config/legacy.py
  32. 11 12
      borgmatic/config/validate.py
  33. 19 13
      borgmatic/execute.py
  34. 6 12
      borgmatic/hooks/command.py
  35. 3 5
      borgmatic/hooks/cronhub.py
  36. 3 5
      borgmatic/hooks/cronitor.py
  37. 2 2
      borgmatic/hooks/dispatch.py
  38. 3 5
      borgmatic/hooks/dump.py
  39. 4 6
      borgmatic/hooks/healthchecks.py
  40. 3 7
      borgmatic/hooks/mongodb.py
  41. 2 4
      borgmatic/hooks/mysql.py
  42. 4 6
      borgmatic/hooks/pagerduty.py
  43. 2 4
      borgmatic/hooks/postgresql.py
  44. 1 1
      borgmatic/hooks/sqlite.py
  45. 1 1
      borgmatic/logger.py
  46. 1 0
      docs/Dockerfile
  47. 15 0
      docs/_includes/index.css
  48. 1 1
      docs/_includes/layouts/base.njk
  49. 2 0
      test_requirements.txt
  50. 6 10
      tests/end-to-end/test_borgmatic.py
  51. 1 1
      tests/end-to-end/test_database.py
  52. 3 5
      tests/end-to-end/test_override.py
  53. 4 12
      tests/end-to-end/test_validate_config.py
  54. 1 1
      tests/integration/config/test_legacy.py
  55. 4 4
      tests/integration/test_execute.py
  56. 10 10
      tests/unit/borg/test_create.py
  57. 17 5
      tests/unit/borg/test_prune.py
  58. 14 14
      tests/unit/config/test_environment.py
  59. 4 4
      tests/unit/config/test_validate.py
  60. 5 12
      tests/unit/hooks/test_command.py
  61. 1 3
      tests/unit/hooks/test_healthchecks.py
  62. 1 1
      tests/unit/hooks/test_mongodb.py
  63. 1 1
      tests/unit/hooks/test_postgresql.py
  64. 48 20
      tests/unit/test_execute.py

+ 3 - 0
.eleventy.js

@@ -1,4 +1,5 @@
 const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
 const pluginSyntaxHighlight = require("@11ty/eleventy-plugin-syntaxhighlight");
+const codeClipboard = require("eleventy-plugin-code-clipboard");
 const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language");
 const inclusiveLangPlugin = require("@11ty/eleventy-plugin-inclusive-language");
 const navigationPlugin = require("@11ty/eleventy-navigation");
 const navigationPlugin = require("@11ty/eleventy-navigation");
 
 
@@ -6,6 +7,7 @@ module.exports = function(eleventyConfig) {
     eleventyConfig.addPlugin(pluginSyntaxHighlight);
     eleventyConfig.addPlugin(pluginSyntaxHighlight);
     eleventyConfig.addPlugin(inclusiveLangPlugin);
     eleventyConfig.addPlugin(inclusiveLangPlugin);
     eleventyConfig.addPlugin(navigationPlugin);
     eleventyConfig.addPlugin(navigationPlugin);
+    eleventyConfig.addPlugin(codeClipboard);
 
 
     let markdownIt = require("markdown-it");
     let markdownIt = require("markdown-it");
     let markdownItAnchor = require("markdown-it-anchor");
     let markdownItAnchor = require("markdown-it-anchor");
@@ -31,6 +33,7 @@ module.exports = function(eleventyConfig) {
         markdownIt(markdownItOptions)
         markdownIt(markdownItOptions)
             .use(markdownItAnchor, markdownItAnchorOptions)
             .use(markdownItAnchor, markdownItAnchorOptions)
             .use(markdownItReplaceLink)
             .use(markdownItReplaceLink)
+            .use(codeClipboard.markdownItCopyButton)
     );
     );
 
 
     eleventyConfig.addPassthroughCopy({"docs/static": "static"});
     eleventyConfig.addPassthroughCopy({"docs/static": "static"});

+ 4 - 0
NEWS

@@ -4,6 +4,10 @@
  * #576: Add support for "file://" paths within "repositories" option.
  * #576: Add support for "file://" paths within "repositories" option.
  * #618: Add support for BORG_FILES_CACHE_TTL environment variable via "borg_files_cache_ttl" option
  * #618: Add support for BORG_FILES_CACHE_TTL environment variable via "borg_files_cache_ttl" option
    in borgmatic's storage configuration.
    in borgmatic's storage configuration.
+ * #623: Fix confusing message when an error occurs running actions for a configuration file.
+ * #655: Fix error when databases are configured and a source directory doesn't exist.
+ * Add code style plugins to enforce use of Python f-strings and prevent single-letter variables.
+   To join in the pedantry, refresh your test environment with "tox --recreate".
 
 
 1.7.9
 1.7.9
  * #295: Add a SQLite database dump/restore hook.
  * #295: Add a SQLite database dump/restore hook.

+ 1 - 1
borgmatic/actions/borg.py

@@ -16,7 +16,7 @@ def run_borg(
     if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
     if borg_arguments.repository is None or borgmatic.config.validate.repositories_match(
         repository, borg_arguments.repository
         repository, borg_arguments.repository
     ):
     ):
-        logger.info('{}: Running arbitrary Borg command'.format(repository['path']))
+        logger.info(f'{repository["path"]}: Running arbitrary Borg command')
         archive_name = borgmatic.borg.rlist.resolve_archive_name(
         archive_name = borgmatic.borg.rlist.resolve_archive_name(
             repository['path'],
             repository['path'],
             borg_arguments.archive,
             borg_arguments.archive,

+ 1 - 1
borgmatic/actions/check.py

@@ -37,7 +37,7 @@ def run_check(
         global_arguments.dry_run,
         global_arguments.dry_run,
         **hook_context,
         **hook_context,
     )
     )
-    logger.info('{}: Running consistency checks'.format(repository['path']))
+    logger.info(f'{repository["path"]}: Running consistency checks')
     borgmatic.borg.check.check_archives(
     borgmatic.borg.check.check_archives(
         repository['path'],
         repository['path'],
         location,
         location,

+ 5 - 1
borgmatic/actions/compact.py

@@ -39,7 +39,7 @@ def run_compact(
         **hook_context,
         **hook_context,
     )
     )
     if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
     if borgmatic.borg.feature.available(borgmatic.borg.feature.Feature.COMPACT, local_borg_version):
-        logger.info('{}: Compacting segments{}'.format(repository['path'], dry_run_label))
+        logger.info(f'{repository["path"]}: Compacting segments{dry_run_label}')
         borgmatic.borg.compact.compact_segments(
         borgmatic.borg.compact.compact_segments(
             global_arguments.dry_run,
             global_arguments.dry_run,
             repository['path'],
             repository['path'],
@@ -52,9 +52,13 @@ def run_compact(
             threshold=compact_arguments.threshold,
             threshold=compact_arguments.threshold,
         )
         )
     else:  # pragma: nocover
     else:  # pragma: nocover
+<<<<<<< HEAD
         logger.info(
         logger.info(
             '{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository['path'])
             '{}: Skipping compact (only available/needed in Borg 1.2+)'.format(repository['path'])
         )
         )
+=======
+        logger.info(f'{repository}: Skipping compact (only available/needed in Borg 1.2+)')
+>>>>>>> f42890430c59a40a17d9a68a193d6a09674770cb
     borgmatic.hooks.command.execute_hook(
     borgmatic.hooks.command.execute_hook(
         hooks.get('after_compact'),
         hooks.get('after_compact'),
         hooks.get('umask'),
         hooks.get('umask'),

+ 1 - 1
borgmatic/actions/create.py

@@ -42,7 +42,7 @@ def run_create(
         global_arguments.dry_run,
         global_arguments.dry_run,
         **hook_context,
         **hook_context,
     )
     )
-    logger.info('{}: Creating archive{}'.format(repository['path'], dry_run_label))
+    logger.info(f'{repository["path"]}: Creating archive{dry_run_label}')
     borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
     borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
         'remove_database_dumps',
         'remove_database_dumps',
         hooks,
         hooks,

+ 1 - 5
borgmatic/actions/export_tar.py

@@ -22,11 +22,7 @@ def run_export_tar(
     if export_tar_arguments.repository is None or borgmatic.config.validate.repositories_match(
     if export_tar_arguments.repository is None or borgmatic.config.validate.repositories_match(
         repository, export_tar_arguments.repository
         repository, export_tar_arguments.repository
     ):
     ):
-        logger.info(
-            '{}: Exporting archive {} as tar file'.format(
-                repository['path'], export_tar_arguments.archive
-            )
-        )
+        logger.info(f'{repository["path"]}: Exporting archive {export_tar_arguments.archive} as tar file')
         borgmatic.borg.export_tar.export_tar_archive(
         borgmatic.borg.export_tar.export_tar_archive(
             global_arguments.dry_run,
             global_arguments.dry_run,
             repository['path'],
             repository['path'],

+ 1 - 3
borgmatic/actions/extract.py

@@ -35,9 +35,7 @@ def run_extract(
     if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
     if extract_arguments.repository is None or borgmatic.config.validate.repositories_match(
         repository, extract_arguments.repository
         repository, extract_arguments.repository
     ):
     ):
-        logger.info(
-            '{}: Extracting archive {}'.format(repository['path'], extract_arguments.archive)
-        )
+        logger.info(f'{repository["path"]}: Extracting archive {extract_arguments.archive}')
         borgmatic.borg.extract.extract_archive(
         borgmatic.borg.extract.extract_archive(
             global_arguments.dry_run,
             global_arguments.dry_run,
             repository['path'],
             repository['path'],

+ 2 - 4
borgmatic/actions/mount.py

@@ -17,11 +17,9 @@ def run_mount(
         repository, mount_arguments.repository
         repository, mount_arguments.repository
     ):
     ):
         if mount_arguments.archive:
         if mount_arguments.archive:
-            logger.info(
-                '{}: Mounting archive {}'.format(repository['path'], mount_arguments.archive)
-            )
+            logger.info(f'{repository["path"]}: Mounting archive {mount_arguments.archive}')
         else:  # pragma: nocover
         else:  # pragma: nocover
-            logger.info('{}: Mounting repository'.format(repository['path']))
+            logger.info(f'{repository["path"]}: Mounting repository')
 
 
         borgmatic.borg.mount.mount_archive(
         borgmatic.borg.mount.mount_archive(
             repository['path'],
             repository['path'],

+ 1 - 1
borgmatic/actions/prune.py

@@ -37,7 +37,7 @@ def run_prune(
         global_arguments.dry_run,
         global_arguments.dry_run,
         **hook_context,
         **hook_context,
     )
     )
-    logger.info('{}: Pruning archives{}'.format(repository['path'], dry_run_label))
+    logger.info(f'{repository["path"]}: Pruning archives{dry_run_label}')
     borgmatic.borg.prune.prune_archives(
     borgmatic.borg.prune.prune_archives(
         global_arguments.dry_run,
         global_arguments.dry_run,
         repository['path'],
         repository['path'],

+ 1 - 1
borgmatic/actions/rcreate.py

@@ -23,7 +23,7 @@ def run_rcreate(
     ):
     ):
         return
         return
 
 
-    logger.info('{}: Creating repository'.format(repository['path']))
+    logger.info(f'{repository["path"]}: Creating repository')
     borgmatic.borg.rcreate.create_repository(
     borgmatic.borg.rcreate.create_repository(
         global_arguments.dry_run,
         global_arguments.dry_run,
         repository['path'],
         repository['path'],

+ 2 - 5
borgmatic/actions/restore.py

@@ -255,11 +255,8 @@ def run_restore(
     ):
     ):
         return
         return
 
 
-    logger.info(
-        '{}: Restoring databases from archive {}'.format(
-            repository['path'], restore_arguments.archive
-        )
-    )
+    logger.info(f'{repository["path"]}: Restoring databases from archive {restore_arguments.archive}')
+
     borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
     borgmatic.hooks.dispatch.call_hooks_even_if_unconfigured(
         'remove_database_dumps',
         'remove_database_dumps',
         hooks,
         hooks,

+ 2 - 3
borgmatic/actions/rinfo.py

@@ -19,9 +19,8 @@ def run_rinfo(
         repository, rinfo_arguments.repository
         repository, rinfo_arguments.repository
     ):
     ):
         if not rinfo_arguments.json:  # pragma: nocover
         if not rinfo_arguments.json:  # pragma: nocover
-            logger.answer(
-                '{}: Displaying repository summary information'.format(repository['path'])
-            )
+            logger.answer(f'{repository["path"]}: Displaying repository summary information')
+
         json_output = borgmatic.borg.rinfo.display_repository_info(
         json_output = borgmatic.borg.rinfo.display_repository_info(
             repository['path'],
             repository['path'],
             storage,
             storage,

+ 2 - 1
borgmatic/actions/rlist.py

@@ -19,7 +19,8 @@ def run_rlist(
         repository, rlist_arguments.repository
         repository, rlist_arguments.repository
     ):
     ):
         if not rlist_arguments.json:  # pragma: nocover
         if not rlist_arguments.json:  # pragma: nocover
-            logger.answer('{}: Listing repository'.format(repository['path']))
+            logger.answer(f'{repository["path"]}: Listing repository')
+
         json_output = borgmatic.borg.rlist.list_repository(
         json_output = borgmatic.borg.rlist.list_repository(
             repository['path'],
             repository['path'],
             storage,
             storage,

+ 2 - 2
borgmatic/borg/check.py

@@ -12,7 +12,7 @@ DEFAULT_CHECKS = (
     {'name': 'repository', 'frequency': '1 month'},
     {'name': 'repository', 'frequency': '1 month'},
     {'name': 'archives', 'frequency': '1 month'},
     {'name': 'archives', 'frequency': '1 month'},
 )
 )
-DEFAULT_PREFIX = '{hostname}-'
+DEFAULT_PREFIX = '{hostname}-'  # noqa: FS003
 
 
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
@@ -196,7 +196,7 @@ def make_check_flags(local_borg_version, checks, check_last=None, prefix=None):
         return common_flags
         return common_flags
 
 
     return (
     return (
-        tuple('--{}-only'.format(check) for check in checks if check in ('repository', 'archives'))
+        tuple(f'--{check}-only' for check in checks if check in ('repository', 'archives'))
         + common_flags
         + common_flags
     )
     )
 
 

+ 1 - 1
borgmatic/borg/create.py

@@ -217,7 +217,7 @@ def make_list_filter_flags(local_borg_version, dry_run):
         return f'{base_flags}-'
         return f'{base_flags}-'
 
 
 
 
-DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
+DEFAULT_ARCHIVE_NAME_FORMAT = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'  # noqa: FS003
 
 
 
 
 def collect_borgmatic_source_directories(borgmatic_source_directory):
 def collect_borgmatic_source_directories(borgmatic_source_directory):

+ 1 - 1
borgmatic/borg/export_tar.py

@@ -56,7 +56,7 @@ def export_tar_archive(
         output_log_level = logging.INFO
         output_log_level = logging.INFO
 
 
     if dry_run:
     if dry_run:
-        logging.info('{}: Skipping export to tar file (dry run)'.format(repository))
+        logging.info(f'{repository}: Skipping export to tar file (dry run)')
         return
         return
 
 
     execute_command(
     execute_command(

+ 1 - 1
borgmatic/borg/flags.py

@@ -10,7 +10,7 @@ def make_flags(name, value):
     if not value:
     if not value:
         return ()
         return ()
 
 
-    flag = '--{}'.format(name.replace('_', '-'))
+    flag = f"--{name.replace('_', '-')}"
 
 
     if value is True:
     if value is True:
         return (flag,)
         return (flag,)

+ 1 - 1
borgmatic/borg/list.py

@@ -113,7 +113,7 @@ def capture_archive_listing(
                     paths=[f'sh:{list_path}'],
                     paths=[f'sh:{list_path}'],
                     find_paths=None,
                     find_paths=None,
                     json=None,
                     json=None,
-                    format='{path}{NL}',
+                    format='{path}{NL}',  # noqa: FS003
                 ),
                 ),
                 local_path,
                 local_path,
                 remote_path,
                 remote_path,

+ 1 - 1
borgmatic/borg/prune.py

@@ -24,7 +24,7 @@ def make_prune_flags(retention_config, local_borg_version):
         )
         )
     '''
     '''
     config = retention_config.copy()
     config = retention_config.copy()
-    prefix = config.pop('prefix', '{hostname}-')
+    prefix = config.pop('prefix', '{hostname}-')  # noqa: FS003
 
 
     if prefix:
     if prefix:
         if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):
         if feature.available(feature.Feature.MATCH_ARCHIVES, local_borg_version):

+ 2 - 2
borgmatic/borg/rlist.py

@@ -42,7 +42,7 @@ def resolve_archive_name(
     except IndexError:
     except IndexError:
         raise ValueError('No archives found in the repository')
         raise ValueError('No archives found in the repository')
 
 
-    logger.debug('{}: Latest archive is {}'.format(repository, latest_archive))
+    logger.debug(f'{repository}: Latest archive is {latest_archive}')
 
 
     return latest_archive
     return latest_archive
 
 
@@ -117,7 +117,7 @@ def list_repository(
     )
     )
 
 
     if rlist_arguments.json:
     if rlist_arguments.json:
-        return execute_command_and_capture_output(main_command, extra_environment=borg_environment,)
+        return execute_command_and_capture_output(main_command, extra_environment=borg_environment)
     else:
     else:
         execute_command(
         execute_command(
             main_command,
             main_command,

+ 1 - 3
borgmatic/commands/arguments.py

@@ -131,9 +131,7 @@ def make_parsers():
         nargs='*',
         nargs='*',
         dest='config_paths',
         dest='config_paths',
         default=config_paths,
         default=config_paths,
-        help='Configuration filenames or directories, defaults to: {}'.format(
-            ' '.join(unexpanded_config_paths)
-        ),
+        help=f"Configuration filenames or directories, defaults to: {' '.join(unexpanded_config_paths)}",
     )
     )
     global_group.add_argument(
     global_group.add_argument(
         '--excludes',
         '--excludes',

+ 15 - 25
borgmatic/commands/borgmatic.py

@@ -70,9 +70,7 @@ def run_configuration(config_filename, config, arguments):
     try:
     try:
         local_borg_version = borg_version.local_borg_version(storage, local_path)
         local_borg_version = borg_version.local_borg_version(storage, local_path)
     except (OSError, CalledProcessError, ValueError) as error:
     except (OSError, CalledProcessError, ValueError) as error:
-        yield from log_error_records(
-            '{}: Error getting local Borg version'.format(config_filename), error
-        )
+        yield from log_error_records(f'{config_filename}: Error getting local Borg version', error)
         return
         return
 
 
     try:
     try:
@@ -100,7 +98,7 @@ def run_configuration(config_filename, config, arguments):
             return
             return
 
 
         encountered_error = error
         encountered_error = error
-        yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
+        yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
 
 
     if not encountered_error:
     if not encountered_error:
         repo_queue = Queue()
         repo_queue = Queue()
@@ -134,7 +132,7 @@ def run_configuration(config_filename, config, arguments):
                     repo_queue.put((repository, retry_num + 1),)
                     repo_queue.put((repository, retry_num + 1),)
                     tuple(  # Consume the generator so as to trigger logging.
                     tuple(  # Consume the generator so as to trigger logging.
                         log_error_records(
                         log_error_records(
-                            '{}: Error running actions for repository'.format(repository['path']),
+                            f'{repository["path"]}: Error running actions for repository',
                             error,
                             error,
                             levelno=logging.WARNING,
                             levelno=logging.WARNING,
                             log_command_error_output=True,
                             log_command_error_output=True,
@@ -149,7 +147,7 @@ def run_configuration(config_filename, config, arguments):
                     return
                     return
 
 
                 yield from log_error_records(
                 yield from log_error_records(
-                    '{}: Error running actions for repository'.format(repository['path']), error
+                    f'{repository["path"]}: Error running actions for repository', error
                 )
                 )
                 encountered_error = error
                 encountered_error = error
                 error_repository = repository['path']
                 error_repository = repository['path']
@@ -171,7 +169,7 @@ def run_configuration(config_filename, config, arguments):
             return
             return
 
 
         encountered_error = error
         encountered_error = error
-        yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
+        yield from log_error_records(f'{repository_path}: Error pinging monitor', error)
 
 
     if not encountered_error:
     if not encountered_error:
         try:
         try:
@@ -198,7 +196,7 @@ def run_configuration(config_filename, config, arguments):
                 return
                 return
 
 
             encountered_error = error
             encountered_error = error
-            yield from log_error_records('{}: Error pinging monitor'.format(config_filename), error)
+            yield from log_error_records(f'{config_filename}: Error pinging monitor', error)
 
 
     if encountered_error and using_primary_action:
     if encountered_error and using_primary_action:
         try:
         try:
@@ -233,9 +231,7 @@ def run_configuration(config_filename, config, arguments):
             if command.considered_soft_failure(config_filename, error):
             if command.considered_soft_failure(config_filename, error):
                 return
                 return
 
 
-            yield from log_error_records(
-                '{}: Error running on-error hook'.format(config_filename), error
-            )
+            yield from log_error_records(f'{config_filename}: Error running on-error hook', error)
 
 
 
 
 def run_actions(
 def run_actions(
@@ -476,9 +472,7 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
                         dict(
                         dict(
                             levelno=logging.WARNING,
                             levelno=logging.WARNING,
                             levelname='WARNING',
                             levelname='WARNING',
-                            msg='{}: Insufficient permissions to read configuration file'.format(
-                                config_filename
-                            ),
+                            msg=f'{config_filename}: Insufficient permissions to read configuration file',
                         )
                         )
                     ),
                     ),
                 ]
                 ]
@@ -490,7 +484,7 @@ def load_configurations(config_filenames, overrides=None, resolve_env=True):
                         dict(
                         dict(
                             levelno=logging.CRITICAL,
                             levelno=logging.CRITICAL,
                             levelname='CRITICAL',
                             levelname='CRITICAL',
-                            msg='{}: Error parsing configuration file'.format(config_filename),
+                            msg=f'{config_filename}: Error parsing configuration file',
                         )
                         )
                     ),
                     ),
                     logging.makeLogRecord(
                     logging.makeLogRecord(
@@ -591,9 +585,7 @@ def collect_configuration_run_summary_logs(configs, arguments):
 
 
     if not configs:
     if not configs:
         yield from log_error_records(
         yield from log_error_records(
-            '{}: No valid configuration files found'.format(
-                ' '.join(arguments['global'].config_paths)
-            )
+            r"{' '.join(arguments['global'].config_paths)}: No valid configuration files found",
         )
         )
         return
         return
 
 
@@ -619,23 +611,21 @@ def collect_configuration_run_summary_logs(configs, arguments):
         error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
         error_logs = tuple(result for result in results if isinstance(result, logging.LogRecord))
 
 
         if error_logs:
         if error_logs:
-            yield from log_error_records(
-                '{}: Error running configuration file'.format(config_filename)
-            )
+            yield from log_error_records(f'{config_filename}: An error occurred')
             yield from error_logs
             yield from error_logs
         else:
         else:
             yield logging.makeLogRecord(
             yield logging.makeLogRecord(
                 dict(
                 dict(
                     levelno=logging.INFO,
                     levelno=logging.INFO,
                     levelname='INFO',
                     levelname='INFO',
-                    msg='{}: Successfully ran configuration file'.format(config_filename),
+                    msg=f'{config_filename}: Successfully ran configuration file',
                 )
                 )
             )
             )
             if results:
             if results:
                 json_results.extend(results)
                 json_results.extend(results)
 
 
     if 'umount' in arguments:
     if 'umount' in arguments:
-        logger.info('Unmounting mount point {}'.format(arguments['umount'].mount_point))
+        logger.info(f"Unmounting mount point {arguments['umount'].mount_point}")
         try:
         try:
             borg_umount.unmount_archive(
             borg_umount.unmount_archive(
                 mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs),
                 mount_point=arguments['umount'].mount_point, local_path=get_local_path(configs),
@@ -683,7 +673,7 @@ def main():  # pragma: no cover
         if error.code == 0:
         if error.code == 0:
             raise error
             raise error
         configure_logging(logging.CRITICAL)
         configure_logging(logging.CRITICAL)
-        logger.critical('Error parsing arguments: {}'.format(' '.join(sys.argv)))
+        logger.critical(f"Error parsing arguments: {' '.join(sys.argv)}")
         exit_with_help_link()
         exit_with_help_link()
 
 
     global_arguments = arguments['global']
     global_arguments = arguments['global']
@@ -716,7 +706,7 @@ def main():  # pragma: no cover
         )
         )
     except (FileNotFoundError, PermissionError) as error:
     except (FileNotFoundError, PermissionError) as error:
         configure_logging(logging.CRITICAL)
         configure_logging(logging.CRITICAL)
-        logger.critical('Error configuring logging: {}'.format(error))
+        logger.critical(f'Error configuring logging: {error}')
         exit_with_help_link()
         exit_with_help_link()
 
 
     logger.debug('Ensuring legacy configuration is upgraded')
     logger.debug('Ensuring legacy configuration is upgraded')

+ 2 - 2
borgmatic/commands/completion.py

@@ -34,7 +34,7 @@ def bash_completion():
             '    local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
             '    local this_script="$(cat "$BASH_SOURCE" 2> /dev/null)"',
             '    local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
             '    local installed_script="$(borgmatic --bash-completion 2> /dev/null)"',
             '    if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
             '    if [ "$this_script" != "$installed_script" ] && [ "$installed_script" != "" ];'
-            '        then cat << EOF\n%s\nEOF' % UPGRADE_MESSAGE,
+            f'        then cat << EOF\n{UPGRADE_MESSAGE}\nEOF',
             '    fi',
             '    fi',
             '}',
             '}',
             'complete_borgmatic() {',
             'complete_borgmatic() {',
@@ -48,7 +48,7 @@ def bash_completion():
             for action, subparser in subparsers.choices.items()
             for action, subparser in subparsers.choices.items()
         )
         )
         + (
         + (
-            '    COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))'
+            '    COMPREPLY=($(compgen -W "%s %s" -- "${COMP_WORDS[COMP_CWORD]}"))'  # noqa: FS003
             % (actions, global_flags),
             % (actions, global_flags),
             '    (check_version &)',
             '    (check_version &)',
             '}',
             '}',

+ 7 - 15
borgmatic/commands/convert_config.py

@@ -28,9 +28,7 @@ def parse_arguments(*arguments):
         '--source-config',
         '--source-config',
         dest='source_config_filename',
         dest='source_config_filename',
         default=DEFAULT_SOURCE_CONFIG_FILENAME,
         default=DEFAULT_SOURCE_CONFIG_FILENAME,
-        help='Source INI-style configuration filename. Default: {}'.format(
-            DEFAULT_SOURCE_CONFIG_FILENAME
-        ),
+        help=f'Source INI-style configuration filename. Default: {DEFAULT_SOURCE_CONFIG_FILENAME}',
     )
     )
     parser.add_argument(
     parser.add_argument(
         '-e',
         '-e',
@@ -46,9 +44,7 @@ def parse_arguments(*arguments):
         '--destination-config',
         '--destination-config',
         dest='destination_config_filename',
         dest='destination_config_filename',
         default=DEFAULT_DESTINATION_CONFIG_FILENAME,
         default=DEFAULT_DESTINATION_CONFIG_FILENAME,
-        help='Destination YAML configuration filename. Default: {}'.format(
-            DEFAULT_DESTINATION_CONFIG_FILENAME
-        ),
+        help=f'Destination YAML configuration filename. Default: {DEFAULT_DESTINATION_CONFIG_FILENAME}',
     )
     )
 
 
     return parser.parse_args(arguments)
     return parser.parse_args(arguments)
@@ -59,19 +55,15 @@ TEXT_WRAP_CHARACTERS = 80
 
 
 def display_result(args):  # pragma: no cover
 def display_result(args):  # pragma: no cover
     result_lines = textwrap.wrap(
     result_lines = textwrap.wrap(
-        'Your borgmatic configuration has been upgraded. Please review the result in {}.'.format(
-            args.destination_config_filename
-        ),
+        f'Your borgmatic configuration has been upgraded. Please review the result in {args.destination_config_filename}.',
         TEXT_WRAP_CHARACTERS,
         TEXT_WRAP_CHARACTERS,
     )
     )
 
 
+    excludes_phrase = (
+        f' and {args.source_excludes_filename}' if args.source_excludes_filename else ''
+    )
     delete_lines = textwrap.wrap(
     delete_lines = textwrap.wrap(
-        'Once you are satisfied, you can safely delete {}{}.'.format(
-            args.source_config_filename,
-            ' and {}'.format(args.source_excludes_filename)
-            if args.source_excludes_filename
-            else '',
-        ),
+        f'Once you are satisfied, you can safely delete {args.source_config_filename}{excludes_phrase}.',
         TEXT_WRAP_CHARACTERS,
         TEXT_WRAP_CHARACTERS,
     )
     )
 
 

+ 4 - 10
borgmatic/commands/generate_config.py

@@ -23,9 +23,7 @@ def parse_arguments(*arguments):
         '--destination',
         '--destination',
         dest='destination_filename',
         dest='destination_filename',
         default=DEFAULT_DESTINATION_CONFIG_FILENAME,
         default=DEFAULT_DESTINATION_CONFIG_FILENAME,
-        help='Destination YAML configuration file, default: {}'.format(
-            DEFAULT_DESTINATION_CONFIG_FILENAME
-        ),
+        help=f'Destination YAML configuration file, default: {DEFAULT_DESTINATION_CONFIG_FILENAME}',
     )
     )
     parser.add_argument(
     parser.add_argument(
         '--overwrite',
         '--overwrite',
@@ -48,17 +46,13 @@ def main():  # pragma: no cover
             overwrite=args.overwrite,
             overwrite=args.overwrite,
         )
         )
 
 
-        print('Generated a sample configuration file at {}.'.format(args.destination_filename))
+        print(f'Generated a sample configuration file at {args.destination_filename}.')
         print()
         print()
         if args.source_filename:
         if args.source_filename:
-            print(
-                'Merged in the contents of configuration file at {}.'.format(args.source_filename)
-            )
+            print(f'Merged in the contents of configuration file at {args.source_filename}.')
             print('To review the changes made, run:')
             print('To review the changes made, run:')
             print()
             print()
-            print(
-                '    diff --unified {} {}'.format(args.source_filename, args.destination_filename)
-            )
+            print(f'    diff --unified {args.source_filename} {args.destination_filename}')
             print()
             print()
         print('This includes all available configuration options with example values. The few')
         print('This includes all available configuration options with example values. The few')
         print('required options are indicated. Please edit the file to suit your needs.')
         print('required options are indicated. Please edit the file to suit your needs.')

+ 3 - 7
borgmatic/commands/validate_config.py

@@ -21,9 +21,7 @@ def parse_arguments(*arguments):
         nargs='+',
         nargs='+',
         dest='config_paths',
         dest='config_paths',
         default=config_paths,
         default=config_paths,
-        help='Configuration filenames or directories, defaults to: {}'.format(
-            ' '.join(config_paths)
-        ),
+        help=f'Configuration filenames or directories, defaults to: {config_paths}',
     )
     )
 
 
     return parser.parse_args(arguments)
     return parser.parse_args(arguments)
@@ -44,13 +42,11 @@ def main():  # pragma: no cover
         try:
         try:
             validate.parse_configuration(config_filename, validate.schema_filename())
             validate.parse_configuration(config_filename, validate.schema_filename())
         except (ValueError, OSError, validate.Validation_error) as error:
         except (ValueError, OSError, validate.Validation_error) as error:
-            logging.critical('{}: Error parsing configuration file'.format(config_filename))
+            logging.critical(f'{config_filename}: Error parsing configuration file')
             logging.critical(error)
             logging.critical(error)
             found_issues = True
             found_issues = True
 
 
     if found_issues:
     if found_issues:
         sys.exit(1)
         sys.exit(1)
     else:
     else:
-        logger.info(
-            'All given configuration files are valid: {}'.format(', '.join(config_filenames))
-        )
+        logger.info(f"All given configuration files are valid: {', '.join(config_filenames)}")

+ 2 - 2
borgmatic/config/collect.py

@@ -16,8 +16,8 @@ def get_default_config_paths(expand_home=True):
     return [
     return [
         '/etc/borgmatic/config.yaml',
         '/etc/borgmatic/config.yaml',
         '/etc/borgmatic.d',
         '/etc/borgmatic.d',
-        '%s/borgmatic/config.yaml' % user_config_directory,
-        '%s/borgmatic.d' % user_config_directory,
+        os.path.join(user_config_directory, 'borgmatic/config.yaml'),
+        os.path.join(user_config_directory, 'borgmatic.d'),
     ]
     ]
 
 
 
 

+ 4 - 1
borgmatic/config/environment.py

@@ -14,11 +14,14 @@ def _resolve_string(matcher):
     if matcher.group('escape') is not None:
     if matcher.group('escape') is not None:
         # in case of escaped envvar, unescape it
         # in case of escaped envvar, unescape it
         return matcher.group('variable')
         return matcher.group('variable')
+
     # resolve the env var
     # resolve the env var
     name, default = matcher.group('name'), matcher.group('default')
     name, default = matcher.group('name'), matcher.group('default')
     out = os.getenv(name, default=default)
     out = os.getenv(name, default=default)
+
     if out is None:
     if out is None:
-        raise ValueError('Cannot find variable ${name} in environment'.format(name=name))
+        raise ValueError(f'Cannot find variable {name} in environment')
+
     return out
     return out
 
 
 
 

+ 4 - 6
borgmatic/config/generate.py

@@ -48,7 +48,7 @@ def _schema_to_sample_configuration(schema, level=0, parent_is_sequence=False):
             config, schema, indent=indent, skip_first=parent_is_sequence
             config, schema, indent=indent, skip_first=parent_is_sequence
         )
         )
     else:
     else:
-        raise ValueError('Schema at level {} is unsupported: {}'.format(level, schema))
+        raise ValueError(f'Schema at level {level} is unsupported: {schema}')
 
 
     return config
     return config
 
 
@@ -84,7 +84,7 @@ def _comment_out_optional_configuration(rendered_config):
     for line in rendered_config.split('\n'):
     for line in rendered_config.split('\n'):
         # Upon encountering an optional configuration option, comment out lines until the next blank
         # Upon encountering an optional configuration option, comment out lines until the next blank
         # line.
         # line.
-        if line.strip().startswith('# {}'.format(COMMENTED_OUT_SENTINEL)):
+        if line.strip().startswith(f'# {COMMENTED_OUT_SENTINEL}'):
             optional = True
             optional = True
             continue
             continue
 
 
@@ -117,9 +117,7 @@ def write_configuration(config_filename, rendered_config, mode=0o600, overwrite=
     '''
     '''
     if not overwrite and os.path.exists(config_filename):
     if not overwrite and os.path.exists(config_filename):
         raise FileExistsError(
         raise FileExistsError(
-            '{} already exists. Aborting. Use --overwrite to replace the file.'.format(
-                config_filename
-            )
+            f'{config_filename} already exists. Aborting. Use --overwrite to replace the file.'
         )
         )
 
 
     try:
     try:
@@ -218,7 +216,7 @@ def remove_commented_out_sentinel(config, field_name):
     except KeyError:
     except KeyError:
         return
         return
 
 
-    if last_comment_value == '# {}\n'.format(COMMENTED_OUT_SENTINEL):
+    if last_comment_value == f'# {COMMENTED_OUT_SENTINEL}\n':
         config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
         config.ca.items[field_name][RUAMEL_YAML_COMMENTS_INDEX].pop()
 
 
 
 

+ 5 - 11
borgmatic/config/legacy.py

@@ -70,13 +70,11 @@ def validate_configuration_format(parser, config_format):
         section_format.name for section_format in config_format
         section_format.name for section_format in config_format
     )
     )
     if unknown_section_names:
     if unknown_section_names:
-        raise ValueError(
-            'Unknown config sections found: {}'.format(', '.join(unknown_section_names))
-        )
+        raise ValueError(f"Unknown config sections found: {', '.join(unknown_section_names)}")
 
 
     missing_section_names = set(required_section_names) - section_names
     missing_section_names = set(required_section_names) - section_names
     if missing_section_names:
     if missing_section_names:
-        raise ValueError('Missing config sections: {}'.format(', '.join(missing_section_names)))
+        raise ValueError(f"Missing config sections: {', '.join(missing_section_names)}")
 
 
     for section_format in config_format:
     for section_format in config_format:
         if section_format.name not in section_names:
         if section_format.name not in section_names:
@@ -91,9 +89,7 @@ def validate_configuration_format(parser, config_format):
 
 
         if unexpected_option_names:
         if unexpected_option_names:
             raise ValueError(
             raise ValueError(
-                'Unexpected options found in config section {}: {}'.format(
-                    section_format.name, ', '.join(sorted(unexpected_option_names))
-                )
+                f"Unexpected options found in config section {section_format.name}: {', '.join(sorted(unexpected_option_names))}",
             )
             )
 
 
         missing_option_names = tuple(
         missing_option_names = tuple(
@@ -105,9 +101,7 @@ def validate_configuration_format(parser, config_format):
 
 
         if missing_option_names:
         if missing_option_names:
             raise ValueError(
             raise ValueError(
-                'Required options missing from config section {}: {}'.format(
-                    section_format.name, ', '.join(missing_option_names)
-                )
+                f"Required options missing from config section {section_format.name}: {', '.join(missing_option_names)}",
             )
             )
 
 
 
 
@@ -137,7 +131,7 @@ def parse_configuration(config_filename, config_format):
     '''
     '''
     parser = RawConfigParser()
     parser = RawConfigParser()
     if not parser.read(config_filename):
     if not parser.read(config_filename):
-        raise ValueError('Configuration file cannot be opened: {}'.format(config_filename))
+        raise ValueError(f'Configuration file cannot be opened: {config_filename}')
 
 
     validate_configuration_format(parser, config_format)
     validate_configuration_format(parser, config_format)
 
 

+ 11 - 12
borgmatic/config/validate.py

@@ -20,9 +20,9 @@ def format_json_error_path_element(path_element):
     Given a path element into a JSON data structure, format it for display as a string.
     Given a path element into a JSON data structure, format it for display as a string.
     '''
     '''
     if isinstance(path_element, int):
     if isinstance(path_element, int):
-        return str('[{}]'.format(path_element))
+        return str(f'[{path_element}]')
 
 
-    return str('.{}'.format(path_element))
+    return str(f'.{path_element}')
 
 
 
 
 def format_json_error(error):
 def format_json_error(error):
@@ -30,10 +30,10 @@ def format_json_error(error):
     Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string.
     Given an instance of jsonschema.exceptions.ValidationError, format it for display as a string.
     '''
     '''
     if not error.path:
     if not error.path:
-        return 'At the top level: {}'.format(error.message)
+        return f'At the top level: {error.message}'
 
 
     formatted_path = ''.join(format_json_error_path_element(element) for element in error.path)
     formatted_path = ''.join(format_json_error_path_element(element) for element in error.path)
-    return "At '{}': {}".format(formatted_path.lstrip('.'), error.message)
+    return f"At '{formatted_path.lstrip('.')}': {error.message}"
 
 
 
 
 class Validation_error(ValueError):
 class Validation_error(ValueError):
@@ -54,9 +54,10 @@ class Validation_error(ValueError):
         '''
         '''
         Render a validation error as a user-facing string.
         Render a validation error as a user-facing string.
         '''
         '''
-        return 'An error occurred while parsing a configuration file at {}:\n'.format(
-            self.config_filename
-        ) + '\n'.join(error for error in self.errors)
+        return (
+            f'An error occurred while parsing a configuration file at {self.config_filename}:\n'
+            + '\n'.join(error for error in self.errors)
+        )
 
 
 
 
 def apply_logical_validation(config_filename, parsed_configuration):
 def apply_logical_validation(config_filename, parsed_configuration):
@@ -72,9 +73,7 @@ def apply_logical_validation(config_filename, parsed_configuration):
             raise Validation_error(
             raise Validation_error(
                 config_filename,
                 config_filename,
                 (
                 (
-                    'Unknown repository in the "consistency" section\'s "check_repositories": {}'.format(
-                        repository
-                    ),
+                    f'Unknown repository in the "consistency" section\'s "check_repositories": {repository}',
                 ),
                 ),
             )
             )
 
 
@@ -173,9 +172,9 @@ def guard_configuration_contains_repository(repository, configurations):
     )
     )
 
 
     if count == 0:
     if count == 0:
-        raise ValueError('Repository {} not found in configuration files'.format(repository))
+        raise ValueError(f'Repository {repository} not found in configuration files')
     if count > 1:
     if count > 1:
-        raise ValueError('Repository {} found in multiple configuration files'.format(repository))
+        raise ValueError(f'Repository {repository} found in multiple configuration files')
 
 
 
 
 def guard_single_repository_selected(repository, configurations):
 def guard_single_repository_selected(repository, configurations):

+ 19 - 13
borgmatic/execute.py

@@ -11,7 +11,7 @@ ERROR_OUTPUT_MAX_LINE_COUNT = 25
 BORG_ERROR_EXIT_CODE = 2
 BORG_ERROR_EXIT_CODE = 2
 
 
 
 
-def exit_code_indicates_error(process, exit_code, borg_local_path=None):
+def exit_code_indicates_error(command, exit_code, borg_local_path=None):
     '''
     '''
     Return True if the given exit code from running a command corresponds to an error. If a Borg
     Return True if the given exit code from running a command corresponds to an error. If a Borg
     local path is given and matches the process' command, then treat exit code 1 as a warning
     local path is given and matches the process' command, then treat exit code 1 as a warning
@@ -20,8 +20,6 @@ def exit_code_indicates_error(process, exit_code, borg_local_path=None):
     if exit_code is None:
     if exit_code is None:
         return False
         return False
 
 
-    command = process.args.split(' ') if isinstance(process.args, str) else process.args
-
     if borg_local_path and command[0] == borg_local_path:
     if borg_local_path and command[0] == borg_local_path:
         return bool(exit_code < 0 or exit_code >= BORG_ERROR_EXIT_CODE)
         return bool(exit_code < 0 or exit_code >= BORG_ERROR_EXIT_CODE)
 
 
@@ -121,8 +119,9 @@ def log_outputs(processes, exclude_stdouts, output_log_level, borg_local_path):
             if exit_code is None:
             if exit_code is None:
                 still_running = True
                 still_running = True
 
 
+            command = process.args.split(' ') if isinstance(process.args, str) else process.args
             # If any process errors, then raise accordingly.
             # If any process errors, then raise accordingly.
-            if exit_code_indicates_error(process, exit_code, borg_local_path):
+            if exit_code_indicates_error(command, exit_code, borg_local_path):
                 # If an error occurs, include its output in the raised exception so that we don't
                 # If an error occurs, include its output in the raised exception so that we don't
                 # inadvertently hide error output.
                 # inadvertently hide error output.
                 output_buffer = output_buffer_for_process(process, exclude_stdouts)
                 output_buffer = output_buffer_for_process(process, exclude_stdouts)
@@ -155,8 +154,8 @@ def log_command(full_command, input_file=None, output_file=None):
     '''
     '''
     logger.debug(
     logger.debug(
         ' '.join(full_command)
         ' '.join(full_command)
-        + (' < {}'.format(getattr(input_file, 'name', '')) if input_file else '')
-        + (' > {}'.format(getattr(output_file, 'name', '')) if output_file else '')
+        + (f" < {getattr(input_file, 'name', '')}" if input_file else '')
+        + (f" > {getattr(output_file, 'name', '')}" if output_file else '')
     )
     )
 
 
 
 
@@ -228,13 +227,20 @@ def execute_command_and_capture_output(
     environment = {**os.environ, **extra_environment} if extra_environment else None
     environment = {**os.environ, **extra_environment} if extra_environment else None
     command = ' '.join(full_command) if shell else full_command
     command = ' '.join(full_command) if shell else full_command
 
 
-    output = subprocess.check_output(
-        command,
-        stderr=subprocess.STDOUT if capture_stderr else None,
-        shell=shell,
-        env=environment,
-        cwd=working_directory,
-    )
+    try:
+        output = subprocess.check_output(
+            command,
+            stderr=subprocess.STDOUT if capture_stderr else None,
+            shell=shell,
+            env=environment,
+            cwd=working_directory,
+        )
+        logger.warning(f'Command output: {output}')
+    except subprocess.CalledProcessError as error:
+        if exit_code_indicates_error(command, error.returncode):
+            raise
+        output = error.output
+        logger.warning(f'Command output: {output}')
 
 
     return output.decode() if output is not None else None
     return output.decode() if output is not None else None
 
 

+ 6 - 12
borgmatic/hooks/command.py

@@ -16,7 +16,7 @@ def interpolate_context(config_filename, hook_description, command, context):
     names/values, interpolate the values by "{name}" into the command and return the result.
     names/values, interpolate the values by "{name}" into the command and return the result.
     '''
     '''
     for name, value in context.items():
     for name, value in context.items():
-        command = command.replace('{%s}' % name, str(value))
+        command = command.replace(f'{{{name}}}', str(value))
 
 
     for unsupported_variable in re.findall(r'{\w+}', command):
     for unsupported_variable in re.findall(r'{\w+}', command):
         logger.warning(
         logger.warning(
@@ -38,7 +38,7 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
     Raise subprocesses.CalledProcessError if an error occurs in a hook.
     Raise subprocesses.CalledProcessError if an error occurs in a hook.
     '''
     '''
     if not commands:
     if not commands:
-        logger.debug('{}: No commands to run for {} hook'.format(config_filename, description))
+        logger.debug(f'{config_filename}: No commands to run for {description} hook')
         return
         return
 
 
     dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
     dry_run_label = ' (dry run; not actually running hooks)' if dry_run else ''
@@ -49,19 +49,15 @@ def execute_hook(commands, umask, config_filename, description, dry_run, **conte
     ]
     ]
 
 
     if len(commands) == 1:
     if len(commands) == 1:
-        logger.info(
-            '{}: Running command for {} hook{}'.format(config_filename, description, dry_run_label)
-        )
+        logger.info(f'{config_filename}: Running command for {description} hook{dry_run_label}')
     else:
     else:
         logger.info(
         logger.info(
-            '{}: Running {} commands for {} hook{}'.format(
-                config_filename, len(commands), description, dry_run_label
-            )
+            f'{config_filename}: Running {len(commands)} commands for {description} hook{dry_run_label}',
         )
         )
 
 
     if umask:
     if umask:
         parsed_umask = int(str(umask), 8)
         parsed_umask = int(str(umask), 8)
-        logger.debug('{}: Set hook umask to {}'.format(config_filename, oct(parsed_umask)))
+        logger.debug(f'{config_filename}: Set hook umask to {oct(parsed_umask)}')
         original_umask = os.umask(parsed_umask)
         original_umask = os.umask(parsed_umask)
     else:
     else:
         original_umask = None
         original_umask = None
@@ -93,9 +89,7 @@ def considered_soft_failure(config_filename, error):
 
 
     if exit_code == SOFT_FAIL_EXIT_CODE:
     if exit_code == SOFT_FAIL_EXIT_CODE:
         logger.info(
         logger.info(
-            '{}: Command hook exited with soft failure exit code ({}); skipping remaining actions'.format(
-                config_filename, SOFT_FAIL_EXIT_CODE
-            )
+            f'{config_filename}: Command hook exited with soft failure exit code ({SOFT_FAIL_EXIT_CODE}); skipping remaining actions',
         )
         )
         return True
         return True
 
 

+ 3 - 5
borgmatic/hooks/cronhub.py

@@ -34,17 +34,15 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
         return
         return
 
 
     dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
     dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
-    formatted_state = '/{}/'.format(MONITOR_STATE_TO_CRONHUB[state])
+    formatted_state = f'/{MONITOR_STATE_TO_CRONHUB[state]}/'
     ping_url = (
     ping_url = (
         hook_config['ping_url']
         hook_config['ping_url']
         .replace('/start/', formatted_state)
         .replace('/start/', formatted_state)
         .replace('/ping/', formatted_state)
         .replace('/ping/', formatted_state)
     )
     )
 
 
-    logger.info(
-        '{}: Pinging Cronhub {}{}'.format(config_filename, state.name.lower(), dry_run_label)
-    )
-    logger.debug('{}: Using Cronhub ping URL {}'.format(config_filename, ping_url))
+    logger.info(f'{config_filename}: Pinging Cronhub {state.name.lower()}{dry_run_label}')
+    logger.debug(f'{config_filename}: Using Cronhub ping URL {ping_url}')
 
 
     if not dry_run:
     if not dry_run:
         logging.getLogger('urllib3').setLevel(logging.ERROR)
         logging.getLogger('urllib3').setLevel(logging.ERROR)

+ 3 - 5
borgmatic/hooks/cronitor.py

@@ -34,12 +34,10 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
         return
         return
 
 
     dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
     dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
-    ping_url = '{}/{}'.format(hook_config['ping_url'], MONITOR_STATE_TO_CRONITOR[state])
+    ping_url = f"{hook_config['ping_url']}/{MONITOR_STATE_TO_CRONITOR[state]}"
 
 
-    logger.info(
-        '{}: Pinging Cronitor {}{}'.format(config_filename, state.name.lower(), dry_run_label)
-    )
-    logger.debug('{}: Using Cronitor ping URL {}'.format(config_filename, ping_url))
+    logger.info(f'{config_filename}: Pinging Cronitor {state.name.lower()}{dry_run_label}')
+    logger.debug(f'{config_filename}: Using Cronitor ping URL {ping_url}')
 
 
     if not dry_run:
     if not dry_run:
         logging.getLogger('urllib3').setLevel(logging.ERROR)
         logging.getLogger('urllib3').setLevel(logging.ERROR)

+ 2 - 2
borgmatic/hooks/dispatch.py

@@ -43,9 +43,9 @@ def call_hook(function_name, hooks, log_prefix, hook_name, *args, **kwargs):
     try:
     try:
         module = HOOK_NAME_TO_MODULE[hook_name]
         module = HOOK_NAME_TO_MODULE[hook_name]
     except KeyError:
     except KeyError:
-        raise ValueError('Unknown hook name: {}'.format(hook_name))
+        raise ValueError(f'Unknown hook name: {hook_name}')
 
 
-    logger.debug('{}: Calling {} hook function {}'.format(log_prefix, hook_name, function_name))
+    logger.debug(f'{log_prefix}: Calling {hook_name} hook function {function_name}')
     return getattr(module, function_name)(config, log_prefix, *args, **kwargs)
     return getattr(module, function_name)(config, log_prefix, *args, **kwargs)
 
 
 
 

+ 3 - 5
borgmatic/hooks/dump.py

@@ -33,7 +33,7 @@ def make_database_dump_filename(dump_path, name, hostname=None):
     Raise ValueError if the database name is invalid.
     Raise ValueError if the database name is invalid.
     '''
     '''
     if os.path.sep in name:
     if os.path.sep in name:
-        raise ValueError('Invalid database name {}'.format(name))
+        raise ValueError(f'Invalid database name {name}')
 
 
     return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
     return os.path.join(os.path.expanduser(dump_path), hostname or 'localhost', name)
 
 
@@ -60,9 +60,7 @@ def remove_database_dumps(dump_path, database_type_name, log_prefix, dry_run):
     '''
     '''
     dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
     dry_run_label = ' (dry run; not actually removing anything)' if dry_run else ''
 
 
-    logger.debug(
-        '{}: Removing {} database dumps{}'.format(log_prefix, database_type_name, dry_run_label)
-    )
+    logger.debug(f'{log_prefix}: Removing {database_type_name} database dumps{dry_run_label}')
 
 
     expanded_path = os.path.expanduser(dump_path)
     expanded_path = os.path.expanduser(dump_path)
 
 
@@ -78,4 +76,4 @@ def convert_glob_patterns_to_borg_patterns(patterns):
     Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
     Convert a sequence of shell glob patterns like "/etc/*" to the corresponding Borg archive
     patterns like "sh:etc/*".
     patterns like "sh:etc/*".
     '''
     '''
-    return ['sh:{}'.format(pattern.lstrip(os.path.sep)) for pattern in patterns]
+    return [f'sh:{pattern.lstrip(os.path.sep)}' for pattern in patterns]

+ 4 - 6
borgmatic/hooks/healthchecks.py

@@ -99,7 +99,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
     ping_url = (
     ping_url = (
         hook_config['ping_url']
         hook_config['ping_url']
         if hook_config['ping_url'].startswith('http')
         if hook_config['ping_url'].startswith('http')
-        else 'https://hc-ping.com/{}'.format(hook_config['ping_url'])
+        else f"https://hc-ping.com/{hook_config['ping_url']}"
     )
     )
     dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
     dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
 
 
@@ -111,12 +111,10 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
 
 
     healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
     healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
     if healthchecks_state:
     if healthchecks_state:
-        ping_url = '{}/{}'.format(ping_url, healthchecks_state)
+        ping_url = f'{ping_url}/{healthchecks_state}'
 
 
-    logger.info(
-        '{}: Pinging Healthchecks {}{}'.format(config_filename, state.name.lower(), dry_run_label)
-    )
-    logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
+    logger.info(f'{config_filename}: Pinging Healthchecks {state.name.lower()}{dry_run_label}')
+    logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
 
 
     if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
     if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
         payload = format_buffered_logs_for_payload()
         payload = format_buffered_logs_for_payload()

+ 3 - 7
borgmatic/hooks/mongodb.py

@@ -27,7 +27,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
     '''
     '''
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
 
 
-    logger.info('{}: Dumping MongoDB databases{}'.format(log_prefix, dry_run_label))
+    logger.info(f'{log_prefix}: Dumping MongoDB databases{dry_run_label}')
 
 
     processes = []
     processes = []
     for database in databases:
     for database in databases:
@@ -38,9 +38,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
         dump_format = database.get('format', 'archive')
         dump_format = database.get('format', 'archive')
 
 
         logger.debug(
         logger.debug(
-            '{}: Dumping MongoDB database {} to {}{}'.format(
-                log_prefix, name, dump_filename, dry_run_label
-            )
+            f'{log_prefix}: Dumping MongoDB database {name} to {dump_filename}{dry_run_label}',
         )
         )
         if dry_run:
         if dry_run:
             continue
             continue
@@ -126,9 +124,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
     )
     )
     restore_command = build_restore_command(extract_process, database, dump_filename)
     restore_command = build_restore_command(extract_process, database, dump_filename)
 
 
-    logger.debug(
-        '{}: Restoring MongoDB database {}{}'.format(log_prefix, database['name'], dry_run_label)
-    )
+    logger.debug(f"{log_prefix}: Restoring MongoDB database {database['name']}{dry_run_label}")
     if dry_run:
     if dry_run:
         return
         return
 
 

+ 2 - 4
borgmatic/hooks/mysql.py

@@ -119,7 +119,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     processes = []
     processes = []
 
 
-    logger.info('{}: Dumping MySQL databases{}'.format(log_prefix, dry_run_label))
+    logger.info(f'{log_prefix}: Dumping MySQL databases{dry_run_label}')
 
 
     for database in databases:
     for database in databases:
         dump_path = make_dump_path(location_config)
         dump_path = make_dump_path(location_config)
@@ -209,9 +209,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
     )
     )
     extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
     extra_environment = {'MYSQL_PWD': database['password']} if 'password' in database else None
 
 
-    logger.debug(
-        '{}: Restoring MySQL database {}{}'.format(log_prefix, database['name'], dry_run_label)
-    )
+    logger.debug(f"{log_prefix}: Restoring MySQL database {database['name']}{dry_run_label}")
     if dry_run:
     if dry_run:
         return
         return
 
 

+ 4 - 6
borgmatic/hooks/pagerduty.py

@@ -29,14 +29,12 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
     '''
     '''
     if state != monitor.State.FAIL:
     if state != monitor.State.FAIL:
         logger.debug(
         logger.debug(
-            '{}: Ignoring unsupported monitoring {} in PagerDuty hook'.format(
-                config_filename, state.name.lower()
-            )
+            f'{config_filename}: Ignoring unsupported monitoring {state.name.lower()} in PagerDuty hook',
         )
         )
         return
         return
 
 
     dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
     dry_run_label = ' (dry run; not actually sending)' if dry_run else ''
-    logger.info('{}: Sending failure event to PagerDuty {}'.format(config_filename, dry_run_label))
+    logger.info(f'{config_filename}: Sending failure event to PagerDuty {dry_run_label}')
 
 
     if dry_run:
     if dry_run:
         return
         return
@@ -50,7 +48,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
             'routing_key': hook_config['integration_key'],
             'routing_key': hook_config['integration_key'],
             'event_action': 'trigger',
             'event_action': 'trigger',
             'payload': {
             'payload': {
-                'summary': 'backup failed on {}'.format(hostname),
+                'summary': f'backup failed on {hostname}',
                 'severity': 'error',
                 'severity': 'error',
                 'source': hostname,
                 'source': hostname,
                 'timestamp': local_timestamp,
                 'timestamp': local_timestamp,
@@ -65,7 +63,7 @@ def ping_monitor(hook_config, config_filename, state, monitoring_log_level, dry_
             },
             },
         }
         }
     )
     )
-    logger.debug('{}: Using PagerDuty payload: {}'.format(config_filename, payload))
+    logger.debug(f'{config_filename}: Using PagerDuty payload: {payload}')
 
 
     logging.getLogger('urllib3').setLevel(logging.ERROR)
     logging.getLogger('urllib3').setLevel(logging.ERROR)
     try:
     try:

+ 2 - 4
borgmatic/hooks/postgresql.py

@@ -93,7 +93,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     processes = []
     processes = []
 
 
-    logger.info('{}: Dumping PostgreSQL databases{}'.format(log_prefix, dry_run_label))
+    logger.info(f'{log_prefix}: Dumping PostgreSQL databases{dry_run_label}')
 
 
     for database in databases:
     for database in databases:
         extra_environment = make_extra_environment(database)
         extra_environment = make_extra_environment(database)
@@ -228,9 +228,7 @@ def restore_database_dump(database_config, log_prefix, location_config, dry_run,
     )
     )
     extra_environment = make_extra_environment(database)
     extra_environment = make_extra_environment(database)
 
 
-    logger.debug(
-        '{}: Restoring PostgreSQL database {}{}'.format(log_prefix, database['name'], dry_run_label)
-    )
+    logger.debug(f"{log_prefix}: Restoring PostgreSQL database {database['name']}{dry_run_label}")
     if dry_run:
     if dry_run:
         return
         return
 
 

+ 1 - 1
borgmatic/hooks/sqlite.py

@@ -26,7 +26,7 @@ def dump_databases(databases, log_prefix, location_config, dry_run):
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     dry_run_label = ' (dry run; not actually dumping anything)' if dry_run else ''
     processes = []
     processes = []
 
 
-    logger.info('{}: Dumping SQLite databases{}'.format(log_prefix, dry_run_label))
+    logger.info(f'{log_prefix}: Dumping SQLite databases{dry_run_label}')
 
 
     for database in databases:
     for database in databases:
         database_path = database['path']
         database_path = database['path']

+ 1 - 1
borgmatic/logger.py

@@ -108,7 +108,7 @@ def color_text(color, message):
     if not color:
     if not color:
         return message
         return message
 
 
-    return '{}{}{}'.format(color, message, colorama.Style.RESET_ALL)
+    return f'{color}{message}{colorama.Style.RESET_ALL}'
 
 
 
 
 def add_logging_level(level_name, level_number):
 def add_logging_level(level_name, level_number):

+ 1 - 0
docs/Dockerfile

@@ -18,6 +18,7 @@ RUN npm install @11ty/eleventy \
     @11ty/eleventy-plugin-syntaxhighlight \
     @11ty/eleventy-plugin-syntaxhighlight \
     @11ty/eleventy-plugin-inclusive-language \
     @11ty/eleventy-plugin-inclusive-language \
     @11ty/eleventy-navigation \
     @11ty/eleventy-navigation \
+    eleventy-plugin-code-clipboard \
     markdown-it \
     markdown-it \
     markdown-it-anchor \
     markdown-it-anchor \
     markdown-it-replace-link
     markdown-it-replace-link

+ 15 - 0
docs/_includes/index.css

@@ -533,3 +533,18 @@ main .elv-toc + h1 .direct-link {
 .header-anchor:hover::after {
 .header-anchor:hover::after {
     content: " 🔗";
     content: " 🔗";
 }
 }
+
+.mdi {
+    display: inline-block;
+    width: 1em;
+    height: 1em;
+    background-color: currentColor;
+    -webkit-mask: no-repeat center / 100%;
+    mask: no-repeat center / 100%;
+    -webkit-mask-image: var(--svg);
+    mask-image: var(--svg);
+}
+
+.mdi.mdi-content-copy {
+    --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M19 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1Z'/%3E%3C/svg%3E");
+}

+ 1 - 1
docs/_includes/layouts/base.njk

@@ -22,6 +22,6 @@
 	<body>
 	<body>
 
 
 		{{ content | safe }}
 		{{ content | safe }}
-
+		{% initClipboardJS %}
 	</body>
 	</body>
 </html>
 </html>

+ 2 - 0
test_requirements.txt

@@ -6,6 +6,8 @@ colorama==0.4.4
 coverage==5.3
 coverage==5.3
 flake8==4.0.1
 flake8==4.0.1
 flake8-quotes==3.3.2
 flake8-quotes==3.3.2
+flake8-use-fstring==1.4
+flake8-variables-names==0.0.5
 flexmock==0.10.4
 flexmock==0.10.4
 isort==5.9.1
 isort==5.9.1
 mccabe==0.6.1
 mccabe==0.6.1

+ 6 - 10
tests/end-to-end/test_borgmatic.py

@@ -12,9 +12,7 @@ def generate_configuration(config_path, repository_path):
     to work for testing (including injecting the given repository path and tacking on an encryption
     to work for testing (including injecting the given repository path and tacking on an encryption
     passphrase).
     passphrase).
     '''
     '''
-    subprocess.check_call(
-        'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
-    )
+    subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
     config = (
     config = (
         open(config_path)
         open(config_path)
         .read()
         .read()
@@ -46,13 +44,13 @@ def test_borgmatic_command():
         generate_configuration(config_path, repository_path)
         generate_configuration(config_path, repository_path)
 
 
         subprocess.check_call(
         subprocess.check_call(
-            'borgmatic -v 2 --config {} init --encryption repokey'.format(config_path).split(' ')
+            f'borgmatic -v 2 --config {config_path} init --encryption repokey'.split(' ')
         )
         )
 
 
         # Run borgmatic to generate a backup archive, and then list it to make sure it exists.
         # Run borgmatic to generate a backup archive, and then list it to make sure it exists.
-        subprocess.check_call('borgmatic --config {}'.format(config_path).split(' '))
+        subprocess.check_call(f'borgmatic --config {config_path}'.split(' '))
         output = subprocess.check_output(
         output = subprocess.check_output(
-            'borgmatic --config {} list --json'.format(config_path).split(' ')
+            f'borgmatic --config {config_path} list --json'.split(' ')
         ).decode(sys.stdout.encoding)
         ).decode(sys.stdout.encoding)
         parsed_output = json.loads(output)
         parsed_output = json.loads(output)
 
 
@@ -63,16 +61,14 @@ def test_borgmatic_command():
         # Extract the created archive into the current (temporary) directory, and confirm that the
         # Extract the created archive into the current (temporary) directory, and confirm that the
         # extracted file looks right.
         # extracted file looks right.
         output = subprocess.check_output(
         output = subprocess.check_output(
-            'borgmatic --config {} extract --archive {}'.format(config_path, archive_name).split(
-                ' '
-            )
+            f'borgmatic --config {config_path} extract --archive {archive_name}'.split(' '),
         ).decode(sys.stdout.encoding)
         ).decode(sys.stdout.encoding)
         extracted_config_path = os.path.join(extract_path, config_path)
         extracted_config_path = os.path.join(extract_path, config_path)
         assert open(extracted_config_path).read() == open(config_path).read()
         assert open(extracted_config_path).read() == open(config_path).read()
 
 
         # Exercise the info action.
         # Exercise the info action.
         output = subprocess.check_output(
         output = subprocess.check_output(
-            'borgmatic --config {} info --json'.format(config_path).split(' ')
+            f'borgmatic --config {config_path} info --json'.split(' '),
         ).decode(sys.stdout.encoding)
         ).decode(sys.stdout.encoding)
         parsed_output = json.loads(output)
         parsed_output = json.loads(output)
 
 

+ 1 - 1
tests/end-to-end/test_database.py

@@ -189,7 +189,7 @@ def test_database_dump_with_error_causes_borgmatic_to_exit():
                     '-v',
                     '-v',
                     '2',
                     '2',
                     '--override',
                     '--override',
-                    "hooks.postgresql_databases=[{'name': 'nope'}]",
+                    "hooks.postgresql_databases=[{'name': 'nope'}]",  # noqa: FS003
                 ]
                 ]
             )
             )
     finally:
     finally:

+ 3 - 5
tests/end-to-end/test_override.py

@@ -10,17 +10,15 @@ def generate_configuration(config_path, repository_path):
     to work for testing (including injecting the given repository path and tacking on an encryption
     to work for testing (including injecting the given repository path and tacking on an encryption
     passphrase).
     passphrase).
     '''
     '''
-    subprocess.check_call(
-        'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
-    )
+    subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
     config = (
     config = (
         open(config_path)
         open(config_path)
         .read()
         .read()
         .replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
         .replace('ssh://user@backupserver/./sourcehostname.borg', repository_path)
-        .replace('- ssh://user@backupserver/./{fqdn}', '')
+        .replace('- ssh://user@backupserver/./{fqdn}', '')  # noqa: FS003
         .replace('- /var/local/backups/local.borg', '')
         .replace('- /var/local/backups/local.borg', '')
         .replace('- /home/user/path with spaces', '')
         .replace('- /home/user/path with spaces', '')
-        .replace('- /home', '- {}'.format(config_path))
+        .replace('- /home', f'- {config_path}')
         .replace('- /etc', '')
         .replace('- /etc', '')
         .replace('- /var/log/syslog*', '')
         .replace('- /var/log/syslog*', '')
         + 'storage:\n    encryption_passphrase: "test"'
         + 'storage:\n    encryption_passphrase: "test"'

+ 4 - 12
tests/end-to-end/test_validate_config.py

@@ -7,12 +7,8 @@ def test_validate_config_command_with_valid_configuration_succeeds():
     with tempfile.TemporaryDirectory() as temporary_directory:
     with tempfile.TemporaryDirectory() as temporary_directory:
         config_path = os.path.join(temporary_directory, 'test.yaml')
         config_path = os.path.join(temporary_directory, 'test.yaml')
 
 
-        subprocess.check_call(
-            'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
-        )
-        exit_code = subprocess.call(
-            'validate-borgmatic-config --config {}'.format(config_path).split(' ')
-        )
+        subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
+        exit_code = subprocess.call(f'validate-borgmatic-config --config {config_path}'.split(' '))
 
 
         assert exit_code == 0
         assert exit_code == 0
 
 
@@ -21,16 +17,12 @@ def test_validate_config_command_with_invalid_configuration_fails():
     with tempfile.TemporaryDirectory() as temporary_directory:
     with tempfile.TemporaryDirectory() as temporary_directory:
         config_path = os.path.join(temporary_directory, 'test.yaml')
         config_path = os.path.join(temporary_directory, 'test.yaml')
 
 
-        subprocess.check_call(
-            'generate-borgmatic-config --destination {}'.format(config_path).split(' ')
-        )
+        subprocess.check_call(f'generate-borgmatic-config --destination {config_path}'.split(' '))
         config = open(config_path).read().replace('keep_daily: 7', 'keep_daily: "7"')
         config = open(config_path).read().replace('keep_daily: 7', 'keep_daily: "7"')
         config_file = open(config_path, 'w')
         config_file = open(config_path, 'w')
         config_file.write(config)
         config_file.write(config)
         config_file.close()
         config_file.close()
 
 
-        exit_code = subprocess.call(
-            'validate-borgmatic-config --config {}'.format(config_path).split(' ')
-        )
+        exit_code = subprocess.call(f'validate-borgmatic-config --config {config_path}'.split(' '))
 
 
         assert exit_code == 1
         assert exit_code == 1

+ 1 - 1
tests/integration/config/test_legacy.py

@@ -7,7 +7,7 @@ from borgmatic.config import legacy as module
 
 
 def test_parse_section_options_with_punctuation_should_return_section_options():
 def test_parse_section_options_with_punctuation_should_return_section_options():
     parser = module.RawConfigParser()
     parser = module.RawConfigParser()
-    parser.read_file(StringIO('[section]\nfoo: {}\n'.format(string.punctuation)))
+    parser.read_file(StringIO(f'[section]\nfoo: {string.punctuation}\n'))
 
 
     section_format = module.Section_format(
     section_format = module.Section_format(
         'section', (module.Config_option('foo', str, required=True),)
         'section', (module.Config_option('foo', str, required=True),)

+ 4 - 4
tests/integration/test_execute.py

@@ -138,10 +138,10 @@ def test_log_outputs_kills_other_processes_when_one_errors():
 
 
     process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
-        process, None, 'borg'
+        ['grep'], None, 'borg'
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
-        process, 2, 'borg'
+        ['grep'], 2, 'borg'
     ).and_return(True)
     ).and_return(True)
     other_process = subprocess.Popen(
     other_process = subprocess.Popen(
         ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
         ['sleep', '2'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
@@ -245,10 +245,10 @@ def test_log_outputs_truncates_long_error_output():
 
 
     process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     process = subprocess.Popen(['grep'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
-        process, None, 'borg'
+        ['grep'], None, 'borg'
     ).and_return(False)
     ).and_return(False)
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
     flexmock(module).should_receive('exit_code_indicates_error').with_args(
-        process, 2, 'borg'
+        ['grep'], 2, 'borg'
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
     flexmock(module).should_receive('output_buffer_for_process').and_return(process.stdout)
 
 

+ 10 - 10
tests/unit/borg/test_create.py

@@ -449,7 +449,7 @@ def test_collect_special_file_paths_excludes_non_special_files():
     ) == ('/foo', '/baz')
     ) == ('/foo', '/baz')
 
 
 
 
-DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'
+DEFAULT_ARCHIVE_NAME = '{hostname}-{now:%Y-%m-%dT%H:%M:%S.%f}'  # noqa: FS003
 REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
 REPO_ARCHIVE_WITH_PATHS = (f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'bar')
 
 
 
 
@@ -2193,7 +2193,7 @@ def test_create_archive_with_source_directories_glob_expands():
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
+        ('borg', 'create', f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'food'),
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -2236,7 +2236,7 @@ def test_create_archive_with_non_matching_source_directories_glob_passes_through
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo*'),
+        ('borg', 'create', f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo*'),
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -2279,7 +2279,7 @@ def test_create_archive_with_glob_calls_borg_with_expanded_directories():
     )
     )
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
-        ('borg', 'create', 'repo::{}'.format(DEFAULT_ARCHIVE_NAME), 'foo', 'food'),
+        ('borg', 'create', f'repo::{DEFAULT_ARCHIVE_NAME}', 'foo', 'food'),
         output_log_level=logging.INFO,
         output_log_level=logging.INFO,
         output_file=None,
         output_file=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -2345,7 +2345,7 @@ def test_create_archive_with_archive_name_format_calls_borg_with_archive_name():
 def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
 def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
-    repository_archive_pattern = 'repo::Documents_{hostname}-{now}'
+    repository_archive_pattern = 'repo::Documents_{hostname}-{now}'  # noqa: FS003
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2380,7 +2380,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
             'repositories': ['repo'],
             'repositories': ['repo'],
             'exclude_patterns': None,
             'exclude_patterns': None,
         },
         },
-        storage_config={'archive_name_format': 'Documents_{hostname}-{now}'},
+        storage_config={'archive_name_format': 'Documents_{hostname}-{now}'},  # noqa: FS003
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
     )
     )
 
 
@@ -2388,7 +2388,7 @@ def test_create_archive_with_archive_name_format_accepts_borg_placeholders():
 def test_create_archive_with_repository_accepts_borg_placeholders():
 def test_create_archive_with_repository_accepts_borg_placeholders():
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.borgmatic.logger).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
     flexmock(module.logging).ANSWER = module.borgmatic.logger.ANSWER
-    repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'
+    repository_archive_pattern = '{fqdn}::Documents_{hostname}-{now}'  # noqa: FS003
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('collect_borgmatic_source_directories').and_return([])
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('deduplicate_directories').and_return(('foo', 'bar'))
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
     flexmock(module).should_receive('map_directories_to_devices').and_return({})
@@ -2417,13 +2417,13 @@ def test_create_archive_with_repository_accepts_borg_placeholders():
 
 
     module.create_archive(
     module.create_archive(
         dry_run=False,
         dry_run=False,
-        repository='{fqdn}',
+        repository='{fqdn}',  # noqa: FS003
         location_config={
         location_config={
             'source_directories': ['foo', 'bar'],
             'source_directories': ['foo', 'bar'],
-            'repositories': ['{fqdn}'],
+            'repositories': ['{fqdn}'],  # noqa: FS003
             'exclude_patterns': None,
             'exclude_patterns': None,
         },
         },
-        storage_config={'archive_name_format': 'Documents_{hostname}-{now}'},
+        storage_config={'archive_name_format': 'Documents_{hostname}-{now}'},  # noqa: FS003
         local_borg_version='1.2.3',
         local_borg_version='1.2.3',
     )
     )
 
 

+ 17 - 5
tests/unit/borg/test_prune.py

@@ -27,27 +27,39 @@ def test_make_prune_flags_returns_flags_from_config_plus_default_prefix_glob():
 
 
     result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
     result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
 
 
-    assert tuple(result) == BASE_PRUNE_FLAGS + (('--match-archives', 'sh:{hostname}-*'),)
+    assert tuple(result) == BASE_PRUNE_FLAGS + (
+        ('--match-archives', 'sh:{hostname}-*'),  # noqa: FS003
+    )
 
 
 
 
 def test_make_prune_flags_accepts_prefix_with_placeholders():
 def test_make_prune_flags_accepts_prefix_with_placeholders():
-    retention_config = OrderedDict((('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}')))
+    retention_config = OrderedDict(
+        (('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))  # noqa: FS003
+    )
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
 
 
     result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
     result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
 
 
-    expected = (('--keep-daily', '1'), ('--match-archives', 'sh:Documents_{hostname}-{now}*'))
+    expected = (
+        ('--keep-daily', '1'),
+        ('--match-archives', 'sh:Documents_{hostname}-{now}*'),  # noqa: FS003
+    )
 
 
     assert tuple(result) == expected
     assert tuple(result) == expected
 
 
 
 
 def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives():
 def test_make_prune_flags_with_prefix_without_borg_features_uses_glob_archives():
-    retention_config = OrderedDict((('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}')))
+    retention_config = OrderedDict(
+        (('keep_daily', 1), ('prefix', 'Documents_{hostname}-{now}'))  # noqa: FS003
+    )
     flexmock(module.feature).should_receive('available').and_return(False)
     flexmock(module.feature).should_receive('available').and_return(False)
 
 
     result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
     result = module.make_prune_flags(retention_config, local_borg_version='1.2.3')
 
 
-    expected = (('--keep-daily', '1'), ('--glob-archives', 'Documents_{hostname}-{now}*'))
+    expected = (
+        ('--keep-daily', '1'),
+        ('--glob-archives', 'Documents_{hostname}-{now}*'),  # noqa: FS003
+    )
 
 
     assert tuple(result) == expected
     assert tuple(result) == expected
 
 

+ 14 - 14
tests/unit/config/test_environment.py

@@ -12,7 +12,7 @@ def test_env(monkeypatch):
 
 
 def test_env_braces(monkeypatch):
 def test_env_braces(monkeypatch):
     monkeypatch.setenv('MY_CUSTOM_VALUE', 'foo')
     monkeypatch.setenv('MY_CUSTOM_VALUE', 'foo')
-    config = {'key': 'Hello ${MY_CUSTOM_VALUE}'}
+    config = {'key': 'Hello ${MY_CUSTOM_VALUE}'}  # noqa: FS003
     module.resolve_env_variables(config)
     module.resolve_env_variables(config)
     assert config == {'key': 'Hello foo'}
     assert config == {'key': 'Hello foo'}
 
 
@@ -20,7 +20,7 @@ def test_env_braces(monkeypatch):
 def test_env_multi(monkeypatch):
 def test_env_multi(monkeypatch):
     monkeypatch.setenv('MY_CUSTOM_VALUE', 'foo')
     monkeypatch.setenv('MY_CUSTOM_VALUE', 'foo')
     monkeypatch.setenv('MY_CUSTOM_VALUE2', 'bar')
     monkeypatch.setenv('MY_CUSTOM_VALUE2', 'bar')
-    config = {'key': 'Hello ${MY_CUSTOM_VALUE}${MY_CUSTOM_VALUE2}'}
+    config = {'key': 'Hello ${MY_CUSTOM_VALUE}${MY_CUSTOM_VALUE2}'}  # noqa: FS003
     module.resolve_env_variables(config)
     module.resolve_env_variables(config)
     assert config == {'key': 'Hello foobar'}
     assert config == {'key': 'Hello foobar'}
 
 
@@ -28,21 +28,21 @@ def test_env_multi(monkeypatch):
 def test_env_escape(monkeypatch):
 def test_env_escape(monkeypatch):
     monkeypatch.setenv('MY_CUSTOM_VALUE', 'foo')
     monkeypatch.setenv('MY_CUSTOM_VALUE', 'foo')
     monkeypatch.setenv('MY_CUSTOM_VALUE2', 'bar')
     monkeypatch.setenv('MY_CUSTOM_VALUE2', 'bar')
-    config = {'key': r'Hello ${MY_CUSTOM_VALUE} \${MY_CUSTOM_VALUE}'}
+    config = {'key': r'Hello ${MY_CUSTOM_VALUE} \${MY_CUSTOM_VALUE}'}  # noqa: FS003
     module.resolve_env_variables(config)
     module.resolve_env_variables(config)
-    assert config == {'key': r'Hello foo ${MY_CUSTOM_VALUE}'}
+    assert config == {'key': r'Hello foo ${MY_CUSTOM_VALUE}'}  # noqa: FS003
 
 
 
 
 def test_env_default_value(monkeypatch):
 def test_env_default_value(monkeypatch):
     monkeypatch.delenv('MY_CUSTOM_VALUE', raising=False)
     monkeypatch.delenv('MY_CUSTOM_VALUE', raising=False)
-    config = {'key': 'Hello ${MY_CUSTOM_VALUE:-bar}'}
+    config = {'key': 'Hello ${MY_CUSTOM_VALUE:-bar}'}  # noqa: FS003
     module.resolve_env_variables(config)
     module.resolve_env_variables(config)
     assert config == {'key': 'Hello bar'}
     assert config == {'key': 'Hello bar'}
 
 
 
 
 def test_env_unknown(monkeypatch):
 def test_env_unknown(monkeypatch):
     monkeypatch.delenv('MY_CUSTOM_VALUE', raising=False)
     monkeypatch.delenv('MY_CUSTOM_VALUE', raising=False)
-    config = {'key': 'Hello ${MY_CUSTOM_VALUE}'}
+    config = {'key': 'Hello ${MY_CUSTOM_VALUE}'}  # noqa: FS003
     with pytest.raises(ValueError):
     with pytest.raises(ValueError):
         module.resolve_env_variables(config)
         module.resolve_env_variables(config)
 
 
@@ -55,20 +55,20 @@ def test_env_full(monkeypatch):
         'dict': {
         'dict': {
             'key': 'value',
             'key': 'value',
             'anotherdict': {
             'anotherdict': {
-                'key': 'My ${MY_CUSTOM_VALUE} here',
-                'other': '${MY_CUSTOM_VALUE}',
-                'escaped': r'\${MY_CUSTOM_VALUE}',
+                'key': 'My ${MY_CUSTOM_VALUE} here',  # noqa: FS003
+                'other': '${MY_CUSTOM_VALUE}',  # noqa: FS003
+                'escaped': r'\${MY_CUSTOM_VALUE}',  # noqa: FS003
                 'list': [
                 'list': [
-                    '/home/${MY_CUSTOM_VALUE}/.local',
+                    '/home/${MY_CUSTOM_VALUE}/.local',  # noqa: FS003
                     '/var/log/',
                     '/var/log/',
-                    '/home/${MY_CUSTOM_VALUE2:-bar}/.config',
+                    '/home/${MY_CUSTOM_VALUE2:-bar}/.config',  # noqa: FS003
                 ],
                 ],
             },
             },
         },
         },
         'list': [
         'list': [
-            '/home/${MY_CUSTOM_VALUE}/.local',
+            '/home/${MY_CUSTOM_VALUE}/.local',  # noqa: FS003
             '/var/log/',
             '/var/log/',
-            '/home/${MY_CUSTOM_VALUE2-bar}/.config',
+            '/home/${MY_CUSTOM_VALUE2-bar}/.config',  # noqa: FS003
         ],
         ],
     }
     }
     module.resolve_env_variables(config)
     module.resolve_env_variables(config)
@@ -79,7 +79,7 @@ def test_env_full(monkeypatch):
             'anotherdict': {
             'anotherdict': {
                 'key': 'My foo here',
                 'key': 'My foo here',
                 'other': 'foo',
                 'other': 'foo',
-                'escaped': '${MY_CUSTOM_VALUE}',
+                'escaped': '${MY_CUSTOM_VALUE}',  # noqa: FS003
                 'list': ['/home/foo/.local', '/var/log/', '/home/bar/.config'],
                 'list': ['/home/foo/.local', '/var/log/', '/home/bar/.config'],
             },
             },
         },
         },

+ 4 - 4
tests/unit/config/test_validate.py

@@ -13,7 +13,7 @@ def test_format_json_error_path_element_formats_property():
 
 
 
 
 def test_format_json_error_formats_error_including_path():
 def test_format_json_error_formats_error_including_path():
-    flexmock(module).format_json_error_path_element = lambda element: '.{}'.format(element)
+    flexmock(module).format_json_error_path_element = lambda element: f'.{element}'
     error = flexmock(message='oops', path=['foo', 'bar'])
     error = flexmock(message='oops', path=['foo', 'bar'])
 
 
     assert module.format_json_error(error) == "At 'foo.bar': oops"
     assert module.format_json_error(error) == "At 'foo.bar': oops"
@@ -66,9 +66,9 @@ def test_apply_logical_validation_does_not_raise_if_archive_name_format_and_pref
     module.apply_logical_validation(
     module.apply_logical_validation(
         'config.yaml',
         'config.yaml',
         {
         {
-            'storage': {'archive_name_format': '{hostname}-{now}'},
-            'retention': {'prefix': '{hostname}-'},
-            'consistency': {'prefix': '{hostname}-'},
+            'storage': {'archive_name_format': '{hostname}-{now}'},  # noqa: FS003
+            'retention': {'prefix': '{hostname}-'},  # noqa: FS003
+            'consistency': {'prefix': '{hostname}-'},  # noqa: FS003
         },
         },
     )
     )
 
 

+ 5 - 12
tests/unit/hooks/test_command.py

@@ -11,27 +11,20 @@ def test_interpolate_context_passes_through_command_without_variable():
 
 
 
 
 def test_interpolate_context_passes_through_command_with_unknown_variable():
 def test_interpolate_context_passes_through_command_with_unknown_variable():
-    assert (
-        module.interpolate_context('test.yaml', 'pre-backup', 'ls {baz}', {'foo': 'bar'})
-        == 'ls {baz}'
-    )
+    command = 'ls {baz}'  # noqa: FS003
+
+    assert module.interpolate_context('test.yaml', 'pre-backup', command, {'foo': 'bar'}) == command
 
 
 
 
 def test_interpolate_context_interpolates_variables():
 def test_interpolate_context_interpolates_variables():
+    command = 'ls {foo}{baz} {baz}'  # noqa: FS003
     context = {'foo': 'bar', 'baz': 'quux'}
     context = {'foo': 'bar', 'baz': 'quux'}
 
 
     assert (
     assert (
-        module.interpolate_context('test.yaml', 'pre-backup', 'ls {foo}{baz} {baz}', context)
-        == 'ls barquux quux'
+        module.interpolate_context('test.yaml', 'pre-backup', command, context) == 'ls barquux quux'
     )
     )
 
 
 
 
-def test_interpolate_context_does_not_touch_unknown_variables():
-    context = {'foo': 'bar', 'baz': 'quux'}
-
-    assert module.interpolate_context('test.yaml', 'pre-backup', 'ls {wtf}', context) == 'ls {wtf}'
-
-
 def test_execute_hook_invokes_each_command():
 def test_execute_hook_invokes_each_command():
     flexmock(module).should_receive('interpolate_context').replace_with(
     flexmock(module).should_receive('interpolate_context').replace_with(
         lambda config_file, hook_description, command, context: command
         lambda config_file, hook_description, command, context: command

+ 1 - 3
tests/unit/hooks/test_healthchecks.py

@@ -206,9 +206,7 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
     payload = 'data'
     payload = 'data'
     flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
     flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
     flexmock(module.requests).should_receive('post').with_args(
     flexmock(module.requests).should_receive('post').with_args(
-        'https://hc-ping.com/{}'.format(hook_config['ping_url']),
-        data=payload.encode('utf-8'),
-        verify=True,
+        f"https://hc-ping.com/{hook_config['ping_url']}", data=payload.encode('utf-8'), verify=True,
     ).and_return(flexmock(ok=True))
     ).and_return(flexmock(ok=True))
 
 
     module.ping_monitor(
     module.ping_monitor(

+ 1 - 1
tests/unit/hooks/test_mongodb.py

@@ -17,7 +17,7 @@ def test_dump_databases_runs_mongodump_for_each_database():
 
 
     for name, process in zip(('foo', 'bar'), processes):
     for name, process in zip(('foo', 'bar'), processes):
         flexmock(module).should_receive('execute_command').with_args(
         flexmock(module).should_receive('execute_command').with_args(
-            ['mongodump', '--db', name, '--archive', '>', 'databases/localhost/{}'.format(name)],
+            ['mongodump', '--db', name, '--archive', '>', f'databases/localhost/{name}'],
             shell=True,
             shell=True,
             run_to_completion=False,
             run_to_completion=False,
         ).and_return(process).once()
         ).and_return(process).once()

+ 1 - 1
tests/unit/hooks/test_postgresql.py

@@ -134,7 +134,7 @@ def test_dump_databases_runs_pg_dump_for_each_database():
                 'custom',
                 'custom',
                 name,
                 name,
                 '>',
                 '>',
-                'databases/localhost/{}'.format(name),
+                f'databases/localhost/{name}',
             ),
             ),
             shell=True,
             shell=True,
             extra_environment={'PGSSLMODE': 'disable'},
             extra_environment={'PGSSLMODE': 'disable'},

+ 48 - 20
tests/unit/test_execute.py

@@ -7,32 +7,32 @@ from borgmatic import execute as module
 
 
 
 
 @pytest.mark.parametrize(
 @pytest.mark.parametrize(
-    'process,exit_code,borg_local_path,expected_result',
+    'command,exit_code,borg_local_path,expected_result',
     (
     (
-        (flexmock(args=['grep']), 2, None, True),
-        (flexmock(args=['grep']), 2, 'borg', True),
-        (flexmock(args=['borg']), 2, 'borg', True),
-        (flexmock(args=['borg1']), 2, 'borg1', True),
-        (flexmock(args=['grep']), 1, None, True),
-        (flexmock(args=['grep']), 1, 'borg', True),
-        (flexmock(args=['borg']), 1, 'borg', False),
-        (flexmock(args=['borg1']), 1, 'borg1', False),
-        (flexmock(args=['grep']), 0, None, False),
-        (flexmock(args=['grep']), 0, 'borg', False),
-        (flexmock(args=['borg']), 0, 'borg', False),
-        (flexmock(args=['borg1']), 0, 'borg1', False),
+        (['grep'], 2, None, True),
+        (['grep'], 2, 'borg', True),
+        (['borg'], 2, 'borg', True),
+        (['borg1'], 2, 'borg1', True),
+        (['grep'], 1, None, True),
+        (['grep'], 1, 'borg', True),
+        (['borg'], 1, 'borg', False),
+        (['borg1'], 1, 'borg1', False),
+        (['grep'], 0, None, False),
+        (['grep'], 0, 'borg', False),
+        (['borg'], 0, 'borg', False),
+        (['borg1'], 0, 'borg1', False),
         # -9 exit code occurs when child process get SIGKILLed.
         # -9 exit code occurs when child process get SIGKILLed.
-        (flexmock(args=['grep']), -9, None, True),
-        (flexmock(args=['grep']), -9, 'borg', True),
-        (flexmock(args=['borg']), -9, 'borg', True),
-        (flexmock(args=['borg1']), -9, 'borg1', True),
-        (flexmock(args=['borg']), None, None, False),
+        (['grep'], -9, None, True),
+        (['grep'], -9, 'borg', True),
+        (['borg'], -9, 'borg', True),
+        (['borg1'], -9, 'borg1', True),
+        (['borg'], None, None, False),
     ),
     ),
 )
 )
 def test_exit_code_indicates_error_respects_exit_code_and_borg_local_path(
 def test_exit_code_indicates_error_respects_exit_code_and_borg_local_path(
-    process, exit_code, borg_local_path, expected_result
+    command, exit_code, borg_local_path, expected_result
 ):
 ):
-    assert module.exit_code_indicates_error(process, exit_code, borg_local_path) is expected_result
+    assert module.exit_code_indicates_error(command, exit_code, borg_local_path) is expected_result
 
 
 
 
 def test_command_for_process_converts_sequence_command_to_string():
 def test_command_for_process_converts_sequence_command_to_string():
@@ -239,6 +239,34 @@ def test_execute_command_and_capture_output_with_capture_stderr_returns_stderr()
     assert output == expected_output
     assert output == expected_output
 
 
 
 
+def test_execute_command_and_capture_output_returns_output_when_process_error_is_not_considered_an_error():
+    full_command = ['foo', 'bar']
+    expected_output = '[]'
+    err_output = b'[]'
+    flexmock(module.os, environ={'a': 'b'})
+    flexmock(module.subprocess).should_receive('check_output').with_args(
+        full_command, stderr=None, shell=False, env=None, cwd=None
+    ).and_raise(subprocess.CalledProcessError(1, full_command, err_output)).once()
+    flexmock(module).should_receive('exit_code_indicates_error').and_return(False).once()
+
+    output = module.execute_command_and_capture_output(full_command)
+
+    assert output == expected_output
+
+
+def test_execute_command_and_capture_output_raises_when_command_errors():
+    full_command = ['foo', 'bar']
+    expected_output = '[]'
+    flexmock(module.os, environ={'a': 'b'})
+    flexmock(module.subprocess).should_receive('check_output').with_args(
+        full_command, stderr=None, shell=False, env=None, cwd=None
+    ).and_raise(subprocess.CalledProcessError(2, full_command, expected_output)).once()
+    flexmock(module).should_receive('exit_code_indicates_error').and_return(True).once()
+
+    with pytest.raises(subprocess.CalledProcessError):
+        module.execute_command_and_capture_output(full_command)
+
+
 def test_execute_command_and_capture_output_returns_output_with_shell():
 def test_execute_command_and_capture_output_returns_output_with_shell():
     full_command = ['foo', 'bar']
     full_command = ['foo', 'bar']
     expected_output = '[]'
     expected_output = '[]'