Browse Source

Merge branch 'main' into logging-verbosity-config

Dan Helfman 3 tháng trước cách đây
mục cha
commit
21cef267c1

+ 18 - 4
NEWS

@@ -1,13 +1,27 @@
-1.9.9.dev0
+1.9.10.dev0
+ * #987: Fix a "list" action error when the "encryption_passcommand" option is set.
+ * #987: When both "encryption_passcommand" and "encryption_passphrase" are configured, prefer
+   "encryption_passphrase" even if it's an empty value.
+ * #988: With the "max_duration" option or the "--max-duration" flag, run the archives and
+   repository checks separately so they don't interfere with one another. Previously, borgmatic
+   refused to run checks in this situation.
+ * #989: Fix the log message code to avoid using Python 3.10+ logging features. Now borgmatic will
+   work with Python 3.9 again.
+ * Add support for Python 3.13.
+
+1.9.9
  * #635: Log the repository path or label on every relevant log message, not just some logs.
  * #635: Log the repository path or label on every relevant log message, not just some logs.
  * #961: When the "encryption_passcommand" option is set, call the command once from borgmatic to
  * #961: When the "encryption_passcommand" option is set, call the command once from borgmatic to
-   collect the encryption passphrase and pass it to Borg multiple times. See the documentation for
-   more information: https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
+   collect the encryption passphrase and then pass it to Borg multiple times. See the documentation
+   for more information: https://torsion.org/borgmatic/docs/how-to/provide-your-passwords/
  * #981: Fix a "spot" check file count delta error.
  * #981: Fix a "spot" check file count delta error.
  * #982: Fix for borgmatic "exclude_patterns" and "exclude_from" recursing into excluded
  * #982: Fix for borgmatic "exclude_patterns" and "exclude_from" recursing into excluded
    subdirectories.
    subdirectories.
- * #983: Fix the Btrfs hook to support subvolumes with names like "@home", different from their
+ * #983: Fix the Btrfs hook to support subvolumes with names like "@home" different from their
    mount points.
    mount points.
+ * #985: Change the default value for the "--original-hostname" flag from "localhost" to no host
+   specified. This way, the "restore" action works without a hostname if there's a single matching
+   database dump.
 
 
 1.9.8
 1.9.8
  * #979: Fix root patterns so they don't have an invalid "sh:" prefix before getting passed to Borg.
  * #979: Fix root patterns so they don't have an invalid "sh:" prefix before getting passed to Borg.

+ 1 - 2
borgmatic/actions/check.py

@@ -386,13 +386,12 @@ def collect_spot_check_source_paths(
             stream_processes=stream_processes,
             stream_processes=stream_processes,
         )
         )
     )
     )
-    borg_environment = borgmatic.borg.environment.make_environment(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
 
 
     paths_output = borgmatic.execute.execute_command_and_capture_output(
     paths_output = borgmatic.execute.execute_command_and_capture_output(
         create_flags + create_positional_arguments,
         create_flags + create_positional_arguments,
         capture_stderr=True,
         capture_stderr=True,
-        extra_environment=borg_environment,
+        extra_environment=borgmatic.borg.environment.make_environment(config),
         working_directory=working_directory,
         working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),

+ 7 - 4
borgmatic/actions/restore.py

@@ -57,7 +57,7 @@ def render_dump_metadata(dump):
     Given a Dump instance, make a display string describing it for use in log messages.
     Given a Dump instance, make a display string describing it for use in log messages.
     '''
     '''
     name = 'unspecified' if dump.data_source_name is UNSPECIFIED else dump.data_source_name
     name = 'unspecified' if dump.data_source_name is UNSPECIFIED else dump.data_source_name
-    hostname = dump.hostname or 'localhost'
+    hostname = dump.hostname or UNSPECIFIED
     port = None if dump.port is UNSPECIFIED else dump.port
     port = None if dump.port is UNSPECIFIED else dump.port
 
 
     if port:
     if port:
@@ -343,12 +343,15 @@ def get_dumps_to_restore(restore_arguments, dumps_from_archive):
                     else UNSPECIFIED
                     else UNSPECIFIED
                 ),
                 ),
                 data_source_name=name,
                 data_source_name=name,
-                hostname=restore_arguments.original_hostname or 'localhost',
+                hostname=restore_arguments.original_hostname or UNSPECIFIED,
                 port=restore_arguments.original_port,
                 port=restore_arguments.original_port,
             )
             )
-            for name in restore_arguments.data_sources
+            for name in restore_arguments.data_sources or (UNSPECIFIED,)
         }
         }
-        if restore_arguments.data_sources
+        if restore_arguments.hook
+        or restore_arguments.data_sources
+        or restore_arguments.original_hostname
+        or restore_arguments.original_port
         else {
         else {
             Dump(
             Dump(
                 hook_name=UNSPECIFIED,
                 hook_name=UNSPECIFIED,

+ 1 - 2
borgmatic/borg/break_lock.py

@@ -34,10 +34,9 @@ def break_lock(
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 
-    borg_environment = environment.make_environment(config)
     execute_command(
     execute_command(
         full_command,
         full_command,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         working_directory=borgmatic.config.paths.get_working_directory(config),
         working_directory=borgmatic.config.paths.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),

+ 41 - 45
borgmatic/borg/check.py

@@ -64,15 +64,11 @@ def make_check_name_flags(checks, archive_filter_flags):
 
 
         ('--repository-only',)
         ('--repository-only',)
 
 
-    However, if both "repository" and "archives" are in checks, then omit them from the returned
-    flags because Borg does both checks by default. If "data" is in checks, that implies "archives".
+    However, if both "repository" and "archives" are in checks, then omit the "only" flags from the
+    returned flags because Borg does both checks by default. Note that a "data" check only works
+    along with an "archives" check.
     '''
     '''
-    if 'data' in checks:
-        data_flags = ('--verify-data',)
-        checks.update({'archives'})
-    else:
-        data_flags = ()
-
+    data_flags = ('--verify-data',) if 'data' in checks else ()
     common_flags = (archive_filter_flags if 'archives' in checks else ()) + data_flags
     common_flags = (archive_filter_flags if 'archives' in checks else ()) + data_flags
 
 
     if {'repository', 'archives'}.issubset(checks):
     if {'repository', 'archives'}.issubset(checks):
@@ -142,51 +138,51 @@ def check_archives(
     except StopIteration:
     except StopIteration:
         repository_check_config = {}
         repository_check_config = {}
 
 
-    if check_arguments.max_duration and 'archives' in checks:
-        raise ValueError('The archives check cannot run when the --max-duration flag is used')
-    if repository_check_config.get('max_duration') and 'archives' in checks:
-        raise ValueError(
-            'The archives check cannot run when the repository check has the max_duration option set'
-        )
-
     max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
     max_duration = check_arguments.max_duration or repository_check_config.get('max_duration')
-    umask = config.get('umask')
 
 
-    borg_environment = environment.make_environment(config)
+    umask = config.get('umask')
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
-
-    full_command = (
-        (local_path, 'check')
-        + (('--repair',) if check_arguments.repair else ())
-        + (('--max-duration', str(max_duration)) if max_duration else ())
-        + make_check_name_flags(checks, archive_filter_flags)
-        + (('--remote-path', remote_path) if remote_path else ())
-        + (('--umask', str(umask)) if umask else ())
-        + (('--log-json',) if global_arguments.log_json else ())
-        + (('--lock-wait', str(lock_wait)) if lock_wait else ())
-        + verbosity_flags
-        + (('--progress',) if check_arguments.progress else ())
-        + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
-        + flags.make_repository_flags(repository_path, local_borg_version)
-    )
-
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
 
 
-    # The Borg repair option triggers an interactive prompt, which won't work when output is
-    # captured. And progress messes with the terminal directly.
-    if check_arguments.repair or check_arguments.progress:
-        execute_command(
-            full_command,
-            output_file=DO_NOT_CAPTURE,
-            extra_environment=borg_environment,
-            working_directory=working_directory,
-            borg_local_path=local_path,
-            borg_exit_codes=borg_exit_codes,
+    if 'data' in checks:
+        checks.add('archives')
+
+    grouped_checks = (checks,)
+
+    # If max_duration is set, then archives and repository checks need to be run separately, as Borg
+    # doesn't support --max-duration along with an archives checks.
+    if max_duration and 'archives' in checks and 'repository' in checks:
+        checks.remove('repository')
+        grouped_checks = (checks, {'repository'})
+
+    for checks_subset in grouped_checks:
+        full_command = (
+            (local_path, 'check')
+            + (('--repair',) if check_arguments.repair else ())
+            + (
+                ('--max-duration', str(max_duration))
+                if max_duration and 'repository' in checks_subset
+                else ()
+            )
+            + make_check_name_flags(checks_subset, archive_filter_flags)
+            + (('--remote-path', remote_path) if remote_path else ())
+            + (('--umask', str(umask)) if umask else ())
+            + (('--log-json',) if global_arguments.log_json else ())
+            + (('--lock-wait', str(lock_wait)) if lock_wait else ())
+            + verbosity_flags
+            + (('--progress',) if check_arguments.progress else ())
+            + (tuple(extra_borg_options.split(' ')) if extra_borg_options else ())
+            + flags.make_repository_flags(repository_path, local_borg_version)
         )
         )
-    else:
+
         execute_command(
         execute_command(
             full_command,
             full_command,
-            extra_environment=borg_environment,
+            # The Borg repair option triggers an interactive prompt, which won't work when output is
+            # captured. And progress messes with the terminal directly.
+            output_file=(
+                DO_NOT_CAPTURE if check_arguments.repair or check_arguments.progress else None
+            ),
+            extra_environment=environment.make_environment(config),
             working_directory=working_directory,
             working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,

+ 8 - 13
borgmatic/borg/create.py

@@ -122,15 +122,14 @@ def collect_special_file_paths(
     config,
     config,
     local_path,
     local_path,
     working_directory,
     working_directory,
-    borg_environment,
     borgmatic_runtime_directory,
     borgmatic_runtime_directory,
 ):
 ):
     '''
     '''
     Given a dry-run flag, a Borg create command as a tuple, a configuration dict, a local Borg path,
     Given a dry-run flag, a Borg create command as a tuple, a configuration dict, a local Borg path,
-    a working directory, a dict of environment variables to pass to Borg, and the borgmatic runtime
-    directory, collect the paths for any special files (character devices, block devices, and named
-    pipes / FIFOs) that Borg would encounter during a create. These are all paths that could cause
-    Borg to hang if its --read-special flag is used.
+    a working directory, and the borgmatic runtime directory, collect the paths for any special
+    files (character devices, block devices, and named pipes / FIFOs) that Borg would encounter
+    during a create. These are all paths that could cause Borg to hang if its --read-special flag is
+    used.
 
 
     Skip looking for special files in the given borgmatic runtime directory, as borgmatic creates
     Skip looking for special files in the given borgmatic runtime directory, as borgmatic creates
     its own special files there for database dumps. And if the borgmatic runtime directory is
     its own special files there for database dumps. And if the borgmatic runtime directory is
@@ -144,7 +143,7 @@ def collect_special_file_paths(
         + ('--dry-run', '--list'),
         + ('--dry-run', '--list'),
         capture_stderr=True,
         capture_stderr=True,
         working_directory=working_directory,
         working_directory=working_directory,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
     )
     )
@@ -299,7 +298,6 @@ def make_base_create_command(
         logger.warning(
         logger.warning(
             'Ignoring configured "read_special" value of false, as true is needed for database hooks.'
             'Ignoring configured "read_special" value of false, as true is needed for database hooks.'
         )
         )
-        borg_environment = environment.make_environment(config)
         working_directory = borgmatic.config.paths.get_working_directory(config)
         working_directory = borgmatic.config.paths.get_working_directory(config)
 
 
         logger.debug('Collecting special file paths')
         logger.debug('Collecting special file paths')
@@ -309,7 +307,6 @@ def make_base_create_command(
             config,
             config,
             local_path,
             local_path,
             working_directory,
             working_directory,
-            borg_environment,
             borgmatic_runtime_directory=borgmatic_runtime_directory,
             borgmatic_runtime_directory=borgmatic_runtime_directory,
         )
         )
 
 
@@ -396,8 +393,6 @@ def create_archive(
     # the terminal directly.
     # the terminal directly.
     output_file = DO_NOT_CAPTURE if progress else None
     output_file = DO_NOT_CAPTURE if progress else None
 
 
-    borg_environment = environment.make_environment(config)
-
     create_flags += (
     create_flags += (
         (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
         (('--info',) if logger.getEffectiveLevel() == logging.INFO and not json else ())
         + (('--stats',) if stats and not json and not dry_run else ())
         + (('--stats',) if stats and not json and not dry_run else ())
@@ -414,7 +409,7 @@ def create_archive(
             output_log_level,
             output_log_level,
             output_file,
             output_file,
             working_directory=working_directory,
             working_directory=working_directory,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )
@@ -422,7 +417,7 @@ def create_archive(
         return execute_command_and_capture_output(
         return execute_command_and_capture_output(
             create_flags + create_positional_arguments,
             create_flags + create_positional_arguments,
             working_directory=working_directory,
             working_directory=working_directory,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )
@@ -432,7 +427,7 @@ def create_archive(
             output_log_level,
             output_log_level,
             output_file,
             output_file,
             working_directory=working_directory,
             working_directory=working_directory,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
         )
         )

+ 3 - 0
borgmatic/borg/environment.py

@@ -28,6 +28,9 @@ def make_environment(config):
     '''
     '''
     Given a borgmatic configuration dict, return its options converted to a Borg environment
     Given a borgmatic configuration dict, return its options converted to a Borg environment
     variable dict.
     variable dict.
+
+    Do not reuse this environment across multiple Borg invocations, because it can include
+    references to resources like anonymous pipes for passphrases—which can only be consumed once.
     '''
     '''
     environment = {}
     environment = {}
 
 

+ 4 - 6
borgmatic/borg/extract.py

@@ -44,7 +44,6 @@ def extract_last_archive_dry_run(
         return
         return
 
 
     list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
     list_flag = ('--list',) if logger.isEnabledFor(logging.DEBUG) else ()
-    borg_environment = environment.make_environment(config)
     full_extract_command = (
     full_extract_command = (
         (local_path, 'extract', '--dry-run')
         (local_path, 'extract', '--dry-run')
         + (('--remote-path', remote_path) if remote_path else ())
         + (('--remote-path', remote_path) if remote_path else ())
@@ -59,7 +58,7 @@ def extract_last_archive_dry_run(
 
 
     execute_command(
     execute_command(
         full_extract_command,
         full_extract_command,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         working_directory=borgmatic.config.paths.get_working_directory(config),
         working_directory=borgmatic.config.paths.get_working_directory(config),
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),
@@ -144,7 +143,6 @@ def extract_archive(
         + (tuple(paths) if paths else ())
         + (tuple(paths) if paths else ())
     )
     )
 
 
-    borg_environment = environment.make_environment(config)
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
     full_destination_path = (
     full_destination_path = (
         os.path.join(working_directory or '', destination_path) if destination_path else None
         os.path.join(working_directory or '', destination_path) if destination_path else None
@@ -156,7 +154,7 @@ def extract_archive(
         return execute_command(
         return execute_command(
             full_command,
             full_command,
             output_file=DO_NOT_CAPTURE,
             output_file=DO_NOT_CAPTURE,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=full_destination_path,
             working_directory=full_destination_path,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
@@ -168,7 +166,7 @@ def extract_archive(
             full_command,
             full_command,
             output_file=subprocess.PIPE,
             output_file=subprocess.PIPE,
             run_to_completion=False,
             run_to_completion=False,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=full_destination_path,
             working_directory=full_destination_path,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
@@ -178,7 +176,7 @@ def extract_archive(
     # if the restore paths don't exist in the archive.
     # if the restore paths don't exist in the archive.
     execute_command(
     execute_command(
         full_command,
         full_command,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         working_directory=full_destination_path,
         working_directory=full_destination_path,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,

+ 3 - 6
borgmatic/borg/list.py

@@ -106,8 +106,6 @@ def capture_archive_listing(
     format to use for the output, and local and remote Borg paths, capture the
     format to use for the output, and local and remote Borg paths, capture the
     output of listing that archive and return it as a list of file paths.
     output of listing that archive and return it as a list of file paths.
     '''
     '''
-    borg_environment = environment.make_environment(config)
-
     return tuple(
     return tuple(
         execute_command_and_capture_output(
         execute_command_and_capture_output(
             make_list_command(
             make_list_command(
@@ -126,7 +124,7 @@ def capture_archive_listing(
                 local_path,
                 local_path,
                 remote_path,
                 remote_path,
             ),
             ),
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=borgmatic.config.paths.get_working_directory(config),
             working_directory=borgmatic.config.paths.get_working_directory(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=config.get('borg_exit_codes'),
             borg_exit_codes=config.get('borg_exit_codes'),
@@ -194,7 +192,6 @@ def list_archive(
             'The --json flag on the list action is not supported when using the --archive/--find flags.'
             'The --json flag on the list action is not supported when using the --archive/--find flags.'
         )
         )
 
 
-    borg_environment = environment.make_environment(config)
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
 
 
     # If there are any paths to find (and there's not a single archive already selected), start by
     # If there are any paths to find (and there's not a single archive already selected), start by
@@ -224,7 +221,7 @@ def list_archive(
                     local_path,
                     local_path,
                     remote_path,
                     remote_path,
                 ),
                 ),
-                extra_environment=borg_environment,
+                extra_environment=environment.make_environment(config),
                 working_directory=borgmatic.config.paths.get_working_directory(config),
                 working_directory=borgmatic.config.paths.get_working_directory(config),
                 borg_local_path=local_path,
                 borg_local_path=local_path,
                 borg_exit_codes=borg_exit_codes,
                 borg_exit_codes=borg_exit_codes,
@@ -260,7 +257,7 @@ def list_archive(
         execute_command(
         execute_command(
             main_command,
             main_command,
             output_log_level=logging.ANSWER,
             output_log_level=logging.ANSWER,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=borgmatic.config.paths.get_working_directory(config),
             working_directory=borgmatic.config.paths.get_working_directory(config),
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,

+ 2 - 3
borgmatic/borg/mount.py

@@ -59,7 +59,6 @@ def mount_archive(
         + (tuple(mount_arguments.paths) if mount_arguments.paths else ())
         + (tuple(mount_arguments.paths) if mount_arguments.paths else ())
     )
     )
 
 
-    borg_environment = environment.make_environment(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
 
 
     # Don't capture the output when foreground mode is used so that ctrl-C can work properly.
     # Don't capture the output when foreground mode is used so that ctrl-C can work properly.
@@ -67,7 +66,7 @@ def mount_archive(
         execute_command(
         execute_command(
             full_command,
             full_command,
             output_file=DO_NOT_CAPTURE,
             output_file=DO_NOT_CAPTURE,
-            extra_environment=borg_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=working_directory,
             working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=config.get('borg_exit_codes'),
             borg_exit_codes=config.get('borg_exit_codes'),
@@ -76,7 +75,7 @@ def mount_archive(
 
 
     execute_command(
     execute_command(
         full_command,
         full_command,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         working_directory=working_directory,
         working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=config.get('borg_exit_codes'),
         borg_exit_codes=config.get('borg_exit_codes'),

+ 1 - 1
borgmatic/borg/passcommand.py

@@ -47,4 +47,4 @@ def get_passphrase_from_passcommand(config):
     passphrase = config.get('encryption_passphrase')
     passphrase = config.get('encryption_passphrase')
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
 
 
-    return run_passcommand(passcommand, bool(passphrase), working_directory)
+    return run_passcommand(passcommand, bool(passphrase is not None), working_directory)

+ 2 - 3
borgmatic/borg/repo_info.py

@@ -50,14 +50,13 @@ def display_repository_info(
         + flags.make_repository_flags(repository_path, local_borg_version)
         + flags.make_repository_flags(repository_path, local_borg_version)
     )
     )
 
 
-    extra_environment = environment.make_environment(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
     working_directory = borgmatic.config.paths.get_working_directory(config)
     borg_exit_codes = config.get('borg_exit_codes')
     borg_exit_codes = config.get('borg_exit_codes')
 
 
     if repo_info_arguments.json:
     if repo_info_arguments.json:
         return execute_command_and_capture_output(
         return execute_command_and_capture_output(
             full_command,
             full_command,
-            extra_environment=extra_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=working_directory,
             working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,
@@ -66,7 +65,7 @@ def display_repository_info(
         execute_command(
         execute_command(
             full_command,
             full_command,
             output_log_level=logging.ANSWER,
             output_log_level=logging.ANSWER,
-            extra_environment=extra_environment,
+            extra_environment=environment.make_environment(config),
             working_directory=working_directory,
             working_directory=working_directory,
             borg_local_path=local_path,
             borg_local_path=local_path,
             borg_exit_codes=borg_exit_codes,
             borg_exit_codes=borg_exit_codes,

+ 2 - 3
borgmatic/borg/repo_list.py

@@ -140,7 +140,6 @@ def list_repository(
     return JSON output).
     return JSON output).
     '''
     '''
     borgmatic.logger.add_custom_log_levels()
     borgmatic.logger.add_custom_log_levels()
-    borg_environment = environment.make_environment(config)
 
 
     main_command = make_repo_list_command(
     main_command = make_repo_list_command(
         repository_path,
         repository_path,
@@ -165,7 +164,7 @@ def list_repository(
 
 
     json_listing = execute_command_and_capture_output(
     json_listing = execute_command_and_capture_output(
         json_command,
         json_command,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         working_directory=working_directory,
         working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,
@@ -179,7 +178,7 @@ def list_repository(
     execute_command(
     execute_command(
         main_command,
         main_command,
         output_log_level=logging.ANSWER,
         output_log_level=logging.ANSWER,
-        extra_environment=borg_environment,
+        extra_environment=environment.make_environment(config),
         working_directory=working_directory,
         working_directory=working_directory,
         borg_local_path=local_path,
         borg_local_path=local_path,
         borg_exit_codes=borg_exit_codes,
         borg_exit_codes=borg_exit_codes,

+ 2 - 2
borgmatic/config/schema.yaml

@@ -632,8 +632,8 @@ properties:
                               long-running repository check into multiple
                               long-running repository check into multiple
                               partial checks. Defaults to no interruption. Only
                               partial checks. Defaults to no interruption. Only
                               applies to the "repository" check, does not check
                               applies to the "repository" check, does not check
-                              the repository index, and is not compatible with a
-                              simultaneous "archives" check or "--repair" flag.
+                              the repository index and is not compatible with
+                              the "--repair" flag.
                           example: 3600
                           example: 3600
                 - required:
                 - required:
                     - name
                     - name

+ 31 - 24
borgmatic/logger.py

@@ -87,11 +87,16 @@ class Multi_stream_handler(logging.Handler):
             handler.setLevel(level)
             handler.setLevel(level)
 
 
 
 
-class Console_no_color_formatter(logging.Formatter):
-    def __init__(self, *args, **kwargs):  # pragma: no cover
-        super(Console_no_color_formatter, self).__init__(
-            '{prefix}{message}', style='{', defaults={'prefix': ''}, *args, **kwargs
-        )
+class Log_prefix_formatter(logging.Formatter):
+    def __init__(self, fmt='{prefix}{message}', style='{', *args, **kwargs):  # pragma: no cover
+        self.prefix = None
+
+        super(Log_prefix_formatter, self).__init__(fmt=fmt, style=style, *args, **kwargs)
+
+    def format(self, record):  # pragma: no cover
+        record.prefix = f'{self.prefix}: ' if self.prefix else ''
+
+        return super(Log_prefix_formatter, self).format(record)
 
 
 
 
 class Color(enum.Enum):
 class Color(enum.Enum):
@@ -105,8 +110,9 @@ class Color(enum.Enum):
 
 
 class Console_color_formatter(logging.Formatter):
 class Console_color_formatter(logging.Formatter):
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):
+        self.prefix = None
         super(Console_color_formatter, self).__init__(
         super(Console_color_formatter, self).__init__(
-            '{prefix}{message}', style='{', defaults={'prefix': ''}, *args, **kwargs
+            '{prefix}{message}', style='{', *args, **kwargs
         )
         )
 
 
     def format(self, record):
     def format(self, record):
@@ -124,6 +130,7 @@ class Console_color_formatter(logging.Formatter):
             .get(record.levelno)
             .get(record.levelno)
             .value
             .value
         )
         )
+        record.prefix = f'{self.prefix}: ' if self.prefix else ''
 
 
         return color_text(color, super(Console_color_formatter, self).format(record))
         return color_text(color, super(Console_color_formatter, self).format(record))
 
 
@@ -188,28 +195,32 @@ def add_custom_log_levels():  # pragma: no cover
 
 
 def get_log_prefix():
 def get_log_prefix():
     '''
     '''
-    Return the current log prefix from the defaults for the formatter on the first logging handler,
-    set by set_log_prefix(). Return None if no such prefix exists.
+    Return the current log prefix set by set_log_prefix(). Return None if no such prefix exists.
+
+    It would be a whole lot easier to use logger.Formatter(defaults=...) instead, but that argument
+    doesn't exist until Python 3.10+.
     '''
     '''
     try:
     try:
-        return next(
-            handler.formatter._style._defaults.get('prefix').rstrip().rstrip(':')
+        formatter = next(
+            handler.formatter
             for handler in logging.getLogger().handlers
             for handler in logging.getLogger().handlers
+            if handler.formatter
+            if hasattr(handler.formatter, 'prefix')
         )
         )
-    except (StopIteration, AttributeError):
+    except StopIteration:
         return None
         return None
 
 
+    return formatter.prefix
+
 
 
 def set_log_prefix(prefix):
 def set_log_prefix(prefix):
     '''
     '''
-    Given a log prefix as a string, set it into the defaults for the formatters on all logging
-    handlers. Note that this overwrites any existing defaults.
+    Given a log prefix as a string, set it into the each handler's formatter so that it can inject
+    the prefix into each logged record.
     '''
     '''
     for handler in logging.getLogger().handlers:
     for handler in logging.getLogger().handlers:
-        try:
-            handler.formatter._style._defaults = {'prefix': f'{prefix}: ' if prefix else ''}
-        except AttributeError:
-            pass
+        if handler.formatter and hasattr(handler.formatter, 'prefix'):
+            handler.formatter.prefix = prefix
 
 
 
 
 class Log_prefix:
 class Log_prefix:
@@ -351,7 +362,7 @@ def configure_logging(
     if color_enabled:
     if color_enabled:
         console_handler.setFormatter(Console_color_formatter())
         console_handler.setFormatter(Console_color_formatter())
     else:
     else:
-        console_handler.setFormatter(Console_no_color_formatter())
+        console_handler.setFormatter(Log_prefix_formatter())
 
 
     console_handler.setLevel(console_log_level)
     console_handler.setLevel(console_log_level)
     handlers = [console_handler]
     handlers = [console_handler]
@@ -369,10 +380,8 @@ def configure_logging(
         if syslog_path:
         if syslog_path:
             syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
             syslog_handler = logging.handlers.SysLogHandler(address=syslog_path)
             syslog_handler.setFormatter(
             syslog_handler.setFormatter(
-                logging.Formatter(
+                Log_prefix_formatter(
                     'borgmatic: {levelname} {prefix}{message}',  # noqa: FS003
                     'borgmatic: {levelname} {prefix}{message}',  # noqa: FS003
-                    style='{',
-                    defaults={'prefix': ''},
                 )
                 )
             )
             )
             syslog_handler.setLevel(syslog_log_level)
             syslog_handler.setLevel(syslog_log_level)
@@ -381,10 +390,8 @@ def configure_logging(
     if log_file and log_file_log_level != logging.DISABLED:
     if log_file and log_file_log_level != logging.DISABLED:
         file_handler = logging.handlers.WatchedFileHandler(log_file)
         file_handler = logging.handlers.WatchedFileHandler(log_file)
         file_handler.setFormatter(
         file_handler.setFormatter(
-            logging.Formatter(
+            Log_prefix_formatter(
                 log_file_format or '[{asctime}] {levelname}: {prefix}{message}',  # noqa: FS003
                 log_file_format or '[{asctime}] {levelname}: {prefix}{message}',  # noqa: FS003
-                style='{',
-                defaults={'prefix': ''},
             )
             )
         )
         )
         file_handler.setLevel(log_file_log_level)
         file_handler.setLevel(log_file_log_level)

+ 1 - 1
pyproject.toml

@@ -1,6 +1,6 @@
 [project]
 [project]
 name = "borgmatic"
 name = "borgmatic"
-version = "1.9.9.dev0"
+version = "1.9.10.dev0"
 authors = [
 authors = [
   { name="Dan Helfman", email="witten@torsion.org" },
   { name="Dan Helfman", email="witten@torsion.org" },
 ]
 ]

+ 1 - 2
scripts/run-full-tests

@@ -18,8 +18,7 @@ if [ -z "$TEST_CONTAINER" ]; then
 fi
 fi
 
 
 apk add --no-cache python3 py3-pip borgbackup postgresql17-client mariadb-client mongodb-tools \
 apk add --no-cache python3 py3-pip borgbackup postgresql17-client mariadb-client mongodb-tools \
-    py3-mongo py3-regex py3-ruamel.yaml py3-ruamel.yaml.clib py3-tox py3-typed-ast py3-yaml bash \
-    sqlite fish
+    py3-mongo py3-regex py3-ruamel.yaml py3-ruamel.yaml.clib py3-tox py3-yaml bash sqlite fish
 export COVERAGE_FILE=/tmp/.coverage
 export COVERAGE_FILE=/tmp/.coverage
 
 
 tox --workdir /tmp/.tox --sitepackages
 tox --workdir /tmp/.tox --sitepackages

+ 0 - 1
test_requirements.txt

@@ -30,4 +30,3 @@ regex
 requests==2.32.2
 requests==2.32.2
 ruamel.yaml>0.15.0
 ruamel.yaml>0.15.0
 toml==0.10.2
 toml==0.10.2
-typed-ast

+ 11 - 11
tests/unit/actions/test_restore.py

@@ -575,11 +575,11 @@ def test_get_dumps_to_restore_gets_requested_dumps_found_in_archive():
     }
     }
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'foo'),
+        module.Dump(module.UNSPECIFIED, 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'bar'),
+        module.Dump(module.UNSPECIFIED, 'bar', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'bar'),
         module.Dump('postgresql_databases', 'bar'),
     ).and_return(True)
     ).and_return(True)
 
 
@@ -644,11 +644,11 @@ def test_get_dumps_to_restore_with_all_in_requested_dumps_finds_all_archive_dump
     }
     }
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'foo'),
+        module.Dump(module.UNSPECIFIED, 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'bar'),
+        module.Dump(module.UNSPECIFIED, 'bar', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'bar'),
         module.Dump('postgresql_databases', 'bar'),
     ).and_return(True)
     ).and_return(True)
 
 
@@ -673,11 +673,11 @@ def test_get_dumps_to_restore_with_all_in_requested_dumps_plus_additional_reques
     }
     }
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'foo'),
+        module.Dump(module.UNSPECIFIED, 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'bar'),
+        module.Dump(module.UNSPECIFIED, 'bar', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'bar'),
         module.Dump('postgresql_databases', 'bar'),
     ).and_return(True)
     ).and_return(True)
 
 
@@ -698,11 +698,11 @@ def test_get_dumps_to_restore_with_all_in_requested_dumps_plus_additional_reques
 def test_get_dumps_to_restore_raises_for_multiple_matching_dumps_in_archive():
 def test_get_dumps_to_restore_raises_for_multiple_matching_dumps_in_archive():
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'foo'),
+        module.Dump(module.UNSPECIFIED, 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'foo'),
+        module.Dump(module.UNSPECIFIED, 'foo', hostname=module.UNSPECIFIED),
         module.Dump('mariadb_databases', 'foo'),
         module.Dump('mariadb_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('render_dump_metadata').and_return('test')
     flexmock(module).should_receive('render_dump_metadata').and_return('test')
@@ -725,7 +725,7 @@ def test_get_dumps_to_restore_raises_for_multiple_matching_dumps_in_archive():
 def test_get_dumps_to_restore_raises_for_all_in_requested_dumps_and_requested_dumps_missing_from_archive():
 def test_get_dumps_to_restore_raises_for_all_in_requested_dumps_and_requested_dumps_missing_from_archive():
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump(module.UNSPECIFIED, 'foo'),
+        module.Dump(module.UNSPECIFIED, 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
     flexmock(module).should_receive('render_dump_metadata').and_return('test')
     flexmock(module).should_receive('render_dump_metadata').and_return('test')
@@ -750,7 +750,7 @@ def test_get_dumps_to_restore_with_requested_hook_name_filters_dumps_found_in_ar
     }
     }
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump('postgresql_databases', 'foo'),
+        module.Dump('postgresql_databases', 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
 
 
@@ -775,7 +775,7 @@ def test_get_dumps_to_restore_with_requested_shortened_hook_name_filters_dumps_f
     }
     }
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').and_return(False)
     flexmock(module).should_receive('dumps_match').with_args(
     flexmock(module).should_receive('dumps_match').with_args(
-        module.Dump('postgresql_databases', 'foo'),
+        module.Dump('postgresql_databases', 'foo', hostname=module.UNSPECIFIED),
         module.Dump('postgresql_databases', 'foo'),
         module.Dump('postgresql_databases', 'foo'),
     ).and_return(True)
     ).and_return(True)
 
 

+ 198 - 71
tests/unit/borg/test_check.py

@@ -8,13 +8,16 @@ from borgmatic.borg import check as module
 from ..test_verbosity import insert_logging_mock
 from ..test_verbosity import insert_logging_mock
 
 
 
 
-def insert_execute_command_mock(command, working_directory=None, borg_exit_codes=None):
+def insert_execute_command_mock(
+    command, output_file=None, working_directory=None, borg_exit_codes=None
+):
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
         working_directory,
         working_directory,
     )
     )
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         command,
         command,
+        output_file=output_file,
         extra_environment=None,
         extra_environment=None,
         working_directory=working_directory,
         working_directory=working_directory,
         borg_local_path=command[0],
         borg_local_path=command[0],
@@ -250,11 +253,11 @@ def test_make_check_name_flags_without_archives_check_and_with_archive_filter_fl
     assert flags == ('--repository-only',)
     assert flags == ('--repository-only',)
 
 
 
 
-def test_make_check_name_flags_with_data_check_returns_flag_and_implies_archives():
+def test_make_check_name_flags_with_archives_and_data_check_returns_verify_data_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_name_flags({'data'}, ())
+    flags = module.make_check_name_flags({'archives', 'data'}, ())
 
 
     assert flags == (
     assert flags == (
         '--archives-only',
         '--archives-only',
@@ -262,28 +265,22 @@ def test_make_check_name_flags_with_data_check_returns_flag_and_implies_archives
     )
     )
 
 
 
 
-def test_make_check_name_flags_with_extract_omits_extract_flag():
+def test_make_check_name_flags_with_repository_and_data_check_returns_verify_data_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_name_flags({'extract'}, ())
+    flags = module.make_check_name_flags({'archives', 'data', 'repository'}, ())
 
 
-    assert flags == ()
+    assert flags == ('--verify-data',)
 
 
 
 
-def test_make_check_name_flags_with_repository_and_data_checks_does_not_return_repository_only():
+def test_make_check_name_flags_with_extract_omits_extract_flag():
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.feature).should_receive('available').and_return(True)
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
     flexmock(module.flags).should_receive('make_match_archives_flags').and_return(())
 
 
-    flags = module.make_check_name_flags(
-        {
-            'repository',
-            'data',
-        },
-        (),
-    )
+    flags = module.make_check_name_flags({'extract'}, ())
 
 
-    assert flags == ('--verify-data',)
+    assert flags == ()
 
 
 
 
 def test_get_repository_id_with_valid_json_does_not_raise():
 def test_get_repository_id_with_valid_json_does_not_raise():
@@ -336,7 +333,9 @@ def test_get_repository_id_with_missing_json_keys_raises():
 
 
 def test_check_archives_with_progress_passes_through_to_borg():
 def test_check_archives_with_progress_passes_through_to_borg():
     config = {}
     config = {}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@@ -369,7 +368,9 @@ def test_check_archives_with_progress_passes_through_to_borg():
 
 
 def test_check_archives_with_repair_passes_through_to_borg():
 def test_check_archives_with_repair_passes_through_to_borg():
     config = {}
     config = {}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
@@ -402,12 +403,15 @@ def test_check_archives_with_repair_passes_through_to_borg():
 
 
 def test_check_archives_with_max_duration_flag_passes_through_to_borg():
 def test_check_archives_with_max_duration_flag_passes_through_to_borg():
     config = {}
     config = {}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '33', 'repo'),
         ('borg', 'check', '--max-duration', '33', 'repo'),
+        output_file=None,
         extra_environment=None,
         extra_environment=None,
         working_directory=None,
         working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -432,37 +436,17 @@ def test_check_archives_with_max_duration_flag_passes_through_to_borg():
     )
     )
 
 
 
 
-def test_check_archives_with_max_duration_flag_and_archives_check_errors():
-    config = {}
-    flexmock(module).should_receive('execute_command').never()
-
-    with pytest.raises(ValueError):
-        module.check_archives(
-            repository_path='repo',
-            config=config,
-            local_borg_version='1.2.3',
-            check_arguments=flexmock(
-                progress=None,
-                repair=None,
-                only_checks=None,
-                force=None,
-                match_archives=None,
-                max_duration=33,
-            ),
-            global_arguments=flexmock(log_json=False),
-            checks={'repository', 'archives'},
-            archive_filter_flags=(),
-        )
-
-
 def test_check_archives_with_max_duration_option_passes_through_to_borg():
 def test_check_archives_with_max_duration_option_passes_through_to_borg():
     config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
     config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '33', 'repo'),
         ('borg', 'check', '--max-duration', '33', 'repo'),
+        output_file=None,
         extra_environment=None,
         extra_environment=None,
         working_directory=None,
         working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -487,37 +471,145 @@ def test_check_archives_with_max_duration_option_passes_through_to_borg():
     )
     )
 
 
 
 
-def test_check_archives_with_max_duration_option_and_archives_check_errors():
-    config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
-    flexmock(module).should_receive('execute_command').never()
+def test_check_archives_with_max_duration_option_and_archives_check_runs_repository_check_separately():
+    config = {'checks': [{'name': 'repository', 'max_duration': 33}, {'name': 'archives'}]}
+    flexmock(module).should_receive('make_check_name_flags').with_args({'archives'}, ()).and_return(
+        ('--archives-only',)
+    )
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(('--repository-only',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'check', '--archives-only', 'repo'))
+    insert_execute_command_mock(
+        ('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
+    )
+
+    module.check_archives(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        check_arguments=flexmock(
+            progress=None,
+            repair=None,
+            only_checks=None,
+            force=None,
+            match_archives=None,
+            max_duration=None,
+        ),
+        global_arguments=flexmock(log_json=False),
+        checks={'repository', 'archives'},
+        archive_filter_flags=(),
+    )
+
+
+def test_check_archives_with_max_duration_flag_and_archives_check_runs_repository_check_separately():
+    config = {'checks': [{'name': 'repository'}, {'name': 'archives'}]}
+    flexmock(module).should_receive('make_check_name_flags').with_args({'archives'}, ()).and_return(
+        ('--archives-only',)
+    )
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(('--repository-only',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'check', '--archives-only', 'repo'))
+    insert_execute_command_mock(
+        ('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
+    )
+
+    module.check_archives(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        check_arguments=flexmock(
+            progress=None,
+            repair=None,
+            only_checks=None,
+            force=None,
+            match_archives=None,
+            max_duration=33,
+        ),
+        global_arguments=flexmock(log_json=False),
+        checks={'repository', 'archives'},
+        archive_filter_flags=(),
+    )
 
 
-    with pytest.raises(ValueError):
-        module.check_archives(
-            repository_path='repo',
-            config=config,
-            local_borg_version='1.2.3',
-            check_arguments=flexmock(
-                progress=None,
-                repair=None,
-                only_checks=None,
-                force=None,
-                match_archives=None,
-                max_duration=None,
-            ),
-            global_arguments=flexmock(log_json=False),
-            checks={'repository', 'archives'},
-            archive_filter_flags=(),
-        )
+
+def test_check_archives_with_max_duration_option_and_data_check_runs_repository_check_separately():
+    config = {'checks': [{'name': 'repository', 'max_duration': 33}, {'name': 'data'}]}
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'data', 'archives'}, ()
+    ).and_return(('--archives-only', '--verify-data'))
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(('--repository-only',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'check', '--archives-only', '--verify-data', 'repo'))
+    insert_execute_command_mock(
+        ('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
+    )
+
+    module.check_archives(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        check_arguments=flexmock(
+            progress=None,
+            repair=None,
+            only_checks=None,
+            force=None,
+            match_archives=None,
+            max_duration=None,
+        ),
+        global_arguments=flexmock(log_json=False),
+        checks={'repository', 'data'},
+        archive_filter_flags=(),
+    )
+
+
+def test_check_archives_with_max_duration_flag_and_data_check_runs_repository_check_separately():
+    config = {'checks': [{'name': 'repository'}, {'name': 'data'}]}
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'data', 'archives'}, ()
+    ).and_return(('--archives-only', '--verify-data'))
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(('--repository-only',))
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'check', '--archives-only', '--verify-data', 'repo'))
+    insert_execute_command_mock(
+        ('borg', 'check', '--max-duration', '33', '--repository-only', 'repo')
+    )
+
+    module.check_archives(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        check_arguments=flexmock(
+            progress=None,
+            repair=None,
+            only_checks=None,
+            force=None,
+            match_archives=None,
+            max_duration=33,
+        ),
+        global_arguments=flexmock(log_json=False),
+        checks={'repository', 'data'},
+        archive_filter_flags=(),
+    )
 
 
 
 
 def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
 def test_check_archives_with_max_duration_flag_overrides_max_duration_option():
     config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
     config = {'checks': [{'name': 'repository', 'max_duration': 33}]}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--max-duration', '44', 'repo'),
         ('borg', 'check', '--max-duration', '44', 'repo'),
+        output_file=None,
         extra_environment=None,
         extra_environment=None,
         working_directory=None,
         working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -575,9 +667,37 @@ def test_check_archives_calls_borg_with_parameters(checks):
     )
     )
 
 
 
 
+def test_check_archives_with_data_check_implies_archives_check_calls_borg_with_parameters():
+    config = {}
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'data', 'archives'}, ()
+    ).and_return(())
+    flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
+    insert_execute_command_mock(('borg', 'check', 'repo'))
+
+    module.check_archives(
+        repository_path='repo',
+        config=config,
+        local_borg_version='1.2.3',
+        check_arguments=flexmock(
+            progress=None,
+            repair=None,
+            only_checks=None,
+            force=None,
+            match_archives=None,
+            max_duration=None,
+        ),
+        global_arguments=flexmock(log_json=False),
+        checks={'data'},
+        archive_filter_flags=(),
+    )
+
+
 def test_check_archives_with_log_info_passes_through_to_borg():
 def test_check_archives_with_log_info_passes_through_to_borg():
     config = {}
     config = {}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_logging_mock(logging.INFO)
     insert_logging_mock(logging.INFO)
     insert_execute_command_mock(('borg', 'check', '--info', 'repo'))
     insert_execute_command_mock(('borg', 'check', '--info', 'repo'))
@@ -602,7 +722,9 @@ def test_check_archives_with_log_info_passes_through_to_borg():
 
 
 def test_check_archives_with_log_debug_passes_through_to_borg():
 def test_check_archives_with_log_debug_passes_through_to_borg():
     config = {}
     config = {}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_logging_mock(logging.DEBUG)
     insert_logging_mock(logging.DEBUG)
     insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo'))
     insert_execute_command_mock(('borg', 'check', '--debug', '--show-rc', 'repo'))
@@ -806,7 +928,9 @@ def test_check_archives_with_retention_prefix():
 
 
 def test_check_archives_with_extra_borg_options_passes_through_to_borg():
 def test_check_archives_with_extra_borg_options_passes_through_to_borg():
     config = {'extra_borg_options': {'check': '--extra --options'}}
     config = {'extra_borg_options': {'check': '--extra --options'}}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
     insert_execute_command_mock(('borg', 'check', '--extra', '--options', 'repo'))
 
 
@@ -829,15 +953,16 @@ def test_check_archives_with_extra_borg_options_passes_through_to_borg():
 
 
 
 
 def test_check_archives_with_match_archives_passes_through_to_borg():
 def test_check_archives_with_match_archives_passes_through_to_borg():
-    config = {}
-    flexmock(module).should_receive('make_check_name_flags').and_return(
-        ('--match-archives', 'foo-*')
-    )
+    config = {'checks': [{'name': 'archives'}]}
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'archives'}, object
+    ).and_return(('--match-archives', 'foo-*'))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module).should_receive('execute_command').with_args(
     flexmock(module).should_receive('execute_command').with_args(
         ('borg', 'check', '--match-archives', 'foo-*', 'repo'),
         ('borg', 'check', '--match-archives', 'foo-*', 'repo'),
+        output_file=None,
         extra_environment=None,
         extra_environment=None,
         working_directory=None,
         working_directory=None,
         borg_local_path='borg',
         borg_local_path='borg',
@@ -864,7 +989,9 @@ def test_check_archives_with_match_archives_passes_through_to_borg():
 
 
 def test_check_archives_calls_borg_with_working_directory():
 def test_check_archives_calls_borg_with_working_directory():
     config = {'working_directory': '/working/dir'}
     config = {'working_directory': '/working/dir'}
-    flexmock(module).should_receive('make_check_name_flags').and_return(())
+    flexmock(module).should_receive('make_check_name_flags').with_args(
+        {'repository'}, ()
+    ).and_return(())
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.flags).should_receive('make_repository_flags').and_return(('repo',))
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.environment).should_receive('make_environment')
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)
     flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(None)

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

@@ -185,6 +185,7 @@ def test_any_parent_directories_treats_unrelated_paths_as_non_match():
 
 
 
 
 def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list():
 def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_list():
+    flexmock(module.environment).should_receive('make_environment').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
         'Processing files ...\n- /foo\n+ /bar\n- /baz'
         'Processing files ...\n- /foo\n+ /bar\n- /baz'
     )
     )
@@ -198,12 +199,12 @@ def test_collect_special_file_paths_parses_special_files_from_borg_dry_run_file_
         config={},
         config={},
         local_path=None,
         local_path=None,
         working_directory=None,
         working_directory=None,
-        borg_environment=None,
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
     ) == ('/foo', '/bar', '/baz')
     ) == ('/foo', '/bar', '/baz')
 
 
 
 
 def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
 def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
+    flexmock(module.environment).should_receive('make_environment').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
         '+ /foo\n- /run/borgmatic/bar\n- /baz'
         '+ /foo\n- /run/borgmatic/bar\n- /baz'
     )
     )
@@ -225,12 +226,12 @@ def test_collect_special_file_paths_skips_borgmatic_runtime_directory():
         config={},
         config={},
         local_path=None,
         local_path=None,
         working_directory=None,
         working_directory=None,
-        borg_environment=None,
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
     ) == ('/foo', '/baz')
     ) == ('/foo', '/baz')
 
 
 
 
 def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
 def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_from_paths_output_errors():
+    flexmock(module.environment).should_receive('make_environment').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
         '+ /foo\n- /bar\n- /baz'
         '+ /foo\n- /bar\n- /baz'
     )
     )
@@ -245,12 +246,12 @@ def test_collect_special_file_paths_with_borgmatic_runtime_directory_missing_fro
             config={},
             config={},
             local_path=None,
             local_path=None,
             working_directory=None,
             working_directory=None,
-            borg_environment=None,
             borgmatic_runtime_directory='/run/borgmatic',
             borgmatic_runtime_directory='/run/borgmatic',
         )
         )
 
 
 
 
 def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory_missing_from_paths_output_does_not_raise():
 def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory_missing_from_paths_output_does_not_raise():
+    flexmock(module.environment).should_receive('make_environment').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
         '+ /foo\n- /bar\n- /baz'
         '+ /foo\n- /bar\n- /baz'
     )
     )
@@ -264,12 +265,12 @@ def test_collect_special_file_paths_with_dry_run_and_borgmatic_runtime_directory
         config={},
         config={},
         local_path=None,
         local_path=None,
         working_directory=None,
         working_directory=None,
-        borg_environment=None,
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
     ) == ('/foo', '/bar', '/baz')
     ) == ('/foo', '/bar', '/baz')
 
 
 
 
 def test_collect_special_file_paths_excludes_non_special_files():
 def test_collect_special_file_paths_excludes_non_special_files():
+    flexmock(module.environment).should_receive('make_environment').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
     flexmock(module).should_receive('execute_command_and_capture_output').and_return(
         '+ /foo\n+ /bar\n+ /baz'
         '+ /foo\n+ /bar\n+ /baz'
     )
     )
@@ -285,12 +286,12 @@ def test_collect_special_file_paths_excludes_non_special_files():
         config={},
         config={},
         local_path=None,
         local_path=None,
         working_directory=None,
         working_directory=None,
-        borg_environment=None,
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
     ) == ('/foo', '/baz')
     ) == ('/foo', '/baz')
 
 
 
 
 def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
 def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
+    flexmock(module.environment).should_receive('make_environment').and_return(None)
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
     flexmock(module).should_receive('execute_command_and_capture_output').with_args(
         ('borg', 'create', '--dry-run', '--list'),
         ('borg', 'create', '--dry-run', '--list'),
         capture_stderr=True,
         capture_stderr=True,
@@ -309,7 +310,6 @@ def test_collect_special_file_paths_omits_exclude_no_dump_flag_from_command():
         config={},
         config={},
         local_path='borg',
         local_path='borg',
         working_directory=None,
         working_directory=None,
-        borg_environment=None,
         borgmatic_runtime_directory='/run/borgmatic',
         borgmatic_runtime_directory='/run/borgmatic',
     )
     )
 
 

+ 16 - 0
tests/unit/borg/test_passcommand.py

@@ -53,3 +53,19 @@ def test_get_passphrase_from_passcommand_with_configured_passphrase_and_passcomm
         )
         )
         is None
         is None
     )
     )
+
+
+def test_get_passphrase_from_passcommand_with_configured_blank_passphrase_and_passcommand_detects_passphrase():
+    flexmock(module.borgmatic.config.paths).should_receive('get_working_directory').and_return(
+        '/working'
+    )
+    flexmock(module).should_receive('run_passcommand').with_args(
+        'command', True, '/working'
+    ).and_return(None).once()
+
+    assert (
+        module.get_passphrase_from_passcommand(
+            {'encryption_passphrase': '', 'encryption_passcommand': 'command'},
+        )
+        is None
+    )

+ 19 - 33
tests/unit/test_logger.py

@@ -228,16 +228,12 @@ def test_add_logging_level_skips_global_setting_if_already_set():
     module.add_logging_level('PLAID', 99)
     module.add_logging_level('PLAID', 99)
 
 
 
 
-def test_get_log_prefix_gets_prefix_from_first_handler():
+def test_get_log_prefix_gets_prefix_from_first_handler_formatter_with_prefix():
     flexmock(module.logging).should_receive('getLogger').and_return(
     flexmock(module.logging).should_receive('getLogger').and_return(
         flexmock(
         flexmock(
             handlers=[
             handlers=[
-                flexmock(
-                    formatter=flexmock(
-                        _style=flexmock(_defaults=flexmock(get=lambda name: 'myprefix: '))
-                    )
-                ),
-                flexmock(),
+                flexmock(formatter=flexmock()),
+                flexmock(formatter=flexmock(prefix='myprefix')),
             ],
             ],
             removeHandler=lambda handler: None,
             removeHandler=lambda handler: None,
         )
         )
@@ -261,8 +257,8 @@ def test_get_log_prefix_with_no_formatters_does_not_raise():
     flexmock(module.logging).should_receive('getLogger').and_return(
     flexmock(module.logging).should_receive('getLogger').and_return(
         flexmock(
         flexmock(
             handlers=[
             handlers=[
-                flexmock(),
-                flexmock(),
+                flexmock(formatter=None),
+                flexmock(formatter=None),
             ],
             ],
             removeHandler=lambda handler: None,
             removeHandler=lambda handler: None,
         )
         )
@@ -276,9 +272,8 @@ def test_get_log_prefix_with_no_prefix_does_not_raise():
         flexmock(
         flexmock(
             handlers=[
             handlers=[
                 flexmock(
                 flexmock(
-                    formatter=flexmock(_style=flexmock(_defaults=flexmock(get=lambda name: None)))
+                    formatter=flexmock(),
                 ),
                 ),
-                flexmock(),
             ],
             ],
             removeHandler=lambda handler: None,
             removeHandler=lambda handler: None,
         )
         )
@@ -287,24 +282,20 @@ def test_get_log_prefix_with_no_prefix_does_not_raise():
     assert module.get_log_prefix() is None
     assert module.get_log_prefix() is None
 
 
 
 
-def test_set_log_prefix_updates_all_handlers():
-    styles = (
-        flexmock(_defaults=None),
-        flexmock(_defaults=None),
+def test_set_log_prefix_updates_all_handler_formatters():
+    formatters = (
+        flexmock(prefix=None),
+        flexmock(prefix=None),
     )
     )
 
 
     flexmock(module.logging).should_receive('getLogger').and_return(
     flexmock(module.logging).should_receive('getLogger').and_return(
         flexmock(
         flexmock(
             handlers=[
             handlers=[
                 flexmock(
                 flexmock(
-                    formatter=flexmock(
-                        _style=styles[0],
-                    )
+                    formatter=formatters[0],
                 ),
                 ),
                 flexmock(
                 flexmock(
-                    formatter=flexmock(
-                        _style=styles[1],
-                    )
+                    formatter=formatters[1],
                 ),
                 ),
             ],
             ],
             removeHandler=lambda handler: None,
             removeHandler=lambda handler: None,
@@ -313,12 +304,12 @@ def test_set_log_prefix_updates_all_handlers():
 
 
     module.set_log_prefix('myprefix')
     module.set_log_prefix('myprefix')
 
 
-    for style in styles:
-        assert style._defaults == {'prefix': 'myprefix: '}
+    for formatter in formatters:
+        assert formatter.prefix == 'myprefix'
 
 
 
 
 def test_set_log_prefix_skips_handlers_without_a_formatter():
 def test_set_log_prefix_skips_handlers_without_a_formatter():
-    style = flexmock(_defaults=None)
+    formatter = flexmock(prefix=None)
 
 
     flexmock(module.logging).should_receive('getLogger').and_return(
     flexmock(module.logging).should_receive('getLogger').and_return(
         flexmock(
         flexmock(
@@ -326,11 +317,8 @@ def test_set_log_prefix_skips_handlers_without_a_formatter():
                 flexmock(
                 flexmock(
                     formatter=None,
                     formatter=None,
                 ),
                 ),
-                flexmock(),
                 flexmock(
                 flexmock(
-                    formatter=flexmock(
-                        _style=style,
-                    )
+                    formatter=formatter,
                 ),
                 ),
             ],
             ],
             removeHandler=lambda handler: None,
             removeHandler=lambda handler: None,
@@ -339,7 +327,7 @@ def test_set_log_prefix_skips_handlers_without_a_formatter():
 
 
     module.set_log_prefix('myprefix')
     module.set_log_prefix('myprefix')
 
 
-    assert style._defaults == {'prefix': 'myprefix: '}
+    assert formatter.prefix == 'myprefix'
 
 
 
 
 def test_log_prefix_sets_prefix_and_then_restores_no_prefix_after():
 def test_log_prefix_sets_prefix_and_then_restores_no_prefix_after():
@@ -621,10 +609,8 @@ def test_configure_logging_to_both_log_file_and_syslog():
 def test_configure_logging_to_log_file_formats_with_custom_log_format():
 def test_configure_logging_to_log_file_formats_with_custom_log_format():
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module).should_receive('add_custom_log_levels')
     flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module.logging).ANSWER = module.ANSWER
-    flexmock(module.logging).should_receive('Formatter').with_args(
+    flexmock(module).should_receive('Log_prefix_formatter').with_args(
         '{message}',  # noqa: FS003
         '{message}',  # noqa: FS003
-        style='{',
-        defaults={'prefix': ''},
     ).once()
     ).once()
     fake_formatter = flexmock()
     fake_formatter = flexmock()
     flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
     flexmock(module).should_receive('Console_color_formatter').and_return(fake_formatter)
@@ -676,7 +662,7 @@ def test_configure_logging_uses_console_no_color_formatter_if_color_disabled():
     flexmock(module.logging).ANSWER = module.ANSWER
     flexmock(module.logging).ANSWER = module.ANSWER
     fake_formatter = flexmock()
     fake_formatter = flexmock()
     flexmock(module).should_receive('Console_color_formatter').never()
     flexmock(module).should_receive('Console_color_formatter').never()
-    flexmock(module).should_receive('Console_no_color_formatter').and_return(fake_formatter)
+    flexmock(module).should_receive('Log_prefix_formatter').and_return(fake_formatter)
     multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
     multi_stream_handler = flexmock(setLevel=lambda level: None, level=logging.INFO)
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     multi_stream_handler.should_receive('setFormatter').with_args(fake_formatter).once()
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)
     flexmock(module).should_receive('Multi_stream_handler').and_return(multi_stream_handler)

+ 1 - 1
tox.ini

@@ -1,5 +1,5 @@
 [tox]
 [tox]
-env_list = py39,py310,py311,py312
+env_list = py39,py310,py311,py312,py313
 skip_missing_interpreters = True
 skip_missing_interpreters = True
 package = editable
 package = editable
 min_version = 4.0
 min_version = 4.0