From ef0eca046931400e82194b2ecec23622f5896d95 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 24 Feb 2026 14:57:36 +0100 Subject: [PATCH 01/27] make validators idempotent --- src/borg/compress.pyx | 3 +++ src/borg/helpers/parseformat.py | 11 +++++++++++ src/borg/helpers/time.py | 2 ++ 3 files changed, 16 insertions(+) diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index ba7d124b32..a5f04998fd 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -627,6 +627,9 @@ class Compressor: class CompressionSpec: def __init__(self, s): + if isinstance(s, CompressionSpec): + self.__dict__.update(s.__dict__) + return values = s.split(',') count = len(values) if count < 1: diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 00dc14e5b0..2505954df6 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -130,6 +130,8 @@ def positive_int_validator(value): def interval(s): """Convert a string representing a valid interval to a number of seconds.""" + if isinstance(s, int): + return s seconds_in_a_minute = 60 seconds_in_an_hour = 60 * seconds_in_a_minute seconds_in_a_day = 24 * seconds_in_an_hour @@ -164,6 +166,8 @@ def interval(s): def ChunkerParams(s): + if isinstance(s, tuple): + return s params = s.strip().split(",") count = len(params) if count == 0: @@ -228,6 +232,8 @@ def ChunkerParams(s): def FilesCacheMode(s): ENTRIES_MAP = dict(ctime="c", mtime="m", size="s", inode="i", rechunk="r", disabled="d") VALID_MODES = ("cis", "ims", "cs", "ms", "cr", "mr", "d", "s") # letters in alpha order + if s in VALID_MODES: + return s entries = set(s.strip().split(",")) if not entries <= set(ENTRIES_MAP): raise argparse.ArgumentTypeError( @@ -369,6 +375,8 @@ def __format__(self, format_spec): def parse_file_size(s): """Return int from file size (1234, 55G, 1.7T).""" + if isinstance(s, int): + return s if not s: return int(s) # will raise s = s.upper() @@ -507,6 +515,9 @@ class Location: local_re = re.compile(local_path_re, re.VERBOSE) def __init__(self, text="", overrides={}, other=False): + if isinstance(text, Location): + self.__dict__.update(text.__dict__) + return self.repo_env_var = "BORG_OTHER_REPO" if other else "BORG_REPO" self.valid = False self.proto = None diff --git a/src/borg/helpers/time.py b/src/borg/helpers/time.py index b98d7fe1f4..49a036c8f4 100644 --- a/src/borg/helpers/time.py +++ b/src/borg/helpers/time.py @@ -37,6 +37,8 @@ def utcfromtimestampns(ts_ns: int) -> datetime: def timestamp(s): """Convert a --timestamp=s argument to a datetime object.""" + if isinstance(s, datetime): + return s try: # is it pointing to a file / directory? ts_ns = safe_ns(os.stat(s).st_mtime_ns) From 4f1736d88026c7e567d1b662b262d40ecf05d097 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 24 Feb 2026 15:11:31 +0100 Subject: [PATCH 02/27] streamline function names for borg key subcommands --- src/borg/archiver/key_cmds.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index 5d7a9503ee..c00d6f80b4 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -18,7 +18,7 @@ class KeysMixIn: @with_repository(compatibility=(Manifest.Operation.CHECK,)) - def do_change_passphrase(self, args, repository, manifest): + def do_key_change_passphrase(self, args, repository, manifest): """Changes the repository key file passphrase.""" key = manifest.key if not hasattr(key, "change_passphrase"): @@ -30,7 +30,7 @@ def do_change_passphrase(self, args, repository, manifest): logger.info("Key location: %s", key.find_key()) @with_repository(exclusive=True, manifest=True, cache=True, compatibility=(Manifest.Operation.CHECK,)) - def do_change_location(self, args, repository, manifest, cache): + def do_key_change_location(self, args, repository, manifest, cache): """Changes the repository key location.""" key = manifest.key if not hasattr(key, "change_passphrase"): @@ -241,12 +241,12 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): "change-passphrase", parents=[common_parser], add_help=False, - description=self.do_change_passphrase.__doc__, + description=self.do_key_change_passphrase.__doc__, epilog=change_passphrase_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, help="change the repository passphrase", ) - subparser.set_defaults(func=self.do_change_passphrase) + subparser.set_defaults(func=self.do_key_change_passphrase) change_location_epilog = process_epilog( """ @@ -265,12 +265,12 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): "change-location", parents=[common_parser], add_help=False, - description=self.do_change_location.__doc__, + description=self.do_key_change_location.__doc__, epilog=change_location_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, help="change the key location", ) - subparser.set_defaults(func=self.do_change_location) + subparser.set_defaults(func=self.do_key_change_location) subparser.add_argument( "key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location" ) From e3dcf987750d506828d7f847c6c459bae437d1eb Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 24 Feb 2026 18:06:05 +0100 Subject: [PATCH 03/27] refactor: separate pattern roots from positional paths Previously, ArgparsePatternAction and ArgparsePatternFileAction appended recursion roots directly to args.paths. This mixed CLI positional paths with paths derived from patterns (e.g., using the `R` root path command in a pattern file), complicating downstream argument parsing and future jsonargparse integration. This commit introduces `args.pattern_roots` as a dedicated list for these accumulated root paths: - All argparse definition sites now initialize `pattern_roots=[]` alongside `paths=[]` - ArgparsePatternAction and ArgparsePatternFileAction write directly to `args.pattern_roots` - The build_matcher utility accepts both `include_paths` and `pattern_roots` and concatenates them internally - `create_cmd` iterations explicitly concatenate both lists before processing This ensures `args.paths` strictly reflects exactly what the user provided positionally, paving the way for a clean jsonargparse implementation without regressions in pattern behavior. --- src/borg/archiver/__init__.py | 9 ++++----- src/borg/archiver/_common.py | 5 +++-- src/borg/archiver/create_cmd.py | 3 ++- src/borg/archiver/diff_cmd.py | 1 + src/borg/archiver/extract_cmd.py | 1 + src/borg/archiver/list_cmd.py | 1 + src/borg/archiver/recreate_cmd.py | 1 + src/borg/archiver/tar_cmds.py | 1 + src/borg/fuse.py | 1 + src/borg/hlfuse.py | 1 + src/borg/patterns.py | 4 ++-- 11 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 834bf7b5cf..6d6cb68073 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -330,7 +330,7 @@ def build_parser(self): parser = argparse.ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False) # paths and patterns must have an empty list as default everywhere - parser.set_defaults(fallback2_func=functools.partial(self.do_maincommand_help, parser), paths=[], patterns=[]) + parser.set_defaults(fallback2_func=functools.partial(self.do_maincommand_help, parser), paths=[], patterns=[], pattern_roots=[]) parser.common_options = self.CommonOptions( define_common_options, suffix_precedence=("_maincommand", "_midcommand", "_subcommand") ) @@ -341,11 +341,11 @@ def build_parser(self): parser.common_options.add_common_group(parser, "_maincommand", provide_defaults=True) common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) - common_parser.set_defaults(paths=[], patterns=[]) + common_parser.set_defaults(paths=[], patterns=[], pattern_roots=[]) parser.common_options.add_common_group(common_parser, "_subcommand") mid_common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) - mid_common_parser.set_defaults(paths=[], patterns=[]) + mid_common_parser.set_defaults(paths=[], patterns=[], pattern_roots=[]) parser.common_options.add_common_group(mid_common_parser, "_midcommand") if parser.prog == "borgfs": @@ -433,8 +433,7 @@ def parse_args(self, args=None): if func == self.do_create and not args.paths: if args.content_from_command or args.paths_from_command: parser.error("No command given.") - elif not args.paths_from_stdin: - # need at least 1 path but args.paths may also be populated from patterns + elif not args.paths_from_stdin and not args.pattern_roots: parser.error("Need at least one PATH argument.") # we can only have a complete knowledge of placeholder replacements we should do **after** arg parsing, # e.g. due to options like --timestamp that override the current time. diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 673e53d41a..2369f49f3e 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -574,10 +574,11 @@ def define_common_options(add_common_option): ) -def build_matcher(inclexcl_patterns, include_paths): +def build_matcher(inclexcl_patterns, include_paths, pattern_roots=()): matcher = PatternMatcher() matcher.add_inclexcl(inclexcl_patterns) - matcher.add_includepaths(include_paths) + paths = list(pattern_roots) + list(include_paths) + matcher.add_includepaths(paths) return matcher diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 426adc0edb..bd6cb8caa0 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -137,7 +137,8 @@ def create_inner(archive, cache, fso): if rc != 0: raise CommandError(f"Command {args.paths[0]!r} exited with status {rc}") else: - for path in args.paths: + paths = list(args.pattern_roots) + list(args.paths) + for path in paths: if path == "": # issue #5637 self.print_warning("An empty string was given as PATH, ignoring.") continue diff --git a/src/borg/archiver/diff_cmd.py b/src/borg/archiver/diff_cmd.py index 0ea0954e84..87b47a7bd6 100644 --- a/src/borg/archiver/diff_cmd.py +++ b/src/borg/archiver/diff_cmd.py @@ -84,6 +84,7 @@ def print_text_output(diff, formatter): wc=None, ) + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(args.patterns, args.paths) diffs_iter = Archive.compare_archives_iter( diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index a3885a0c11..1a020f371b 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -33,6 +33,7 @@ def do_extract(self, args, repository, manifest, archive): "For example, install locales and use: LANG=en_US.UTF-8" ) + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(args.patterns, args.paths) progress = args.progress diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index e3c13679fe..0df719c427 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -19,6 +19,7 @@ class ListMixIn: @with_repository(compatibility=(Manifest.Operation.READ,)) def do_list(self, args, repository, manifest): """List archive contents.""" + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(args.patterns, args.paths) if args.format is not None: format = args.format diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 4ab928e251..149f665a94 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -18,6 +18,7 @@ class RecreateMixIn: @with_repository(cache=True, compatibility=(Manifest.Operation.CHECK,)) def do_recreate(self, args, repository, manifest, cache): """Recreate archives.""" + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(args.patterns, args.paths) self.output_list = args.output_list self.output_filter = args.output_filter diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index af67d6d104..fe0d7bd1d3 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -86,6 +86,7 @@ def do_export_tar(self, args, repository, manifest, archive): self._export_tar(args, archive, _stream) def _export_tar(self, args, archive, tarstream): + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(args.patterns, args.paths) progress = args.progress diff --git a/src/borg/fuse.py b/src/borg/fuse.py index 15c1e13d46..10cf6c4bd6 100644 --- a/src/borg/fuse.py +++ b/src/borg/fuse.py @@ -370,6 +370,7 @@ def _process_archive(self, archive_id, prefix=[]): t0 = time.perf_counter() archive = Archive(self._manifest, archive_id) strip_components = self._args.strip_components + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(self._args.patterns, self._args.paths) hlm = HardLinkManager(id_type=bytes, info_type=str) # hlid -> path diff --git a/src/borg/hlfuse.py b/src/borg/hlfuse.py index c08475961e..d05cd71276 100644 --- a/src/borg/hlfuse.py +++ b/src/borg/hlfuse.py @@ -193,6 +193,7 @@ def _process_archive(self, archive_id, root_node=None): archive = Archive(self._manifest, archive_id) strip_components = self._args.strip_components + # omitting args.pattern_roots here, restricting to paths only by cli args.paths: matcher = build_matcher(self._args.patterns, self._args.paths) hlm = HardLinkManager(id_type=bytes, info_type=str) diff --git a/src/borg/patterns.py b/src/borg/patterns.py index c1f8f57277..8c03987b77 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -41,7 +41,7 @@ def __init__(self, nargs=1, **kw): super().__init__(nargs=nargs, **kw) def __call__(self, parser, args, values, option_string=None): - parse_patternfile_line(values[0], args.paths, args.patterns, ShellPattern) + parse_patternfile_line(values[0], args.pattern_roots, args.patterns, ShellPattern) class ArgparsePatternFileAction(argparse.Action): @@ -60,7 +60,7 @@ def __call__(self, parser, args, values, option_string=None): raise Error(str(e)) def parse(self, fobj, args): - load_pattern_file(fobj, args.paths, args.patterns) + load_pattern_file(fobj, args.pattern_roots, args.patterns) class ArgparseExcludeFileAction(ArgparsePatternFileAction): From 2cff41d89441b8743022060811d8f8c7d719cabc Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Tue, 24 Feb 2026 22:40:22 +0100 Subject: [PATCH 04/27] migrate to jsonargparse --- pyproject.toml | 1 + src/borg/archiver/__init__.py | 54 +++++---- src/borg/archiver/_common.py | 20 +++- src/borg/archiver/analyze_cmd.py | 8 +- src/borg/archiver/benchmark_cmd.py | 23 ++-- src/borg/archiver/check_cmd.py | 11 +- src/borg/archiver/compact_cmd.py | 8 +- src/borg/archiver/completion_cmd.py | 8 +- src/borg/archiver/create_cmd.py | 10 +- src/borg/archiver/debug_cmd.py | 115 ++++++++------------ src/borg/archiver/delete_cmd.py | 8 +- src/borg/archiver/diff_cmd.py | 8 +- src/borg/archiver/extract_cmd.py | 8 +- src/borg/archiver/help_cmd.py | 16 ++- src/borg/archiver/info_cmd.py | 8 +- src/borg/archiver/key_cmds.py | 35 +++--- src/borg/archiver/list_cmd.py | 8 +- src/borg/archiver/lock_cmds.py | 14 +-- src/borg/archiver/mount_cmds.py | 14 +-- src/borg/archiver/prune_cmd.py | 8 +- src/borg/archiver/recreate_cmd.py | 8 +- src/borg/archiver/rename_cmd.py | 8 +- src/borg/archiver/repo_compress_cmd.py | 8 +- src/borg/archiver/repo_create_cmd.py | 8 +- src/borg/archiver/repo_delete_cmd.py | 8 +- src/borg/archiver/repo_info_cmd.py | 8 +- src/borg/archiver/repo_list_cmd.py | 8 +- src/borg/archiver/repo_space_cmd.py | 8 +- src/borg/archiver/serve_cmd.py | 7 +- src/borg/archiver/tag_cmd.py | 20 ++-- src/borg/archiver/tar_cmds.py | 14 +-- src/borg/archiver/transfer_cmd.py | 8 +- src/borg/archiver/undelete_cmd.py | 8 +- src/borg/archiver/version_cmd.py | 7 +- src/borg/helpers/jap_helpers.py | 33 ++++++ src/borg/helpers/parseformat.py | 4 +- src/borg/testsuite/archiver/tag_cmd_test.py | 4 +- 37 files changed, 287 insertions(+), 267 deletions(-) create mode 100644 src/borg/helpers/jap_helpers.py diff --git a/pyproject.toml b/pyproject.toml index bd175a5cbc..ad6539a60b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "shtab>=1.8.0", "backports-zstd; python_version < '3.14'", # for python < 3.14. "xxhash>=2.0.0", + "jsonargparse @ git+https://github.com/omni-us/jsonargparse.git@main", ] [project.optional-dependencies] diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 6d6cb68073..7ee8792b0f 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -27,6 +27,8 @@ import signal from datetime import datetime, timezone + from jsonargparse import ArgumentParser + from ..logger import create_logger, setup_logging logger = create_logger() @@ -40,6 +42,7 @@ from ..helpers import format_file_size from ..helpers import remove_surrogates, text_to_json from ..helpers import DatetimeWrapper, replace_placeholders + from ..helpers.jap_helpers import flatten_namespace from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm @@ -63,16 +66,7 @@ PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance." -def get_func(args): - # This works around https://bugs.python.org/issue9351 - # func is used at the leaf parsers of the argparse parser tree, - # fallback_func at next level towards the root, - # fallback2_func at the 2nd next level (which is root in our case). - for name in "func", "fallback_func", "fallback2_func": - func = getattr(args, name, None) - if func is not None: - return func - raise Exception("expected func attributes not found") + from .analyze_cmd import AnalyzeMixIn @@ -277,7 +271,7 @@ def add_argument(*args, **kwargs): # Note: We control all inputs. kwargs["help"] = kwargs["help"] % kwargs if not is_append: - kwargs["default"] = self.default_sentinel + kwargs["default"] = argparse.SUPPRESS common_group.add_argument(*args, **kwargs) @@ -328,9 +322,8 @@ def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise def build_parser(self): from ._common import define_common_options - parser = argparse.ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False) + parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False) # paths and patterns must have an empty list as default everywhere - parser.set_defaults(fallback2_func=functools.partial(self.do_maincommand_help, parser), paths=[], patterns=[], pattern_roots=[]) parser.common_options = self.CommonOptions( define_common_options, suffix_precedence=("_maincommand", "_midcommand", "_subcommand") ) @@ -340,18 +333,16 @@ def build_parser(self): parser.add_argument("--cockpit", dest="cockpit", action="store_true", help="Start the Borg TUI") parser.common_options.add_common_group(parser, "_maincommand", provide_defaults=True) - common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) - common_parser.set_defaults(paths=[], patterns=[], pattern_roots=[]) + common_parser = ArgumentParser(add_help=False, prog=self.prog) parser.common_options.add_common_group(common_parser, "_subcommand") - mid_common_parser = argparse.ArgumentParser(add_help=False, prog=self.prog) - mid_common_parser.set_defaults(paths=[], patterns=[], pattern_roots=[]) + mid_common_parser = ArgumentParser(add_help=False, prog=self.prog) parser.common_options.add_common_group(mid_common_parser, "_midcommand") if parser.prog == "borgfs": return self.build_parser_borgfs(parser) - subparsers = parser.add_subparsers(title="required arguments", metavar="") + subparsers = parser.add_subcommands(required=False, title="required arguments", metavar="") self.build_parser_analyze(subparsers, common_parser, mid_common_parser) self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser) @@ -424,8 +415,15 @@ def parse_args(self, args=None): args = self.preprocess_args(args) parser = self.build_parser() args = parser.parse_args(args or ["-h"]) + args = flatten_namespace(args) + + # Ensure list defaults previously handled by set_defaults are present + for list_attr in ("paths", "patterns", "pattern_roots"): + if getattr(args, list_attr, None) is None: + setattr(args, list_attr, []) + parser.common_options.resolve(args) - func = get_func(args) + func = self.get_func(args, parser) if func == self.do_create and args.paths and args.paths_from_stdin: parser.error("Must not pass PATH with --paths-from-stdin.") if args.progress and getattr(args, "output_list", False) and not args.log_json: @@ -451,8 +449,24 @@ def parse_args(self, args=None): if value: setattr(args, name, [replace_placeholders(elem) for elem in value]) + args.func = func + return args + def get_func(self, args, parser): + if not getattr(args, "subcommand", None): + return functools.partial(self.do_maincommand_help, parser) + + method_name = "do_" + args.subcommand.replace(" ", "_").replace("-", "_") + func = getattr(self, method_name, None) + if func is not None: + if method_name == "do_help": + return functools.partial(func, parser) + return func + + # fallback to general help for e.g., "borg key" + return functools.partial(self.do_maincommand_help, parser) + def prerun_checks(self, logger, is_serve): selftest(logger) @@ -485,7 +499,7 @@ def _setup_topic_debugging(self, args): def run(self, args): os.umask(args.umask) # early, before opening files self.lock_wait = args.lock_wait - func = get_func(args) + func = args.func # do not use loggers before this! is_serve = func == self.do_serve self.log_json = args.log_json and not is_serve diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 2369f49f3e..9ae39a769d 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -1,3 +1,4 @@ +import argparse import functools import os import textwrap @@ -268,6 +269,20 @@ def process_epilog(epilog): def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): + add_option( + "--pattern-roots-internal", + dest="pattern_roots", + action="append", + default=[], + help=argparse.SUPPRESS, + ) + add_option( + "--patterns-internal", + dest="patterns", + action="append", + default=[], + help=argparse.SUPPRESS, + ) add_option( "-e", "--exclude", @@ -275,6 +290,7 @@ def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components dest="patterns", type=parse_exclude_pattern, action="append", + default=[], help="exclude paths matching PATTERN", ) add_option( @@ -372,7 +388,6 @@ def define_archive_filters_group( metavar="N", dest="first", type=positive_int_validator, - default=0, action=Highlander, help="consider the first N archives after other filters are applied", ) @@ -381,7 +396,6 @@ def define_archive_filters_group( metavar="N", dest="last", type=positive_int_validator, - default=0, action=Highlander, help="consider the last N archives after other filters are applied", ) @@ -508,7 +522,7 @@ def define_common_options(add_common_option): "--umask", metavar="M", dest="umask", - type=lambda s: int(s, 8), + type=lambda s: s if isinstance(s, int) else int(s, 8), default=UMASK_DEFAULT, action=Highlander, help="set umask to M (local only, default: %(default)04o)", diff --git a/src/borg/archiver/analyze_cmd.py b/src/borg/archiver/analyze_cmd.py index e556095886..1d99075ad6 100644 --- a/src/borg/archiver/analyze_cmd.py +++ b/src/borg/archiver/analyze_cmd.py @@ -2,6 +2,8 @@ from collections import defaultdict import os +from jsonargparse import ArgumentParser + from ._common import with_repository, define_archive_filters_group from ..archive import Archive from ..constants import * # NOQA @@ -126,14 +128,12 @@ def build_parser_analyze(self, subparsers, common_parser, mid_common_parser): to recreate existing archives without them. """ ) - subparser = subparsers.add_parser( - "analyze", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_analyze.__doc__, epilog=analyze_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="analyze archives", ) - subparser.set_defaults(func=self.do_analyze) + subparsers.add_subcommand("analyze", subparser, help="analyze archives") define_archive_filters_group(subparser) diff --git a/src/borg/archiver/benchmark_cmd.py b/src/borg/archiver/benchmark_cmd.py index cf346e65a1..ce317e4289 100644 --- a/src/borg/archiver/benchmark_cmd.py +++ b/src/borg/archiver/benchmark_cmd.py @@ -1,12 +1,13 @@ import argparse from contextlib import contextmanager -import functools import json import logging import os import tempfile import time +from jsonargparse import ArgumentParser + from ..constants import * # NOQA from ..crypto.key import FlexiKey from ..helpers import format_file_size @@ -355,18 +356,16 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): benchmark_epilog = process_epilog("These commands do various benchmarks.") - subparser = subparsers.add_parser( - "benchmark", + subparser = ArgumentParser( parents=[mid_common_parser], add_help=False, description="benchmark command", epilog=benchmark_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="benchmark command", ) + subparsers.add_subcommand("benchmark", subparser, help="benchmark command") - benchmark_parsers = subparser.add_subparsers(title="required arguments", metavar="") - subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) + benchmark_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="") bench_crud_epilog = process_epilog( """ @@ -409,16 +408,14 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): Try multiple measurements and having a otherwise idle machine (and network, if you use it). """ ) - subparser = benchmark_parsers.add_parser( - "crud", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_benchmark_crud.__doc__, epilog=bench_crud_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="benchmarks Borg CRUD (create, extract, update, delete).", ) - subparser.set_defaults(func=self.do_benchmark_crud) + benchmark_parsers.add_subcommand("crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete).") subparser.add_argument("path", metavar="PATH", help="path where to create benchmark input data") subparser.add_argument("--json-lines", action="store_true", help="Format output as JSON Lines.") @@ -434,14 +431,12 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): - enough free memory so there will be no slow down due to paging activity """ ) - subparser = benchmark_parsers.add_parser( - "cpu", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_benchmark_cpu.__doc__, epilog=bench_cpu_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="benchmarks Borg CPU-bound operations.", ) - subparser.set_defaults(func=self.do_benchmark_cpu) + benchmark_parsers.add_subcommand("cpu", subparser, help="benchmarks Borg CPU-bound operations.") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index e78aef563f..a39232468b 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -1,4 +1,7 @@ import argparse + +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from ..archive import ArchiveChecker from ..constants import * # NOQA @@ -60,7 +63,7 @@ def do_check(self, args, repository): repair=args.repair, find_lost_archives=args.find_lost_archives, match=args.match_archives, - sort_by=args.sort_by or "ts", + sort_by=args.sort_by or "timestamp", first=args.first, last=args.last, older=args.older, @@ -182,16 +185,14 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): ``borg compact`` would remove the archives' data completely. """ ) - subparser = subparsers.add_parser( - "check", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_check.__doc__, epilog=check_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="verify the repository", ) - subparser.set_defaults(func=self.do_check) + subparsers.add_subcommand("check", subparser, help="verify the repository") subparser.add_argument( "--repository-only", dest="repo_only", action="store_true", help="only perform repository checks" ) diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 1cd07a0ba9..4e1cbe1c92 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -1,6 +1,8 @@ import argparse from pathlib import Path +from jsonargparse import ArgumentParser + from ._common import with_repository from ..archive import Archive from ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo @@ -257,16 +259,14 @@ def build_parser_compact(self, subparsers, common_parser, mid_common_parser): thus it cannot compute before/after compaction size statistics). """ ) - subparser = subparsers.add_parser( - "compact", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_compact.__doc__, epilog=compact_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="compact the repository", ) - subparser.set_defaults(func=self.do_compact) + subparsers.add_subcommand("compact", subparser, help="compact the repository") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" ) diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index abcc04f792..9120dd965d 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -54,6 +54,8 @@ import shtab +from jsonargparse import ArgumentParser + from ._common import process_epilog from ..constants import * # NOQA from ..helpers import ( @@ -750,16 +752,14 @@ def build_parser_completion(self, subparsers, common_parser, mid_common_parser): """ ) - subparser = subparsers.add_parser( - "completion", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_completion.__doc__, epilog=completion_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="output shell completion script", ) - subparser.set_defaults(func=self.do_completion) + subparsers.add_subcommand("completion", subparser, help="output shell completion script") subparser.add_argument( "shell", metavar="SHELL", choices=shells, help="shell to generate completion for (one of: %(choices)s)" ) diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index bd6cb8caa0..6313837e26 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -9,6 +9,8 @@ import time from io import TextIOWrapper +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from .. import helpers from ..archive import Archive, is_special @@ -772,16 +774,14 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): """ ) - subparser = subparsers.add_parser( - "create", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_create.__doc__, epilog=create_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="create a backup", ) - subparser.set_defaults(func=self.do_create) + subparsers.add_subcommand("create", subparser, help="create a backup") # note: --dry-run and --stats are mutually exclusive, but we do not want to abort when # parsing, but rather proceed with the dry-run, but without stats (see run() method). @@ -831,7 +831,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): "--stdin-mode", metavar="M", dest="stdin_mode", - type=lambda s: int(s, 8), + type=lambda s: s if isinstance(s, int) else int(s, 8), default=STDIN_MODE_DEFAULT, action=Highlander, help="set mode to M in archive for stdin data (default: %(default)04o)", diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index ed3d4ee51d..2f311320f3 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -1,8 +1,9 @@ import argparse -import functools import json import textwrap +from jsonargparse import ArgumentParser + from ..archive import Archive from ..compress import CompressionSpec from ..constants import * # NOQA @@ -319,18 +320,16 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): what you are doing or if a trusted developer tells you what to do.""" ) - subparser = subparsers.add_parser( - "debug", + subparser = ArgumentParser( parents=[mid_common_parser], add_help=False, description="debugging command (not intended for normal use)", epilog=debug_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="debugging command (not intended for normal use)", ) + subparsers.add_subcommand("debug", subparser, help="debugging command (not intended for normal use)") - debug_parsers = subparser.add_subparsers(title="required arguments", metavar="") - subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) + debug_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="") debug_info_epilog = process_epilog( """ @@ -339,32 +338,28 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): already appended at the end of the traceback. """ ) - subparser = debug_parsers.add_parser( - "info", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_info.__doc__, epilog=debug_info_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="show system infos for debugging / bug reports (debug)", ) - subparser.set_defaults(func=self.do_debug_info) + debug_parsers.add_subcommand("info", subparser, help="show system infos for debugging / bug reports (debug)") debug_dump_archive_items_epilog = process_epilog( """ This command dumps raw (but decrypted and decompressed) archive items (only metadata) to files. """ ) - subparser = debug_parsers.add_parser( - "dump-archive-items", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_dump_archive_items.__doc__, epilog=debug_dump_archive_items_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="dump archive items (metadata) (debug)", ) - subparser.set_defaults(func=self.do_debug_dump_archive_items) + debug_parsers.add_subcommand("dump-archive-items", subparser, help="dump archive items (metadata) (debug)") subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") debug_dump_archive_epilog = process_epilog( @@ -372,16 +367,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command dumps all metadata of an archive in a decoded form to a file. """ ) - subparser = debug_parsers.add_parser( - "dump-archive", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_dump_archive.__doc__, epilog=debug_dump_archive_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="dump decoded archive metadata (debug)", ) - subparser.set_defaults(func=self.do_debug_dump_archive) + debug_parsers.add_subcommand("dump-archive", subparser, help="dump decoded archive metadata (debug)") subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into") @@ -390,16 +383,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command dumps manifest metadata of a repository in a decoded form to a file. """ ) - subparser = debug_parsers.add_parser( - "dump-manifest", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_dump_manifest.__doc__, epilog=debug_dump_manifest_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="dump decoded repository metadata (debug)", ) - subparser.set_defaults(func=self.do_debug_dump_manifest) + debug_parsers.add_subcommand("dump-manifest", subparser, help="dump decoded repository metadata (debug)") subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into") debug_dump_repo_objs_epilog = process_epilog( @@ -407,32 +398,28 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command dumps raw (but decrypted and decompressed) repo objects to files. """ ) - subparser = debug_parsers.add_parser( - "dump-repo-objs", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_dump_repo_objs.__doc__, epilog=debug_dump_repo_objs_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="dump repo objects (debug)", ) - subparser.set_defaults(func=self.do_debug_dump_repo_objs) + debug_parsers.add_subcommand("dump-repo-objs", subparser, help="dump repo objects (debug)") debug_search_repo_objs_epilog = process_epilog( """ This command searches raw (but decrypted and decompressed) repo objects for a specific bytes sequence. """ ) - subparser = debug_parsers.add_parser( - "search-repo-objs", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_search_repo_objs.__doc__, epilog=debug_search_repo_objs_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="search repo objects (debug)", ) - subparser.set_defaults(func=self.do_debug_search_repo_objs) + debug_parsers.add_subcommand("search-repo-objs", subparser, help="search repo objects (debug)") subparser.add_argument( "wanted", metavar="WANTED", @@ -445,16 +432,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command computes the id-hash for some file content. """ ) - subparser = debug_parsers.add_parser( - "id-hash", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_id_hash.__doc__, epilog=debug_id_hash_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="compute id-hash for some file content (debug)", ) - subparser.set_defaults(func=self.do_debug_id_hash) + debug_parsers.add_subcommand("id-hash", subparser, help="compute id-hash for some file content (debug)") subparser.add_argument( "path", metavar="PATH", type=str, help="content for which the id-hash shall get computed" ) @@ -465,16 +450,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command parses the object file into metadata (as json) and uncompressed data. """ ) - subparser = debug_parsers.add_parser( - "parse-obj", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_parse_obj.__doc__, epilog=debug_parse_obj_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="parse borg object file into meta dict and data", ) - subparser.set_defaults(func=self.do_debug_parse_obj) + debug_parsers.add_subcommand("parse-obj", subparser, help="parse borg object file into meta dict and data") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") subparser.add_argument( "object_path", metavar="OBJECT_PATH", type=str, help="path of the object file to parse data from" @@ -492,16 +475,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command formats the file and metadata into a Borg object file. """ ) - subparser = debug_parsers.add_parser( - "format-obj", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_format_obj.__doc__, epilog=debug_format_obj_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="format file and metadata into a Borg object file", ) - subparser.set_defaults(func=self.do_debug_format_obj) + debug_parsers.add_subcommand("format-obj", subparser, help="format file and metadata into a Borg object file") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") subparser.add_argument( "binary_path", metavar="BINARY_PATH", type=str, help="path of the file to convert into an object file" @@ -531,16 +512,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command gets an object from the repository. """ ) - subparser = debug_parsers.add_parser( - "get-obj", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_get_obj.__doc__, epilog=debug_get_obj_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="get object from repository (debug)", ) - subparser.set_defaults(func=self.do_debug_get_obj) + debug_parsers.add_subcommand("get-obj", subparser, help="get object from repository (debug)") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") subparser.add_argument("path", metavar="PATH", type=str, help="file to write object data into") @@ -549,16 +528,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command puts an object into the repository. """ ) - subparser = debug_parsers.add_parser( - "put-obj", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_put_obj.__doc__, epilog=debug_put_obj_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="put object to repository (debug)", ) - subparser.set_defaults(func=self.do_debug_put_obj) + debug_parsers.add_subcommand("put-obj", subparser, help="put object to repository (debug)") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to put into the repo") subparser.add_argument("path", metavar="PATH", type=str, help="file to read and create object from") @@ -567,16 +544,14 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): This command deletes objects from the repository. """ ) - subparser = debug_parsers.add_parser( - "delete-obj", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_delete_obj.__doc__, epilog=debug_delete_obj_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="delete object from repository (debug)", ) - subparser.set_defaults(func=self.do_debug_delete_obj) + debug_parsers.add_subcommand("delete-obj", subparser, help="delete object from repository (debug)") subparser.add_argument( "ids", metavar="IDs", nargs="+", type=str, help="hex object ID(s) to delete from the repo" ) @@ -586,15 +561,13 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): Convert a Borg profile to a Python cProfile compatible profile. """ ) - subparser = debug_parsers.add_parser( - "convert-profile", - parents=[common_parser], + subparser = ArgumentParser( + parents=[mid_common_parser], add_help=False, description=self.do_debug_convert_profile.__doc__, epilog=debug_convert_profile_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="convert Borg profile to Python profile (debug)", ) - subparser.set_defaults(func=self.do_debug_convert_profile) + debug_parsers.add_subcommand("convert-profile", subparser, help="convert Borg profile to Python profile (debug)") subparser.add_argument("input", metavar="INPUT", type=str, help="Borg profile") subparser.add_argument("output", metavar="OUTPUT", type=str, help="Output file") diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 49c913f934..995c6e2e41 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -1,6 +1,8 @@ import argparse import logging +from jsonargparse import ArgumentParser + from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator @@ -80,16 +82,14 @@ def build_parser_delete(self, subparsers, common_parser, mid_common_parser): patterns, see :ref:`borg_patterns`). """ ) - subparser = subparsers.add_parser( - "delete", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_delete.__doc__, epilog=delete_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="delete archives", ) - subparser.set_defaults(func=self.do_delete) + subparsers.add_subcommand("delete", subparser, help="delete archives") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" ) diff --git a/src/borg/archiver/diff_cmd.py b/src/borg/archiver/diff_cmd.py index 87b47a7bd6..ceaa0b0d27 100644 --- a/src/borg/archiver/diff_cmd.py +++ b/src/borg/archiver/diff_cmd.py @@ -4,6 +4,8 @@ import sys import os +from jsonargparse import ArgumentParser + from ._common import with_repository, build_matcher, Highlander from ..archive import Archive from ..constants import * # NOQA @@ -294,16 +296,14 @@ def diff_sort_spec_validator(s): raise argparse.ArgumentTypeError(f"unsupported sort field: {field}") return ",".join(parts) - subparser = subparsers.add_parser( - "diff", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_diff.__doc__, epilog=diff_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="find differences in archive contents", ) - subparser.set_defaults(func=self.do_diff) + subparsers.add_subcommand("diff", subparser, help="find differences in archive contents") subparser.add_argument( "--numeric-ids", dest="numeric_ids", diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index 1a020f371b..fb5a67e766 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -3,6 +3,8 @@ import logging import stat +from jsonargparse import ArgumentParser + from ._common import with_repository, with_archive from ._common import build_filter, build_matcher from ..archive import BackupError @@ -155,16 +157,14 @@ def build_parser_extract(self, subparsers, common_parser, mid_common_parser): group, permissions, etc. """ ) - subparser = subparsers.add_parser( - "extract", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_extract.__doc__, epilog=extract_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="extract archive contents", ) - subparser.set_defaults(func=self.do_extract) + subparsers.add_subcommand("extract", subparser, help="extract archive contents") subparser.add_argument( "--list", dest="output_list", action="store_true", help="output a verbose list of items (files, dirs, ...)" ) diff --git a/src/borg/archiver/help_cmd.py b/src/borg/archiver/help_cmd.py index a73ef7e909..3b1486cd0b 100644 --- a/src/borg/archiver/help_cmd.py +++ b/src/borg/archiver/help_cmd.py @@ -1,7 +1,8 @@ import collections -import functools import textwrap +from jsonargparse import ArgumentParser + from ..constants import * # NOQA from ..helpers.nanorst import rst_to_terminal @@ -523,7 +524,10 @@ class HelpMixIn: borg create --compression obfuscate,250,zstd,3 ...\n\n""" ) - def do_help(self, parser, commands, args): + def do_help(self, parser, args): + commands = getattr(parser, "_subcommands_action", None) + commands = commands._name_parser_map if commands else {} + if not args.topic: parser.print_help() elif args.topic in self.helptext: @@ -551,10 +555,12 @@ def do_subcommand_help(self, parser, args): do_maincommand_help = do_subcommand_help def build_parser_help(self, subparsers, common_parser, mid_common_parser, parser): - subparser = subparsers.add_parser( - "help", parents=[common_parser], add_help=False, description="Extra help", help="Extra help" + subparser = ArgumentParser( + parents=[common_parser], + add_help=False, + description="Extra help", ) + subparsers.add_subcommand("help", subparser, help="Extra help") subparser.add_argument("--epilog-only", dest="epilog_only", action="store_true") subparser.add_argument("--usage-only", dest="usage_only", action="store_true") - subparser.set_defaults(func=functools.partial(self.do_help, parser, subparsers.choices)) subparser.add_argument("topic", metavar="TOPIC", type=str, nargs="?", help="additional help on TOPIC") diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index dbb8b3ab60..c4a808265d 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -2,6 +2,8 @@ import textwrap from datetime import timedelta +from jsonargparse import ArgumentParser + from ._common import with_repository from ..archive import Archive from ..constants import * # NOQA @@ -78,16 +80,14 @@ def build_parser_info(self, subparsers, common_parser, mid_common_parser): = all chunks in the repository. """ ) - subparser = subparsers.add_parser( - "info", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_info.__doc__, epilog=info_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="show repository or archive information", ) - subparser.set_defaults(func=self.do_info) + subparsers.add_subcommand("info", subparser, help="show repository or archive information") subparser.add_argument("--json", action="store_true", help="format output as JSON") define_archive_filters_group(subparser) subparser.add_argument( diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index c00d6f80b4..0984a38fba 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -1,7 +1,8 @@ import argparse -import functools import os +from jsonargparse import ArgumentParser + from ..constants import * # NOQA from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey @@ -120,18 +121,16 @@ def do_key_import(self, args, repository): def build_parser_keys(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog - subparser = subparsers.add_parser( - "key", + subparser = ArgumentParser( parents=[mid_common_parser], add_help=False, description="Manage the keyfile or repokey of a repository", epilog="", formatter_class=argparse.RawDescriptionHelpFormatter, - help="manage the repository key", ) + subparsers.add_subcommand("key", subparser, help="manage the repository key") - key_parsers = subparser.add_subparsers(title="required arguments", metavar="") - subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser)) + key_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="") key_export_epilog = process_epilog( """ @@ -164,16 +163,14 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): HTML template with a QR code and a copy of the ``--paper``-formatted key. """ ) - subparser = key_parsers.add_parser( - "export", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_key_export.__doc__, epilog=key_export_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="export the repository key for backup", ) - subparser.set_defaults(func=self.do_key_export) + key_parsers.add_subcommand("export", subparser, help="export the repository key for backup") subparser.add_argument("path", metavar="PATH", nargs="?", type=PathSpec, help="where to store the backup") subparser.add_argument( "--paper", @@ -206,16 +203,14 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): key import`` creates a new key file in ``$BORG_KEYS_DIR``. """ ) - subparser = key_parsers.add_parser( - "import", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_key_import.__doc__, epilog=key_import_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="import the repository key from backup", ) - subparser.set_defaults(func=self.do_key_import) + key_parsers.add_subcommand("import", subparser, help="import the repository key from backup") subparser.add_argument( "path", metavar="PATH", nargs="?", type=PathSpec, help="path to the backup ('-' to read from stdin)" ) @@ -237,16 +232,14 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): does not protect future (nor past) backups to the same repository. """ ) - subparser = key_parsers.add_parser( - "change-passphrase", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_key_change_passphrase.__doc__, epilog=change_passphrase_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="change the repository passphrase", ) - subparser.set_defaults(func=self.do_key_change_passphrase) + key_parsers.add_subcommand("change-passphrase", subparser, help="change the repository passphrase") change_location_epilog = process_epilog( """ @@ -261,16 +254,14 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): thus you must ONLY give the key location (keyfile or repokey). """ ) - subparser = key_parsers.add_parser( - "change-location", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_key_change_location.__doc__, epilog=change_location_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="change the key location", ) - subparser.set_defaults(func=self.do_key_change_location) + key_parsers.add_subcommand("change-location", subparser, help="change the key location") subparser.add_argument( "key_mode", metavar="KEY_LOCATION", choices=("repokey", "keyfile"), help="select key location" ) diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index 0df719c427..4a355275e9 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -3,6 +3,8 @@ import textwrap import sys +from jsonargparse import ArgumentParser + from ._common import with_repository, build_matcher, Highlander from ..archive import Archive from ..cache import Cache @@ -103,16 +105,14 @@ def build_parser_list(self, subparsers, common_parser, mid_common_parser): ) + ItemFormatter.keys_help() ) - subparser = subparsers.add_parser( - "list", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_list.__doc__, epilog=list_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="list archive contents", ) - subparser.set_defaults(func=self.do_list) + subparsers.add_subcommand("list", subparser, help="list archive contents") subparser.add_argument( "--short", dest="short", action="store_true", help="only print file/directory names, nothing else" ) diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index 1739da6df3..0e5e251f27 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -1,6 +1,8 @@ import argparse import subprocess +from jsonargparse import ArgumentParser + from ._common import with_repository from ..cache import Cache from ..constants import * # NOQA @@ -45,16 +47,14 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): trying to access the cache or the repository. """ ) - subparser = subparsers.add_parser( - "break-lock", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_break_lock.__doc__, epilog=break_lock_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="break the repository and cache locks", ) - subparser.set_defaults(func=self.do_break_lock) + subparsers.add_subcommand("break-lock", subparser, help="break the repository and cache locks") with_lock_epilog = process_epilog( """ @@ -77,15 +77,13 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): Borg is cautious and does not automatically remove stale locks made by a different host. """ ) - subparser = subparsers.add_parser( - "with-lock", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_with_lock.__doc__, epilog=with_lock_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="run a user command with the lock held", ) - subparser.set_defaults(func=self.do_with_lock) + subparsers.add_subcommand("with-lock", subparser, help="run a user command with the lock held") subparser.add_argument("command", metavar="COMMAND", help="command to run") subparser.add_argument("args", metavar="ARGS", nargs=argparse.REMAINDER, help="command arguments") diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index d37d8cb47b..7a4164de1e 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -1,6 +1,8 @@ import argparse import os +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import RTError @@ -151,15 +153,14 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser the logger to output to a file. """ ) - subparser = subparsers.add_parser( - "mount", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_mount.__doc__, epilog=mount_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="mount a repository", ) + subparsers.add_subcommand("mount", subparser, help="mount a repository") self._define_borg_mount(subparser) umount_epilog = process_epilog( @@ -170,16 +171,14 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser command - usually this is either umount or fusermount -u. """ ) - subparser = subparsers.add_parser( - "umount", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_umount.__doc__, epilog=umount_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="unmount a repository", ) - subparser.set_defaults(func=self.do_umount) + subparsers.add_subcommand("umount", subparser, help="unmount a repository") subparser.add_argument( "mountpoint", metavar="MOUNTPOINT", type=str, help="mountpoint of the filesystem to unmount" ) @@ -196,7 +195,6 @@ def build_parser_borgfs(self, parser): def _define_borg_mount(self, parser): from ._common import define_exclusion_group, define_archive_filters_group - parser.set_defaults(func=self.do_mount) parser.add_argument("mountpoint", metavar="MOUNTPOINT", type=str, help="where to mount the filesystem") parser.add_argument( "-f", "--foreground", dest="foreground", action="store_true", help="stay in foreground, do not daemonize" diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 481b2014e2..4f8481bddc 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -5,6 +5,8 @@ from operator import attrgetter import os +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error @@ -273,16 +275,14 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): the ``borg repo-list`` description for more details about the format string). """ ) - subparser = subparsers.add_parser( - "prune", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_prune.__doc__, epilog=prune_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="prune archives", ) - subparser.set_defaults(func=self.do_prune) + subparsers.add_subcommand("prune", subparser, help="prune archives") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" ) diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 149f665a94..ff074261e5 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -1,5 +1,7 @@ import argparse +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from ._common import build_matcher from ..archive import ArchiveRecreater @@ -102,16 +104,14 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser): if the chunks are still missing. """ ) - subparser = subparsers.add_parser( - "recreate", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_recreate.__doc__, epilog=recreate_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help=self.do_recreate.__doc__, ) - subparser.set_defaults(func=self.do_recreate) + subparsers.add_subcommand("recreate", subparser, help=self.do_recreate.__doc__) subparser.add_argument( "--list", dest="output_list", action="store_true", help="output verbose list of items (files, dirs, ...)" ) diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index bdb338843f..316f9734cd 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -1,5 +1,7 @@ import argparse +from jsonargparse import ArgumentParser + from ._common import with_repository, with_archive from ..constants import * # NOQA from ..helpers import archivename_validator @@ -28,16 +30,14 @@ def build_parser_rename(self, subparsers, common_parser, mid_common_parser): This results in a different archive ID. """ ) - subparser = subparsers.add_parser( - "rename", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_rename.__doc__, epilog=rename_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="rename an archive", ) - subparser.set_defaults(func=self.do_rename) + subparsers.add_subcommand("rename", subparser, help="rename an archive") subparser.add_argument( "name", metavar="OLDNAME", type=archivename_validator, help="specify the current archive name" ) diff --git a/src/borg/archiver/repo_compress_cmd.py b/src/borg/archiver/repo_compress_cmd.py index a5aeb9a545..7bf38e31c8 100644 --- a/src/borg/archiver/repo_compress_cmd.py +++ b/src/borg/archiver/repo_compress_cmd.py @@ -1,6 +1,8 @@ import argparse from collections import defaultdict +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from ..constants import * # NOQA from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE @@ -180,16 +182,14 @@ def build_parser_repo_compress(self, subparsers, common_parser, mid_common_parse You do **not** need to run ``borg compact`` after ``borg repo-compress``. """ ) - subparser = subparsers.add_parser( - "repo-compress", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_repo_compress.__doc__, epilog=repo_compress_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help=self.do_repo_compress.__doc__, ) - subparser.set_defaults(func=self.do_repo_compress) + subparsers.add_subcommand("repo-compress", subparser, help=self.do_repo_compress.__doc__) subparser.add_argument( "-C", diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index 07b60fa8d5..7856e46fca 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -1,5 +1,7 @@ import argparse +from jsonargparse import ArgumentParser + from ._common import with_repository, with_other_repository, Highlander from ..cache import Cache from ..constants import * # NOQA @@ -190,16 +192,14 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) Then use ``borg transfer --other-repo ORIG_REPO --from-borg1 ...`` to transfer the archives. """ ) - subparser = subparsers.add_parser( - "repo-create", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_repo_create.__doc__, epilog=repo_create_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="create a new, empty repository", ) - subparser.set_defaults(func=self.do_repo_create) + subparsers.add_subcommand("repo-create", subparser, help="create a new, empty repository") subparser.add_argument( "--other-repo", metavar="SRC_REPOSITORY", diff --git a/src/borg/archiver/repo_delete_cmd.py b/src/borg/archiver/repo_delete_cmd.py index aa2b531eaa..51319c6a31 100644 --- a/src/borg/archiver/repo_delete_cmd.py +++ b/src/borg/archiver/repo_delete_cmd.py @@ -1,5 +1,7 @@ import argparse +from jsonargparse import ArgumentParser + from ._common import with_repository from ..cache import Cache, SecurityManager from ..constants import * # NOQA @@ -102,16 +104,14 @@ def build_parser_repo_delete(self, subparsers, common_parser, mid_common_parser) Always first use ``--dry-run --list`` to see what would be deleted. """ ) - subparser = subparsers.add_parser( - "repo-delete", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_repo_delete.__doc__, epilog=repo_delete_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="delete a repository", ) - subparser.set_defaults(func=self.do_repo_delete) + subparsers.add_subcommand("repo-delete", subparser, help="delete a repository") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" ) diff --git a/src/borg/archiver/repo_info_cmd.py b/src/borg/archiver/repo_info_cmd.py index 0b11ed6e8a..43c37d5036 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -1,6 +1,8 @@ import argparse import textwrap +from jsonargparse import ArgumentParser + from ._common import with_repository from ..constants import * # NOQA from ..helpers import bin_to_hex, json_print, basic_json_data @@ -63,14 +65,12 @@ def build_parser_repo_info(self, subparsers, common_parser, mid_common_parser): This command displays detailed information about the repository. """ ) - subparser = subparsers.add_parser( - "repo-info", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_repo_info.__doc__, epilog=repo_info_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="show repository information", ) - subparser.set_defaults(func=self.do_repo_info) + subparsers.add_subcommand("repo-info", subparser, help="show repository information") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/repo_list_cmd.py b/src/borg/archiver/repo_list_cmd.py index 6f5c5ae47f..a91c4a53be 100644 --- a/src/borg/archiver/repo_list_cmd.py +++ b/src/borg/archiver/repo_list_cmd.py @@ -3,6 +3,8 @@ import textwrap import sys +from jsonargparse import ArgumentParser + from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data @@ -85,16 +87,14 @@ def build_parser_repo_list(self, subparsers, common_parser, mid_common_parser): ) + ArchiveFormatter.keys_help() ) - subparser = subparsers.add_parser( - "repo-list", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_repo_list.__doc__, epilog=repo_list_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="list repository contents", ) - subparser.set_defaults(func=self.do_repo_list) + subparsers.add_subcommand("repo-list", subparser, help="list repository contents") subparser.add_argument( "--short", dest="short", action="store_true", help="only print the archive IDs, nothing else" ) diff --git a/src/borg/archiver/repo_space_cmd.py b/src/borg/archiver/repo_space_cmd.py index 45c1646a28..72a03007d7 100644 --- a/src/borg/archiver/repo_space_cmd.py +++ b/src/borg/archiver/repo_space_cmd.py @@ -2,6 +2,8 @@ import math import os +from jsonargparse import ArgumentParser + from borgstore.store import ItemInfo from ._common import with_repository, Highlander @@ -86,16 +88,14 @@ def build_parser_repo_space(self, subparsers, common_parser, mid_common_parser): Reserved space is always rounded up to full reservation blocks of 64 MiB. """ ) - subparser = subparsers.add_parser( - "repo-space", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_repo_space.__doc__, epilog=repo_space_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="manage reserved space in a repository", ) - subparser.set_defaults(func=self.do_repo_space) + subparsers.add_subcommand("repo-space", subparser, help="manage reserved space in a repository") subparser.add_argument( "--reserve", metavar="SPACE", diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index f8c1700676..6774d65a26 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -19,6 +19,7 @@ def do_serve(self, args): ).serve() def build_parser_serve(self, subparsers, common_parser, mid_common_parser): + from jsonargparse import ArgumentParser from ._common import process_epilog serve_epilog = process_epilog( @@ -52,16 +53,14 @@ def build_parser_serve(self, subparsers, common_parser, mid_common_parser): Existing archives can be read, but no archives can be created or deleted. """ ) - subparser = subparsers.add_parser( - "serve", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_serve.__doc__, epilog=serve_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="start the repository server process", ) - subparser.set_defaults(func=self.do_serve) + subparsers.add_subcommand("serve", subparser, help="start the repository server process") subparser.add_argument( "--restrict-to-path", metavar="PATH", diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py index 3dffbd8030..1b24ca08a4 100644 --- a/src/borg/archiver/tag_cmd.py +++ b/src/borg/archiver/tag_cmd.py @@ -1,5 +1,7 @@ import argparse +from jsonargparse import ArgumentParser + from ._common import with_repository, define_archive_filters_group from ..archive import Archive from ..constants import * # NOQA @@ -80,39 +82,37 @@ def build_parser_tag(self, subparsers, common_parser, mid_common_parser): removed). """ ) - subparser = subparsers.add_parser( - "tag", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_tag.__doc__, epilog=tag_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="tag archives", ) - subparser.set_defaults(func=self.do_tag) + subparsers.add_subcommand("tag", subparser, help="tag archives") subparser.add_argument( "--set", dest="set_tags", metavar="TAG", type=tag_validator, - action="append", - help="set tags (can be given multiple times)", + nargs="+", + help="set tags", ) subparser.add_argument( "--add", dest="add_tags", metavar="TAG", type=tag_validator, - action="append", - help="add tags (can be given multiple times)", + nargs="+", + help="add tags", ) subparser.add_argument( "--remove", dest="remove_tags", metavar="TAG", type=tag_validator, - action="append", - help="remove tags (can be given multiple times)", + nargs="+", + help="remove tags", ) define_archive_filters_group(subparser) subparser.add_argument( diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index fe0d7bd1d3..8830ebcdc9 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -5,6 +5,8 @@ import stat import tarfile +from jsonargparse import ArgumentParser + from ..archive import Archive, TarfileObjectProcessors, ChunksProcessor from ..compress import CompressionSpec from ..constants import * # NOQA @@ -384,16 +386,14 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): pass over the archive metadata. """ ) - subparser = subparsers.add_parser( - "export-tar", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_export_tar.__doc__, epilog=export_tar_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="create tarball from archive", ) - subparser.set_defaults(func=self.do_export_tar) + subparsers.add_subcommand("export-tar", subparser, help="create tarball from archive") subparser.add_argument( "--tar-filter", dest="tar_filter", @@ -460,16 +460,14 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): ``--ignore-zeros`` option to skip through the stop markers between them. """ ) - subparser = subparsers.add_parser( - "import-tar", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_import_tar.__doc__, epilog=import_tar_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help=self.do_import_tar.__doc__, ) - subparser.set_defaults(func=self.do_import_tar) + subparsers.add_subcommand("import-tar", subparser, help=self.do_import_tar.__doc__) subparser.add_argument( "--tar-filter", dest="tar_filter", diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index 99813039dd..a034b06353 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -1,5 +1,7 @@ import argparse +from jsonargparse import ArgumentParser + from ._common import with_repository, with_other_repository, Highlander from ..archive import Archive, cached_hash, DownloadPipeline from ..chunkers import get_chunker @@ -333,16 +335,14 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): """ ) - subparser = subparsers.add_parser( - "transfer", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_transfer.__doc__, epilog=transfer_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="transfer of archives from another repository", ) - subparser.set_defaults(func=self.do_transfer) + subparsers.add_subcommand("transfer", subparser, help="transfer of archives from another repository") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change repository, just check" ) diff --git a/src/borg/archiver/undelete_cmd.py b/src/borg/archiver/undelete_cmd.py index a0455518f2..1c2bdf6827 100644 --- a/src/borg/archiver/undelete_cmd.py +++ b/src/borg/archiver/undelete_cmd.py @@ -1,6 +1,8 @@ import argparse import logging +from jsonargparse import ArgumentParser + from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator @@ -72,16 +74,14 @@ def build_parser_undelete(self, subparsers, common_parser, mid_common_parser): patterns, see :ref:`borg_patterns`). """ ) - subparser = subparsers.add_parser( - "undelete", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_undelete.__doc__, epilog=undelete_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="undelete archives", ) - subparser.set_defaults(func=self.do_undelete) + subparsers.add_subcommand("undelete", subparser, help="undelete archives") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" ) diff --git a/src/borg/archiver/version_cmd.py b/src/borg/archiver/version_cmd.py index 36c52a9975..bfa825ba0b 100644 --- a/src/borg/archiver/version_cmd.py +++ b/src/borg/archiver/version_cmd.py @@ -23,6 +23,7 @@ def do_version(self, args): print(f"{format_version(client_version)} / {format_version(server_version)}") def build_parser_version(self, subparsers, common_parser, mid_common_parser): + from jsonargparse import ArgumentParser from ._common import process_epilog version_epilog = process_epilog( @@ -51,13 +52,11 @@ def build_parser_version(self, subparsers, common_parser, mid_common_parser): You can also use ``borg --version`` to display a potentially more precise client version. """ ) - subparser = subparsers.add_parser( - "version", + subparser = ArgumentParser( parents=[common_parser], add_help=False, description=self.do_version.__doc__, epilog=version_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, - help="display the Borg client and server versions", ) - subparser.set_defaults(func=self.do_version) + subparsers.add_subcommand("version", subparser, help="display the Borg client and server versions") diff --git a/src/borg/helpers/jap_helpers.py b/src/borg/helpers/jap_helpers.py new file mode 100644 index 0000000000..be6962ea25 --- /dev/null +++ b/src/borg/helpers/jap_helpers.py @@ -0,0 +1,33 @@ +import argparse +from typing import Any + + +def flatten_namespace(ns: Any) -> argparse.Namespace: + """ + Recursively flattens a nested namespace into a single-level namespace. + JSONArgparse uses nested namespaces for subcommands, whereas borg's + internal dispatch and logic expect a flat namespace. + """ + flat = argparse.Namespace() + + # Extract the nested path of subcommands + subcmds = [] + current = ns + while current and hasattr(current, "subcommand") and current.subcommand: + subcmds.append(current.subcommand) + current = getattr(current, current.subcommand, None) + + if subcmds: + flat.subcommand = " ".join(subcmds) + + def _flatten(source, target): + items = vars(source).items() if hasattr(source, '__dict__') else source.items() if hasattr(source, 'items') else [] + for k, v in items: + if isinstance(v, argparse.Namespace) or type(v).__name__ == 'Namespace': + _flatten(v, target) + else: + if k != "subcommand" and not hasattr(target, k): + setattr(target, k, v) + + _flatten(ns, flat) + return flat diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 2505954df6..c4e7133d4b 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -121,7 +121,7 @@ def decode_dict(d, keys, encoding="utf-8", errors="surrogateescape"): def positive_int_validator(value): - """argparse type for positive integers.""" + """argparse type for positive integers, N > 0.""" int_value = int(value) if int_value <= 0: raise argparse.ArgumentTypeError("A positive integer is required: %s" % value) @@ -352,7 +352,7 @@ def SortBySpec(text): from ..manifest import AI_HUMAN_SORT_KEYS for token in text.split(","): - if token not in AI_HUMAN_SORT_KEYS: + if token not in AI_HUMAN_SORT_KEYS and token != "ts": # idempotency: do not reject ts raise argparse.ArgumentTypeError("Invalid sort key: %s" % token) return text.replace("timestamp", "ts").replace("archive", "name") diff --git a/src/borg/testsuite/archiver/tag_cmd_test.py b/src/borg/testsuite/archiver/tag_cmd_test.py index 06be79730e..2ada635c28 100644 --- a/src/borg/testsuite/archiver/tag_cmd_test.py +++ b/src/borg/testsuite/archiver/tag_cmd_test.py @@ -15,7 +15,7 @@ def test_tag_set(archivers, request): assert "tags: aa." in output output = cmd(archiver, "tag", "-a", "archive", "--set", "bb") assert "tags: bb." in output - output = cmd(archiver, "tag", "-a", "archive", "--set", "bb", "--set", "aa") + output = cmd(archiver, "tag", "-a", "archive", "--set", "bb", "aa") assert "tags: aa,bb." in output # sorted! output = cmd(archiver, "tag", "-a", "archive", "--set", "") assert "tags: ." in output # no tags! @@ -46,7 +46,7 @@ def test_tag_set_noclobber_special(archivers, request): output = cmd(archiver, "tag", "-a", "archive", "--set", "clobber") assert "tags: @PROT." in output # it is possible though to use --set if the existing special tags are also given: - output = cmd(archiver, "tag", "-a", "archive", "--set", "noclobber", "--set", "@PROT") + output = cmd(archiver, "tag", "-a", "archive", "--set", "noclobber", "@PROT") assert "tags: @PROT,noclobber." in output From 9a469e32cf40399a8b9e26d4aea90b3e1df2f171 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 25 Feb 2026 15:43:39 +0100 Subject: [PATCH 05/27] fix make build_man / build_usage --- scripts/make.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/make.py b/scripts/make.py index f7bae4d41d..a8877f1c4a 100644 --- a/scripts/make.py +++ b/scripts/make.py @@ -46,7 +46,7 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None): is_subcommand = False choices = {} for action in parser._actions: - if action.choices is not None and "SubParsersAction" in str(action.__class__): + if action.choices is not None and "SubCommands" in str(action.__class__): is_subcommand = True for cmd, parser in action.choices.items(): choices[prefix + cmd] = parser @@ -323,7 +323,7 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None): is_subcommand = False choices = {} for action in parser._actions: - if action.choices is not None and "SubParsersAction" in str(action.__class__): + if action.choices is not None and "SubCommands" in str(action.__class__): is_subcommand = True for cmd, parser in action.choices.items(): choices[prefix + cmd] = parser @@ -349,7 +349,7 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None): self.write_heading(write, "SYNOPSIS") if is_intermediary: - subparsers = [action for action in parser._actions if "SubParsersAction" in str(action.__class__)][0] + subparsers = [action for action in parser._actions if "SubCommands" in str(action.__class__)][0] for subcommand in subparsers.choices: write("| borg", "[common options]", command, subcommand, "...") self.see_also.setdefault(command, []).append(f"{command}-{subcommand}") From 27561bd9cc4c9e6e640998e279dfa26d12e31a92 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 25 Feb 2026 12:10:40 +0100 Subject: [PATCH 06/27] SortBySpec: avoid triggering bandit security checker Guess it was triggered due to naming the variable "token", maybe "sort_key" is less problematic. --- src/borg/helpers/parseformat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index c4e7133d4b..87b4b731e8 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -351,9 +351,9 @@ def FilesystemPathSpec(text): def SortBySpec(text): from ..manifest import AI_HUMAN_SORT_KEYS - for token in text.split(","): - if token not in AI_HUMAN_SORT_KEYS and token != "ts": # idempotency: do not reject ts - raise argparse.ArgumentTypeError("Invalid sort key: %s" % token) + for sort_key in text.split(","): + if sort_key not in AI_HUMAN_SORT_KEYS and sort_key != "ts": # idempotency: do not reject ts + raise argparse.ArgumentTypeError("Invalid sort key: %s" % sort_key) return text.replace("timestamp", "ts").replace("archive", "name") From e39d06ee988d2fd4b6e3b94371f191ff8c95d6b8 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 25 Feb 2026 11:48:08 +0100 Subject: [PATCH 07/27] blacken src --- src/borg/archiver/__init__.py | 3 --- src/borg/archiver/_common.py | 16 ++-------------- src/borg/archiver/benchmark_cmd.py | 4 +++- src/borg/archiver/debug_cmd.py | 4 +++- src/borg/archiver/help_cmd.py | 6 +----- src/borg/archiver/tag_cmd.py | 25 +++---------------------- src/borg/helpers/jap_helpers.py | 14 ++++++++------ 7 files changed, 20 insertions(+), 52 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 7ee8792b0f..34312cc2db 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -66,9 +66,6 @@ PURE_PYTHON_MSGPACK_WARNING = "Using a pure-python msgpack! This will result in lower performance." - - - from .analyze_cmd import AnalyzeMixIn from .benchmark_cmd import BenchmarkMixIn from .check_cmd import CheckMixIn diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 9ae39a769d..26ccee9bc5 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -269,20 +269,8 @@ def process_epilog(epilog): def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): - add_option( - "--pattern-roots-internal", - dest="pattern_roots", - action="append", - default=[], - help=argparse.SUPPRESS, - ) - add_option( - "--patterns-internal", - dest="patterns", - action="append", - default=[], - help=argparse.SUPPRESS, - ) + add_option("--pattern-roots-internal", dest="pattern_roots", action="append", default=[], help=argparse.SUPPRESS) + add_option("--patterns-internal", dest="patterns", action="append", default=[], help=argparse.SUPPRESS) add_option( "-e", "--exclude", diff --git a/src/borg/archiver/benchmark_cmd.py b/src/borg/archiver/benchmark_cmd.py index ce317e4289..8a94cfd273 100644 --- a/src/borg/archiver/benchmark_cmd.py +++ b/src/borg/archiver/benchmark_cmd.py @@ -415,7 +415,9 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): epilog=bench_crud_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, ) - benchmark_parsers.add_subcommand("crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete).") + benchmark_parsers.add_subcommand( + "crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete)." + ) subparser.add_argument("path", metavar="PATH", help="path where to create benchmark input data") subparser.add_argument("--json-lines", action="store_true", help="Format output as JSON Lines.") diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 2f311320f3..fa35ac295a 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -568,6 +568,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): epilog=debug_convert_profile_epilog, formatter_class=argparse.RawDescriptionHelpFormatter, ) - debug_parsers.add_subcommand("convert-profile", subparser, help="convert Borg profile to Python profile (debug)") + debug_parsers.add_subcommand( + "convert-profile", subparser, help="convert Borg profile to Python profile (debug)" + ) subparser.add_argument("input", metavar="INPUT", type=str, help="Borg profile") subparser.add_argument("output", metavar="OUTPUT", type=str, help="Output file") diff --git a/src/borg/archiver/help_cmd.py b/src/borg/archiver/help_cmd.py index 3b1486cd0b..41b4124818 100644 --- a/src/borg/archiver/help_cmd.py +++ b/src/borg/archiver/help_cmd.py @@ -555,11 +555,7 @@ def do_subcommand_help(self, parser, args): do_maincommand_help = do_subcommand_help def build_parser_help(self, subparsers, common_parser, mid_common_parser, parser): - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description="Extra help", - ) + subparser = ArgumentParser(parents=[common_parser], add_help=False, description="Extra help") subparsers.add_subcommand("help", subparser, help="Extra help") subparser.add_argument("--epilog-only", dest="epilog_only", action="store_true") subparser.add_argument("--usage-only", dest="usage_only", action="store_true") diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py index 1b24ca08a4..23f9c9d12a 100644 --- a/src/borg/archiver/tag_cmd.py +++ b/src/borg/archiver/tag_cmd.py @@ -90,29 +90,10 @@ def build_parser_tag(self, subparsers, common_parser, mid_common_parser): formatter_class=argparse.RawDescriptionHelpFormatter, ) subparsers.add_subcommand("tag", subparser, help="tag archives") + subparser.add_argument("--set", dest="set_tags", metavar="TAG", type=tag_validator, nargs="+", help="set tags") + subparser.add_argument("--add", dest="add_tags", metavar="TAG", type=tag_validator, nargs="+", help="add tags") subparser.add_argument( - "--set", - dest="set_tags", - metavar="TAG", - type=tag_validator, - nargs="+", - help="set tags", - ) - subparser.add_argument( - "--add", - dest="add_tags", - metavar="TAG", - type=tag_validator, - nargs="+", - help="add tags", - ) - subparser.add_argument( - "--remove", - dest="remove_tags", - metavar="TAG", - type=tag_validator, - nargs="+", - help="remove tags", + "--remove", dest="remove_tags", metavar="TAG", type=tag_validator, nargs="+", help="remove tags" ) define_archive_filters_group(subparser) subparser.add_argument( diff --git a/src/borg/helpers/jap_helpers.py b/src/borg/helpers/jap_helpers.py index be6962ea25..95c4f04563 100644 --- a/src/borg/helpers/jap_helpers.py +++ b/src/borg/helpers/jap_helpers.py @@ -9,25 +9,27 @@ def flatten_namespace(ns: Any) -> argparse.Namespace: internal dispatch and logic expect a flat namespace. """ flat = argparse.Namespace() - + # Extract the nested path of subcommands subcmds = [] current = ns while current and hasattr(current, "subcommand") and current.subcommand: subcmds.append(current.subcommand) current = getattr(current, current.subcommand, None) - + if subcmds: flat.subcommand = " ".join(subcmds) - + def _flatten(source, target): - items = vars(source).items() if hasattr(source, '__dict__') else source.items() if hasattr(source, 'items') else [] + items = ( + vars(source).items() if hasattr(source, "__dict__") else source.items() if hasattr(source, "items") else [] + ) for k, v in items: - if isinstance(v, argparse.Namespace) or type(v).__name__ == 'Namespace': + if isinstance(v, argparse.Namespace) or type(v).__name__ == "Namespace": _flatten(v, target) else: if k != "subcommand" and not hasattr(target, k): setattr(target, k, v) - + _flatten(ns, flat) return flat From 393b2cbc117208a8bab6196f4f87f3faf9645a71 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 25 Feb 2026 16:53:46 +0100 Subject: [PATCH 08/27] import from jsonargparse: SUPPRESS, REMAINDER, NameSpace --- src/borg/archiver/__init__.py | 6 +++--- src/borg/archiver/_common.py | 7 ++++--- src/borg/archiver/lock_cmds.py | 4 ++-- src/borg/helpers/jap_helpers.py | 8 ++++---- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 34312cc2db..4df887479b 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -27,7 +27,7 @@ import signal from datetime import datetime, timezone - from jsonargparse import ArgumentParser + from jsonargparse import ArgumentParser, Namespace, SUPPRESS from ..logger import create_logger, setup_logging @@ -268,14 +268,14 @@ def add_argument(*args, **kwargs): # Note: We control all inputs. kwargs["help"] = kwargs["help"] % kwargs if not is_append: - kwargs["default"] = argparse.SUPPRESS + kwargs["default"] = SUPPRESS common_group.add_argument(*args, **kwargs) common_group = parser.add_argument_group("Common options") self.define_common_options(add_argument) - def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise is not like a dict. + def resolve(self, args: Namespace): # Namespace has "in" but otherwise is not like a dict. """ Resolve the multiple definitions of each common option to the final value. """ diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 26ccee9bc5..302eefedcf 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -1,8 +1,9 @@ -import argparse import functools import os import textwrap +from jsonargparse import SUPPRESS + import borg from ..archive import Archive from ..constants import * # NOQA @@ -269,8 +270,8 @@ def process_epilog(epilog): def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components=False): - add_option("--pattern-roots-internal", dest="pattern_roots", action="append", default=[], help=argparse.SUPPRESS) - add_option("--patterns-internal", dest="patterns", action="append", default=[], help=argparse.SUPPRESS) + add_option("--pattern-roots-internal", dest="pattern_roots", action="append", default=[], help=SUPPRESS) + add_option("--patterns-internal", dest="patterns", action="append", default=[], help=SUPPRESS) add_option( "-e", "--exclude", diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index 0e5e251f27..03ab393a34 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -1,7 +1,7 @@ import argparse import subprocess -from jsonargparse import ArgumentParser +from jsonargparse import ArgumentParser, REMAINDER from ._common import with_repository from ..cache import Cache @@ -86,4 +86,4 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): ) subparsers.add_subcommand("with-lock", subparser, help="run a user command with the lock held") subparser.add_argument("command", metavar="COMMAND", help="command to run") - subparser.add_argument("args", metavar="ARGS", nargs=argparse.REMAINDER, help="command arguments") + subparser.add_argument("args", metavar="ARGS", nargs=REMAINDER, help="command arguments") diff --git a/src/borg/helpers/jap_helpers.py b/src/borg/helpers/jap_helpers.py index 95c4f04563..80691b3797 100644 --- a/src/borg/helpers/jap_helpers.py +++ b/src/borg/helpers/jap_helpers.py @@ -1,14 +1,14 @@ -import argparse +from jsonargparse import Namespace from typing import Any -def flatten_namespace(ns: Any) -> argparse.Namespace: +def flatten_namespace(ns: Any) -> Namespace: """ Recursively flattens a nested namespace into a single-level namespace. JSONArgparse uses nested namespaces for subcommands, whereas borg's internal dispatch and logic expect a flat namespace. """ - flat = argparse.Namespace() + flat = Namespace() # Extract the nested path of subcommands subcmds = [] @@ -25,7 +25,7 @@ def _flatten(source, target): vars(source).items() if hasattr(source, "__dict__") else source.items() if hasattr(source, "items") else [] ) for k, v in items: - if isinstance(v, argparse.Namespace) or type(v).__name__ == "Namespace": + if isinstance(v, Namespace) or type(v).__name__ == "Namespace": _flatten(v, target) else: if k != "subcommand" and not hasattr(target, k): From 223a834a3d2121a46d206bbd71d38374a700ac20 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 25 Feb 2026 18:28:30 +0100 Subject: [PATCH 09/27] adapt borg completion for jsonargparse --- src/borg/archiver/completion_cmd.py | 36 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 9120dd965d..90d1443004 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -55,6 +55,7 @@ import shtab from jsonargparse import ArgumentParser +from jsonargparse._actions import _ActionSubCommands from ._common import process_epilog from ..constants import * # NOQA @@ -630,12 +631,11 @@ """ -def _attach_completion(parser: argparse.ArgumentParser, type_class, completion_dict: dict): +def _attach_completion(parser: ArgumentParser, type_class, completion_dict: dict): """Tag all arguments with type `type_class` with completion choices from `completion_dict`.""" for action in parser._actions: - # Recurse into subparsers - if isinstance(action, argparse._SubParsersAction): + if isinstance(action, _ActionSubCommands): for sub in action.choices.values(): _attach_completion(sub, type_class, completion_dict) continue @@ -644,10 +644,10 @@ def _attach_completion(parser: argparse.ArgumentParser, type_class, completion_d action.complete = completion_dict # type: ignore[attr-defined] -def _attach_help_completion(parser: argparse.ArgumentParser, completion_dict: dict): +def _attach_help_completion(parser: ArgumentParser, completion_dict: dict): """Tag the 'topic' argument of the 'help' command with static completion choices.""" for action in parser._actions: - if isinstance(action, argparse._SubParsersAction): + if isinstance(action, _ActionSubCommands): for sub in action.choices.values(): _attach_help_completion(sub, completion_dict) continue @@ -694,7 +694,7 @@ def do_completion(self, args): # Collect all commands and help topics for "borg help" completion help_choices = list(self.helptext.keys()) for action in parser._actions: - if isinstance(action, argparse._SubParsersAction): + if isinstance(action, _ActionSubCommands): help_choices.extend(action.choices.keys()) help_completion_fn = "_borg_help_topics" @@ -734,8 +734,28 @@ def do_completion(self, args): } bash_preamble = partial_format(BASH_PREAMBLE_TMPL, mapping) zsh_preamble = partial_format(ZSH_PREAMBLE_TMPL, mapping) - preamble = {"bash": bash_preamble, "zsh": zsh_preamble} - script = shtab.complete(parser, shell=args.shell, preamble=preamble) # nosec B604 + + from jsonargparse._completions import ( + prepare_actions_context, + shtab_prepare_actions, + norm_name, + bash_compgen_typehint, + ) + + prog = norm_name(parser.prog) + if not prog: + prog = "borg" + preambles = [] + if args.shell == "bash": + preambles.append(bash_compgen_typehint.strip().replace("%s", prog)) + preambles.append(bash_preamble) + elif args.shell == "zsh": + preambles.append(zsh_preamble) + + with prepare_actions_context(args.shell, prog, preambles): + shtab_prepare_actions(parser) + + script = shtab.complete(parser, shell=args.shell, preamble="\n".join(preambles)) # nosec B604 print(script) def build_parser_completion(self, subparsers, common_parser, mid_common_parser): From a7083976129f99bece9099689f56309b19b0a4f0 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Wed, 25 Feb 2026 22:30:54 +0100 Subject: [PATCH 10/27] fix argparsing test --- .../testsuite/archiver/argparsing_test.py | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/borg/testsuite/archiver/argparsing_test.py b/src/borg/testsuite/archiver/argparsing_test.py index 974becf332..58d09076d6 100644 --- a/src/borg/testsuite/archiver/argparsing_test.py +++ b/src/borg/testsuite/archiver/argparsing_test.py @@ -1,6 +1,8 @@ import argparse import pytest +from jsonargparse import ArgumentParser + from . import Archiver, RK_ENCRYPTION, cmd @@ -93,15 +95,15 @@ def define_common_options(add_common_option): @pytest.fixture def basic_parser(self): - parser = argparse.ArgumentParser(prog="test", description="test parser", add_help=False) + parser = ArgumentParser(prog="test", description="test parser", add_help=False) parser.common_options = Archiver.CommonOptions( self.define_common_options, suffix_precedence=("_level0", "_level1") ) return parser @pytest.fixture - def subparsers(self, basic_parser): - return basic_parser.add_subparsers(title="required arguments", metavar="") + def subcommands(self, basic_parser): + return basic_parser.add_subcommands(required=False, title="required arguments", metavar="") @pytest.fixture def parser(self, basic_parser): @@ -110,27 +112,28 @@ def parser(self, basic_parser): @pytest.fixture def common_parser(self, parser): - common_parser = argparse.ArgumentParser(add_help=False, prog="test") + common_parser = ArgumentParser(add_help=False, prog="test") parser.common_options.add_common_group(common_parser, "_level1") return common_parser @pytest.fixture - def parse_vars_from_line(self, parser, subparsers, common_parser): - subparser = subparsers.add_parser( - "subcommand", + def parse_vars_from_line(self, parser, subcommands, common_parser): + from ...helpers.jap_helpers import flatten_namespace + + subparser = ArgumentParser( parents=[common_parser], add_help=False, description="foo", epilog="bar", - help="baz", formatter_class=argparse.RawDescriptionHelpFormatter, ) - subparser.set_defaults(func=1234) subparser.add_argument("--foo-bar", dest="foo_bar", action="store_true") + subcommands.add_subcommand("subcmd", subparser, help="baz") def parse_vars_from_line(*line): print(line) args = parser.parse_args(line) + args = flatten_namespace(args) parser.common_options.resolve(args) return vars(args) @@ -144,25 +147,25 @@ def test_simple(self, parse_vars_from_line): "progress": False, } - assert parse_vars_from_line("--error", "subcommand", "--critical") == { + assert parse_vars_from_line("--error", "subcmd", "--critical") == { "append": [], "lock_wait": 1, "log_level": "critical", "progress": False, "foo_bar": False, - "func": 1234, + "subcommand": "subcmd", } with pytest.raises(SystemExit): - parse_vars_from_line("--foo-bar", "subcommand") + parse_vars_from_line("--foo-bar", "subcmd") - assert parse_vars_from_line("--append=foo", "--append", "bar", "subcommand", "--append", "baz") == { + assert parse_vars_from_line("--append=foo", "--append", "bar", "subcmd", "--append", "baz") == { "append": ["foo", "bar", "baz"], "lock_wait": 1, "log_level": "warning", "progress": False, "foo_bar": False, - "func": 1234, + "subcommand": "subcmd", } @pytest.mark.parametrize("position", ("before", "after", "both")) @@ -171,7 +174,7 @@ def test_flag_position_independence(self, parse_vars_from_line, position, flag, line = [] if position in ("before", "both"): line.append(flag) - line.append("subcommand") + line.append("subcmd") if position in ("after", "both"): line.append(flag) @@ -181,7 +184,7 @@ def test_flag_position_independence(self, parse_vars_from_line, position, flag, "log_level": "warning", "progress": False, "foo_bar": False, - "func": 1234, + "subcommand": "subcmd", args_key: args_value, } From be2b3363e2d98f7755fdbe872d9fd74f1f8743d5 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 26 Feb 2026 00:30:58 +0100 Subject: [PATCH 11/27] reorg imports --- src/borg/archiver/__init__.py | 8 +- src/borg/archiver/_common.py | 3 +- src/borg/archiver/analyze_cmd.py | 6 +- src/borg/archiver/benchmark_cmd.py | 10 +-- src/borg/archiver/check_cmd.py | 7 +- src/borg/archiver/compact_cmd.py | 6 +- src/borg/archiver/completion_cmd.py | 20 ++--- src/borg/archiver/create_cmd.py | 7 +- src/borg/archiver/debug_cmd.py | 32 ++++---- src/borg/archiver/delete_cmd.py | 6 +- src/borg/archiver/diff_cmd.py | 13 ++- src/borg/archiver/extract_cmd.py | 6 +- src/borg/archiver/help_cmd.py | 4 +- src/borg/archiver/info_cmd.py | 6 +- src/borg/archiver/key_cmds.py | 14 ++-- src/borg/archiver/list_cmd.py | 7 +- src/borg/archiver/lock_cmds.py | 8 +- src/borg/archiver/mount_cmds.py | 10 +-- src/borg/archiver/prune_cmd.py | 6 +- src/borg/archiver/recreate_cmd.py | 7 +- src/borg/archiver/rename_cmd.py | 7 +- src/borg/archiver/repo_compress_cmd.py | 6 +- src/borg/archiver/repo_create_cmd.py | 7 +- src/borg/archiver/repo_delete_cmd.py | 7 +- src/borg/archiver/repo_info_cmd.py | 6 +- src/borg/archiver/repo_list_cmd.py | 7 +- src/borg/archiver/repo_space_cmd.py | 7 +- src/borg/archiver/serve_cmd.py | 6 +- src/borg/archiver/tag_cmd.py | 7 +- src/borg/archiver/tar_cmds.py | 8 +- src/borg/archiver/transfer_cmd.py | 13 +-- src/borg/archiver/undelete_cmd.py | 6 +- src/borg/archiver/version_cmd.py | 6 +- src/borg/compress.pyx | 14 +--- src/borg/helpers/jap_helpers.py | 6 +- src/borg/helpers/parseformat.py | 82 ++++++++----------- src/borg/patterns.py | 14 ++-- .../testsuite/archiver/argparsing_test.py | 8 +- src/borg/testsuite/compress_test.py | 4 +- .../testsuite/helpers/parseformat_test.py | 3 +- src/borg/testsuite/patterns_test.py | 4 +- 41 files changed, 151 insertions(+), 263 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 4df887479b..6329221b60 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -15,7 +15,6 @@ sys.exit(2) # == EXIT_ERROR try: - import argparse import faulthandler import functools import inspect @@ -27,8 +26,6 @@ import signal from datetime import datetime, timezone - from jsonargparse import ArgumentParser, Namespace, SUPPRESS - from ..logger import create_logger, setup_logging logger = create_logger() @@ -42,8 +39,7 @@ from ..helpers import format_file_size from ..helpers import remove_surrogates, text_to_json from ..helpers import DatetimeWrapper, replace_placeholders - from ..helpers.jap_helpers import flatten_namespace - + from ..helpers.jap_helpers import flatten_namespace, ArgumentTypeError, ArgumentParser, Namespace, SUPPRESS from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from ..helpers import ErrorIgnoringTextIOWrapper @@ -643,7 +639,7 @@ def main(): # pragma: no cover tb = format_tb(e) print(tb, file=sys.stderr) sys.exit(e.exit_code) - except argparse.ArgumentTypeError as e: + except ArgumentTypeError as e: # we might not have logging setup yet, so get out quickly print(str(e), file=sys.stderr) sys.exit(CommandError.exit_mcode if modern_ec else EXIT_ERROR) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index 302eefedcf..ad2817d6db 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -2,8 +2,6 @@ import os import textwrap -from jsonargparse import SUPPRESS - import borg from ..archive import Archive from ..constants import * # NOQA @@ -11,6 +9,7 @@ from ..helpers import Error from ..helpers import SortBySpec, positive_int_validator, location_validator, Location, relative_time_marker_validator from ..helpers import Highlander +from ..helpers.jap_helpers import SUPPRESS from ..helpers.nanorst import rst_to_terminal from ..manifest import Manifest, AI_HUMAN_SORT_KEYS from ..patterns import PatternMatcher diff --git a/src/borg/archiver/analyze_cmd.py b/src/borg/archiver/analyze_cmd.py index 1d99075ad6..6ea240a5a7 100644 --- a/src/borg/archiver/analyze_cmd.py +++ b/src/borg/archiver/analyze_cmd.py @@ -1,14 +1,12 @@ -import argparse from collections import defaultdict import os -from jsonargparse import ArgumentParser - from ._common import with_repository, define_archive_filters_group from ..archive import Archive from ..constants import * # NOQA from ..helpers import bin_to_hex, Error from ..helpers import ProgressIndicatorPercent +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..remote import RemoteRepository from ..repository import Repository @@ -133,7 +131,7 @@ def build_parser_analyze(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_analyze.__doc__, epilog=analyze_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("analyze", subparser, help="analyze archives") define_archive_filters_group(subparser) diff --git a/src/borg/archiver/benchmark_cmd.py b/src/borg/archiver/benchmark_cmd.py index 8a94cfd273..abfd85b9e5 100644 --- a/src/borg/archiver/benchmark_cmd.py +++ b/src/borg/archiver/benchmark_cmd.py @@ -1,4 +1,3 @@ -import argparse from contextlib import contextmanager import json import logging @@ -6,14 +5,13 @@ import tempfile import time -from jsonargparse import ArgumentParser - from ..constants import * # NOQA from ..crypto.key import FlexiKey from ..helpers import format_file_size from ..helpers import json_print from ..helpers import msgpack from ..helpers import get_reset_ec +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..item import Item from ..platform import SyncFile @@ -361,7 +359,7 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): add_help=False, description="benchmark command", epilog=benchmark_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("benchmark", subparser, help="benchmark command") @@ -413,7 +411,7 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_benchmark_crud.__doc__, epilog=bench_crud_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) benchmark_parsers.add_subcommand( "crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete)." @@ -438,7 +436,7 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_benchmark_cpu.__doc__, epilog=bench_cpu_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) benchmark_parsers.add_subcommand("cpu", subparser, help="benchmarks Borg CPU-bound operations.") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index a39232468b..ac48febcd7 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -1,12 +1,9 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from ..archive import ArchiveChecker from ..constants import * # NOQA from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError from ..helpers import yes +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..logger import create_logger @@ -190,7 +187,7 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_check.__doc__, epilog=check_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("check", subparser, help="verify the repository") subparser.add_argument( diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 4e1cbe1c92..113f3f23de 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -1,13 +1,11 @@ -import argparse from pathlib import Path -from jsonargparse import ArgumentParser - from ._common import with_repository from ..archive import Archive from ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo from ..cache import files_cache_name, discover_files_cache_names from ..helpers import get_cache_dir +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..constants import * # NOQA from ..hashindex import ChunkIndex, ChunkIndexEntry from ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex @@ -264,7 +262,7 @@ def build_parser_compact(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_compact.__doc__, epilog=compact_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("compact", subparser, help="compact the repository") subparser.add_argument( diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 90d1443004..12a1b04b2e 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -50,12 +50,10 @@ - Suggests common file size values (500M, 1G, 10G, 100G, 1T, etc.) """ -import argparse - import shtab -from jsonargparse import ArgumentParser from jsonargparse._actions import _ActionSubCommands +from jsonargparse._completions import prepare_actions_context, shtab_prepare_actions, bash_compgen_typehint from ._common import process_epilog from ..constants import * # NOQA @@ -69,6 +67,7 @@ relative_time_marker_validator, parse_file_size, ) +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..helpers.time import timestamp from ..compress import CompressionSpec from ..helpers.parseformat import partial_format @@ -344,7 +343,6 @@ } """ - # Global zsh preamble providing dynamic completion for aid: archive IDs. # # Notes: @@ -735,16 +733,8 @@ def do_completion(self, args): bash_preamble = partial_format(BASH_PREAMBLE_TMPL, mapping) zsh_preamble = partial_format(ZSH_PREAMBLE_TMPL, mapping) - from jsonargparse._completions import ( - prepare_actions_context, - shtab_prepare_actions, - norm_name, - bash_compgen_typehint, - ) - - prog = norm_name(parser.prog) - if not prog: - prog = "borg" + parser.prog = "borg" + prog = "borg" preambles = [] if args.shell == "bash": preambles.append(bash_compgen_typehint.strip().replace("%s", prog)) @@ -777,7 +767,7 @@ def build_parser_completion(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_completion.__doc__, epilog=completion_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("completion", subparser, help="output shell completion script") subparser.add_argument( diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 6313837e26..a6184832c6 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -1,6 +1,5 @@ import errno import sys -import argparse import logging import os import posixpath @@ -9,8 +8,6 @@ import time from io import TextIOWrapper -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from .. import helpers from ..archive import Archive, is_special @@ -33,6 +30,7 @@ from ..helpers import iter_separated from ..helpers import MakePathSafeAction from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..patterns import PatternMatcher from ..platform import is_win32 @@ -681,7 +679,6 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): macOS examples are the apfs mounts of a typical macOS installation. Therefore, when using ``--one-file-system``, you should double-check that the backup works as intended. - .. _list_item_flags: Item flags @@ -779,7 +776,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_create.__doc__, epilog=create_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("create", subparser, help="create a backup") diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index fa35ac295a..d8feb8d9d4 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -1,9 +1,6 @@ -import argparse import json import textwrap -from jsonargparse import ArgumentParser - from ..archive import Archive from ..compress import CompressionSpec from ..constants import * # NOQA @@ -14,6 +11,7 @@ from ..helpers import StableDict from ..helpers import archivename_validator from ..helpers import CommandError, RTError +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..platform import get_process_id from ..repository import Repository, LIST_SCAN_LIMIT, repo_lister @@ -325,7 +323,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description="debugging command (not intended for normal use)", epilog=debug_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("debug", subparser, help="debugging command (not intended for normal use)") @@ -343,7 +341,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_info.__doc__, epilog=debug_info_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("info", subparser, help="show system infos for debugging / bug reports (debug)") @@ -357,7 +355,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_dump_archive_items.__doc__, epilog=debug_dump_archive_items_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-archive-items", subparser, help="dump archive items (metadata) (debug)") subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") @@ -372,7 +370,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_dump_archive.__doc__, epilog=debug_dump_archive_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-archive", subparser, help="dump decoded archive metadata (debug)") subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") @@ -388,7 +386,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_dump_manifest.__doc__, epilog=debug_dump_manifest_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-manifest", subparser, help="dump decoded repository metadata (debug)") subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into") @@ -403,7 +401,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_dump_repo_objs.__doc__, epilog=debug_dump_repo_objs_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-repo-objs", subparser, help="dump repo objects (debug)") @@ -417,7 +415,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_search_repo_objs.__doc__, epilog=debug_search_repo_objs_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("search-repo-objs", subparser, help="search repo objects (debug)") subparser.add_argument( @@ -437,7 +435,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_id_hash.__doc__, epilog=debug_id_hash_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("id-hash", subparser, help="compute id-hash for some file content (debug)") subparser.add_argument( @@ -455,7 +453,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_parse_obj.__doc__, epilog=debug_parse_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("parse-obj", subparser, help="parse borg object file into meta dict and data") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") @@ -480,7 +478,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_format_obj.__doc__, epilog=debug_format_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("format-obj", subparser, help="format file and metadata into a Borg object file") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") @@ -517,7 +515,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_get_obj.__doc__, epilog=debug_get_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("get-obj", subparser, help="get object from repository (debug)") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") @@ -533,7 +531,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_put_obj.__doc__, epilog=debug_put_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("put-obj", subparser, help="put object to repository (debug)") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to put into the repo") @@ -549,7 +547,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_delete_obj.__doc__, epilog=debug_delete_obj_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("delete-obj", subparser, help="delete object from repository (debug)") subparser.add_argument( @@ -566,7 +564,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_debug_convert_profile.__doc__, epilog=debug_convert_profile_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand( "convert-profile", subparser, help="convert Borg profile to Python profile (debug)" diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 995c6e2e41..5298e5e3e7 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -1,11 +1,9 @@ -import argparse import logging -from jsonargparse import ArgumentParser - from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -87,7 +85,7 @@ def build_parser_delete(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_delete.__doc__, epilog=delete_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("delete", subparser, help="delete archives") subparser.add_argument( diff --git a/src/borg/archiver/diff_cmd.py b/src/borg/archiver/diff_cmd.py index ceaa0b0d27..c3a993b64f 100644 --- a/src/borg/archiver/diff_cmd.py +++ b/src/borg/archiver/diff_cmd.py @@ -1,16 +1,14 @@ -import argparse import textwrap import json import sys import os -from jsonargparse import ArgumentParser - from ._common import with_repository, build_matcher, Highlander from ..archive import Archive from ..constants import * # NOQA from ..helpers import BaseFormatter, DiffFormatter, archivename_validator, PathSpec, BorgJsonEncoder from ..helpers import IncludePatternNeverMatchedWarning, remove_surrogates +from ..helpers.jap_helpers import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter from ..item import ItemDiff from ..manifest import Manifest from ..logger import create_logger @@ -206,7 +204,6 @@ def build_parser_diff(self, subparsers, common_parser, mid_common_parser): The following keys are always available: - """ ) + BaseFormatter.keys_help() @@ -271,7 +268,7 @@ def build_parser_diff(self, subparsers, common_parser, mid_common_parser): def diff_sort_spec_validator(s): if not isinstance(s, str): - raise argparse.ArgumentTypeError("unsupported sort field (not a string)") + raise ArgumentTypeError("unsupported sort field (not a string)") allowed = { "path", "size_added", @@ -289,11 +286,11 @@ def diff_sort_spec_validator(s): } parts = [p.strip() for p in s.split(",") if p.strip()] if not parts: - raise argparse.ArgumentTypeError("unsupported sort field: empty spec") + raise ArgumentTypeError("unsupported sort field: empty spec") for spec in parts: field = spec[1:] if spec and spec[0] in (">", "<") else spec if field not in allowed: - raise argparse.ArgumentTypeError(f"unsupported sort field: {field}") + raise ArgumentTypeError(f"unsupported sort field: {field}") return ",".join(parts) subparser = ArgumentParser( @@ -301,7 +298,7 @@ def diff_sort_spec_validator(s): add_help=False, description=self.do_diff.__doc__, epilog=diff_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("diff", subparser, help="find differences in archive contents") subparser.add_argument( diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index fb5a67e766..c6fb5743d0 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -1,10 +1,7 @@ import sys -import argparse import logging import stat -from jsonargparse import ArgumentParser - from ._common import with_repository, with_archive from ._common import build_filter, build_matcher from ..archive import BackupError @@ -14,6 +11,7 @@ from ..helpers import HardLinkManager from ..helpers import ProgressIndicatorPercent from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -162,7 +160,7 @@ def build_parser_extract(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_extract.__doc__, epilog=extract_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("extract", subparser, help="extract archive contents") subparser.add_argument( diff --git a/src/borg/archiver/help_cmd.py b/src/borg/archiver/help_cmd.py index 41b4124818..68e138cdc7 100644 --- a/src/borg/archiver/help_cmd.py +++ b/src/borg/archiver/help_cmd.py @@ -1,8 +1,7 @@ import collections import textwrap -from jsonargparse import ArgumentParser - +from ..helpers.jap_helpers import ArgumentParser from ..constants import * # NOQA from ..helpers.nanorst import rst_to_terminal @@ -162,7 +161,6 @@ class HelpMixIn: # not '/home/user/importantjunk' or '/etc/junk': $ borg create -e 'home/*/junk' archive / - # The contents of directories in '/home' are not backed up when their name # ends in '.tmp' $ borg create --exclude 're:^home/[^/]+\\.tmp/' archive / diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index c4a808265d..97c769c92a 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -1,13 +1,11 @@ -import argparse import textwrap from datetime import timedelta -from jsonargparse import ArgumentParser - from ._common import with_repository from ..archive import Archive from ..constants import * # NOQA from ..helpers import format_timedelta, json_print, basic_json_data, archivename_validator +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -85,7 +83,7 @@ def build_parser_info(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_info.__doc__, epilog=info_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("info", subparser, help="show repository or archive information") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index 0984a38fba..bd8161470f 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -1,13 +1,11 @@ -import argparse import os -from jsonargparse import ArgumentParser - from ..constants import * # NOQA from ..crypto.key import AESOCBRepoKey, CHPORepoKey, Blake2AESOCBRepoKey, Blake2CHPORepoKey from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey from ..crypto.keymanager import KeyManager from ..helpers import PathSpec, CommandError +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ._common import with_repository @@ -126,7 +124,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): add_help=False, description="Manage the keyfile or repokey of a repository", epilog="", - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("key", subparser, help="manage the repository key") @@ -168,7 +166,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_key_export.__doc__, epilog=key_export_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) key_parsers.add_subcommand("export", subparser, help="export the repository key for backup") subparser.add_argument("path", metavar="PATH", nargs="?", type=PathSpec, help="where to store the backup") @@ -208,7 +206,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_key_import.__doc__, epilog=key_import_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) key_parsers.add_subcommand("import", subparser, help="import the repository key from backup") subparser.add_argument( @@ -237,7 +235,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_key_change_passphrase.__doc__, epilog=change_passphrase_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) key_parsers.add_subcommand("change-passphrase", subparser, help="change the repository passphrase") @@ -259,7 +257,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_key_change_location.__doc__, epilog=change_location_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) key_parsers.add_subcommand("change-location", subparser, help="change the key location") subparser.add_argument( diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index 4a355275e9..19a4d44fc6 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -1,15 +1,13 @@ -import argparse import os import textwrap import sys -from jsonargparse import ArgumentParser - from ._common import with_repository, build_matcher, Highlander from ..archive import Archive from ..cache import Cache from ..constants import * # NOQA from ..helpers import ItemFormatter, BaseFormatter, archivename_validator, PathSpec +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -92,7 +90,6 @@ def build_parser_list(self, subparsers, common_parser, mid_common_parser): The following keys are always available: - """ ) + BaseFormatter.keys_help() @@ -110,7 +107,7 @@ def build_parser_list(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_list.__doc__, epilog=list_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("list", subparser, help="list archive contents") subparser.add_argument( diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index 03ab393a34..17cf90e0ba 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -1,12 +1,10 @@ -import argparse import subprocess -from jsonargparse import ArgumentParser, REMAINDER - from ._common import with_repository from ..cache import Cache from ..constants import * # NOQA from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter, REMAINDER from ..logger import create_logger @@ -52,7 +50,7 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_break_lock.__doc__, epilog=break_lock_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("break-lock", subparser, help="break the repository and cache locks") @@ -82,7 +80,7 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_with_lock.__doc__, epilog=with_lock_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("with-lock", subparser, help="run a user command with the lock held") subparser.add_argument("command", metavar="COMMAND", help="command to run") diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index 7a4164de1e..9174e3fd7f 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -1,13 +1,11 @@ -import argparse import os -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import RTError from ..helpers import PathSpec from ..helpers import umount +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..remote import cache_if_remote @@ -158,7 +156,7 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser add_help=False, description=self.do_mount.__doc__, epilog=mount_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("mount", subparser, help="mount a repository") self._define_borg_mount(subparser) @@ -176,7 +174,7 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser add_help=False, description=self.do_umount.__doc__, epilog=umount_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("umount", subparser, help="unmount a repository") subparser.add_argument( @@ -187,7 +185,7 @@ def build_parser_borgfs(self, parser): assert parser.prog == "borgfs" parser.description = self.do_mount.__doc__ parser.epilog = "For more information, see borg mount --help." - parser.formatter_class = argparse.RawDescriptionHelpFormatter + parser.formatter_class = RawDescriptionHelpFormatter parser.help = "mount a repository" self._define_borg_mount(parser) return parser diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 4f8481bddc..eeb20a8666 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -1,16 +1,14 @@ -import argparse from collections import OrderedDict from datetime import datetime, timezone, timedelta import logging from operator import attrgetter import os -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error from ..helpers import archivename_validator +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -280,7 +278,7 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_prune.__doc__, epilog=prune_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("prune", subparser, help="prune archives") subparser.add_argument( diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index ff074261e5..12d3f4a3d7 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -1,7 +1,3 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from ._common import build_matcher from ..archive import ArchiveRecreater @@ -9,6 +5,7 @@ from ..compress import CompressionSpec from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex from ..helpers import timestamp +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -109,7 +106,7 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_recreate.__doc__, epilog=recreate_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("recreate", subparser, help=self.do_recreate.__doc__) subparser.add_argument( diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index 316f9734cd..9fadaabf5e 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -1,10 +1,7 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository, with_archive from ..constants import * # NOQA from ..helpers import archivename_validator +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -35,7 +32,7 @@ def build_parser_rename(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_rename.__doc__, epilog=rename_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("rename", subparser, help="rename an archive") subparser.add_argument( diff --git a/src/borg/archiver/repo_compress_cmd.py b/src/borg/archiver/repo_compress_cmd.py index 7bf38e31c8..7c547ac79c 100644 --- a/src/borg/archiver/repo_compress_cmd.py +++ b/src/borg/archiver/repo_compress_cmd.py @@ -1,13 +1,11 @@ -import argparse from collections import defaultdict -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from ..constants import * # NOQA from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE from ..hashindex import ChunkIndex from ..helpers import sig_int, ProgressIndicatorPercent, Error +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..repository import Repository from ..remote import RemoteRepository from ..manifest import Manifest @@ -187,7 +185,7 @@ def build_parser_repo_compress(self, subparsers, common_parser, mid_common_parse add_help=False, description=self.do_repo_compress.__doc__, epilog=repo_compress_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("repo-compress", subparser, help=self.do_repo_compress.__doc__) diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index 7856e46fca..97b2c5a8ef 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -1,13 +1,10 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository, with_other_repository, Highlander from ..cache import Cache from ..constants import * # NOQA from ..crypto.key import key_creator, key_argument_names from ..helpers import CancelledByUser from ..helpers import location_validator, Location +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -197,7 +194,7 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) add_help=False, description=self.do_repo_create.__doc__, epilog=repo_create_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("repo-create", subparser, help="create a new, empty repository") subparser.add_argument( diff --git a/src/borg/archiver/repo_delete_cmd.py b/src/borg/archiver/repo_delete_cmd.py index 51319c6a31..7d0638e5fc 100644 --- a/src/borg/archiver/repo_delete_cmd.py +++ b/src/borg/archiver/repo_delete_cmd.py @@ -1,7 +1,3 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository from ..cache import Cache, SecurityManager from ..constants import * # NOQA @@ -9,6 +5,7 @@ from ..helpers import format_archive from ..helpers import bin_to_hex from ..helpers import yes +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest, NoManifestError from ..logger import create_logger @@ -109,7 +106,7 @@ def build_parser_repo_delete(self, subparsers, common_parser, mid_common_parser) add_help=False, description=self.do_repo_delete.__doc__, epilog=repo_delete_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("repo-delete", subparser, help="delete a repository") subparser.add_argument( diff --git a/src/borg/archiver/repo_info_cmd.py b/src/borg/archiver/repo_info_cmd.py index 43c37d5036..914cf433dd 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -1,11 +1,9 @@ -import argparse import textwrap -from jsonargparse import ArgumentParser - from ._common import with_repository from ..constants import * # NOQA from ..helpers import bin_to_hex, json_print, basic_json_data +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -70,7 +68,7 @@ def build_parser_repo_info(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_repo_info.__doc__, epilog=repo_info_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("repo-info", subparser, help="show repository information") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/repo_list_cmd.py b/src/borg/archiver/repo_list_cmd.py index a91c4a53be..39487ac4f6 100644 --- a/src/borg/archiver/repo_list_cmd.py +++ b/src/borg/archiver/repo_list_cmd.py @@ -1,13 +1,11 @@ -import argparse import os import textwrap import sys -from jsonargparse import ArgumentParser - from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -74,7 +72,6 @@ def build_parser_repo_list(self, subparsers, common_parser, mid_common_parser): The following keys are always available: - """ ) + BaseFormatter.keys_help() @@ -92,7 +89,7 @@ def build_parser_repo_list(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_repo_list.__doc__, epilog=repo_list_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("repo-list", subparser, help="list repository contents") subparser.add_argument( diff --git a/src/borg/archiver/repo_space_cmd.py b/src/borg/archiver/repo_space_cmd.py index 72a03007d7..329f5715da 100644 --- a/src/borg/archiver/repo_space_cmd.py +++ b/src/borg/archiver/repo_space_cmd.py @@ -1,14 +1,12 @@ -import argparse import math import os -from jsonargparse import ArgumentParser - from borgstore.store import ItemInfo from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import parse_file_size, format_file_size +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..logger import create_logger @@ -84,7 +82,6 @@ def build_parser_repo_space(self, subparsers, common_parser, mid_common_parser): $ borg compact -v # only this actually frees space of deleted archives $ borg repo-space --reserve 1G # reserve space again for next time - Reserved space is always rounded up to full reservation blocks of 64 MiB. """ ) @@ -93,7 +90,7 @@ def build_parser_repo_space(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_repo_space.__doc__, epilog=repo_space_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("repo-space", subparser, help="manage reserved space in a repository") subparser.add_argument( diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index 6774d65a26..68b4d78f86 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -1,9 +1,8 @@ -import argparse - from ..constants import * # NOQA from ..remote import RepositoryServer from ..logger import create_logger +from ..helpers.jap_helpers import RawDescriptionHelpFormatter, ArgumentParser logger = create_logger() @@ -19,7 +18,6 @@ def do_serve(self, args): ).serve() def build_parser_serve(self, subparsers, common_parser, mid_common_parser): - from jsonargparse import ArgumentParser from ._common import process_epilog serve_epilog = process_epilog( @@ -58,7 +56,7 @@ def build_parser_serve(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_serve.__doc__, epilog=serve_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("serve", subparser, help="start the repository server process") subparser.add_argument( diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py index 23f9c9d12a..e6de40a434 100644 --- a/src/borg/archiver/tag_cmd.py +++ b/src/borg/archiver/tag_cmd.py @@ -1,11 +1,8 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository, define_archive_filters_group from ..archive import Archive from ..constants import * # NOQA from ..helpers import bin_to_hex, archivename_validator, tag_validator, Error +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -87,7 +84,7 @@ def build_parser_tag(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_tag.__doc__, epilog=tag_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("tag", subparser, help="tag archives") subparser.add_argument("--set", dest="set_tags", metavar="TAG", type=tag_validator, nargs="+", help="set tags") diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index 8830ebcdc9..416b55fc2c 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -1,12 +1,9 @@ -import argparse import base64 import logging import os import stat import tarfile -from jsonargparse import ArgumentParser - from ..archive import Archive, TarfileObjectProcessors, ChunksProcessor from ..compress import CompressionSpec from ..constants import * # NOQA @@ -21,6 +18,7 @@ from ..helpers import timestamp, archive_ts_now from ..helpers import basic_json_data, json_print from ..helpers import log_multi +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ._common import with_repository, with_archive, Highlander, define_exclusion_group @@ -391,7 +389,7 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_export_tar.__doc__, epilog=export_tar_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("export-tar", subparser, help="create tarball from archive") subparser.add_argument( @@ -465,7 +463,7 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_import_tar.__doc__, epilog=import_tar_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("import-tar", subparser, help=self.do_import_tar.__doc__) subparser.add_argument( diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index a034b06353..62001f0808 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -1,7 +1,3 @@ -import argparse - -from jsonargparse import ArgumentParser - from ._common import with_repository, with_other_repository, Highlander from ..archive import Archive, cached_hash, DownloadPipeline from ..chunkers import get_chunker @@ -12,6 +8,7 @@ from ..helpers import location_validator, Location, archivename_validator, comment_validator from ..helpers import format_file_size, bin_to_hex from ..helpers import ChunkerParams, ChunkIteratorFileWrapper +from ..helpers.jap_helpers import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter from ..item import ChunkListEntry from ..manifest import Manifest from ..legacyrepository import LegacyRepository @@ -158,7 +155,7 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non for archive_info in archive_infos: try: archivename_validator(archive_info.name) - except argparse.ArgumentTypeError as err: + except ArgumentTypeError as err: an_errors.append(str(err)) if an_errors: an_errors.insert(0, "Invalid archive names detected, please rename them before transfer:") @@ -169,7 +166,7 @@ def do_transfer(self, args, *, repository, manifest, cache, other_repository=Non archive = Archive(other_manifest, archive_info.id) try: comment_validator(archive.metadata.get("comment", "")) - except argparse.ArgumentTypeError as err: + except ArgumentTypeError as err: ac_errors.append(f"{archive_info.name}: {err}") if ac_errors: ac_errors.insert(0, "Invalid archive comments detected, please fix them before transfer:") @@ -311,7 +308,6 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): borg --repo=DST_REPO transfer --other-repo=SRC_REPO # do it! borg --repo=DST_REPO transfer --other-repo=SRC_REPO --dry-run # check! anything left? - Data migration / upgrade from borg 1.x ++++++++++++++++++++++++++++++++++++++ @@ -332,7 +328,6 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): borg --repo=DST_REPO transfer --other-repo=SRC_REPO \\ --chunker-params=buzhash,19,23,21,4095 - """ ) subparser = ArgumentParser( @@ -340,7 +335,7 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_transfer.__doc__, epilog=transfer_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("transfer", subparser, help="transfer of archives from another repository") subparser.add_argument( diff --git a/src/borg/archiver/undelete_cmd.py b/src/borg/archiver/undelete_cmd.py index 1c2bdf6827..e0f5da7f0d 100644 --- a/src/borg/archiver/undelete_cmd.py +++ b/src/borg/archiver/undelete_cmd.py @@ -1,11 +1,9 @@ -import argparse import logging -from jsonargparse import ArgumentParser - from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator +from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger @@ -79,7 +77,7 @@ def build_parser_undelete(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_undelete.__doc__, epilog=undelete_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("undelete", subparser, help="undelete archives") subparser.add_argument( diff --git a/src/borg/archiver/version_cmd.py b/src/borg/archiver/version_cmd.py index bfa825ba0b..4cd8d0ddda 100644 --- a/src/borg/archiver/version_cmd.py +++ b/src/borg/archiver/version_cmd.py @@ -1,7 +1,6 @@ -import argparse - from .. import __version__ from ..constants import * # NOQA +from ..helpers.jap_helpers import RawDescriptionHelpFormatter, ArgumentParser from ..remote import RemoteRepository from ..logger import create_logger @@ -23,7 +22,6 @@ def do_version(self, args): print(f"{format_version(client_version)} / {format_version(server_version)}") def build_parser_version(self, subparsers, common_parser, mid_common_parser): - from jsonargparse import ArgumentParser from ._common import process_epilog version_epilog = process_epilog( @@ -57,6 +55,6 @@ def build_parser_version(self, subparsers, common_parser, mid_common_parser): add_help=False, description=self.do_version.__doc__, epilog=version_epilog, - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("version", subparser, help="display the Borg client and server versions") diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index a5f04998fd..33a7720439 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -15,10 +15,10 @@ which compressor has been used to compress the data and dispatch to the correct decompressor. """ -from argparse import ArgumentTypeError import math import random from struct import Struct +import sys import zlib try: @@ -28,15 +28,13 @@ except ImportError: from .constants import MAX_DATA_SIZE, ROBJ_FILE_STREAM from .helpers import Buffer, DecompressionError - -import sys +from .helpers.jap_helpers import ArgumentTypeError if sys.version_info >= (3, 14): from compression import zstd else: from backports import zstd - cdef extern from "lz4.h": int LZ4_compress_default(const char* source, char* dest, int inputSize, int maxOutputSize) nogil int LZ4_decompress_safe(const char* source, char* dest, int inputSize, int maxOutputSize) nogil @@ -120,7 +118,6 @@ cdef class CompressorBase: else: pass # raise ValueError("size not present and not in legacy mode") - cdef class DecidingCompressor(CompressorBase): """ base class for (de)compression classes that (based on an internal _decide @@ -188,7 +185,6 @@ class CNONE(CompressorBase): self.check_fix_size(meta, data) return meta, data - class LZ4(DecidingCompressor): """ raw LZ4 compression / decompression (liblz4). @@ -260,7 +256,6 @@ class LZ4(DecidingCompressor): self.check_fix_size(meta, data) return meta, data - class LZMA(DecidingCompressor): """ lzma compression / decompression @@ -355,7 +350,6 @@ class ZLIB(DecidingCompressor): except zlib.error as e: raise DecompressionError(str(e)) from None - class ZLIB_legacy(CompressorBase): """ zlib compression / decompression (python stdlib) @@ -402,7 +396,6 @@ class ZLIB_legacy(CompressorBase): except zlib.error as e: raise DecompressionError(str(e)) from None - class Auto(CompressorBase): """ Meta-Compressor that decides which compression to use based on LZ4's ratio. @@ -484,7 +477,6 @@ class Auto(CompressorBase): def detect(cls, data): raise NotImplementedError - class ObfuscateSize(CompressorBase): """ Meta-Compressor that obfuscates the compressed data size. @@ -569,7 +561,6 @@ class ObfuscateSize(CompressorBase): self.compressor = compressor_cls() return self.compressor.decompress(meta, compressed_data) # decompress data - # Maps valid compressor names to their class COMPRESSOR_TABLE = { CNONE.name: CNONE, @@ -624,7 +615,6 @@ class Compressor: else: raise ValueError('No decompressor for this data found: %r.', data[:2]) - class CompressionSpec: def __init__(self, s): if isinstance(s, CompressionSpec): diff --git a/src/borg/helpers/jap_helpers.py b/src/borg/helpers/jap_helpers.py index 80691b3797..a726a6d8e1 100644 --- a/src/borg/helpers/jap_helpers.py +++ b/src/borg/helpers/jap_helpers.py @@ -1,6 +1,10 @@ -from jsonargparse import Namespace from typing import Any +# here are the only imports from argparse and jsonargparse, +# all other imports of these names import them from here: +from argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter # noqa: F401 +from jsonargparse import ArgumentParser, Namespace, SUPPRESS, REMAINDER # noqa: F401 + def flatten_namespace(ns: Any) -> Namespace: """ diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 87b4b731e8..aaee4816a6 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -1,5 +1,4 @@ import abc -import argparse import base64 import binascii import hashlib @@ -24,6 +23,7 @@ from .errors import Error from .fs import get_keys_dir, make_path_safe, slashify +from .jap_helpers import Action, ArgumentError, ArgumentTypeError from .msgpack import Timestamp from .time import OutputTimestamp, format_time, safe_timestamp from .. import __version__ as borg_version @@ -124,7 +124,7 @@ def positive_int_validator(value): """argparse type for positive integers, N > 0.""" int_value = int(value) if int_value <= 0: - raise argparse.ArgumentTypeError("A positive integer is required: %s" % value) + raise ArgumentTypeError("A positive integer is required: %s" % value) return int_value @@ -152,7 +152,7 @@ def interval(s): number = s[:-1] suffix = s[-1] else: - raise argparse.ArgumentTypeError(f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}') + raise ArgumentTypeError(f'Unexpected time unit "{s[-1]}": choose from {", ".join(multiplier)}') try: seconds = int(number) * multiplier[suffix] @@ -160,7 +160,7 @@ def interval(s): seconds = -1 if seconds <= 0: - raise argparse.ArgumentTypeError(f'Invalid number "{number}": expected positive integer') + raise ArgumentTypeError(f'Invalid number "{number}": expected positive integer') return seconds @@ -171,7 +171,7 @@ def ChunkerParams(s): params = s.strip().split(",") count = len(params) if count == 0: - raise argparse.ArgumentTypeError("no chunker params given") + raise ArgumentTypeError("no chunker params given") algo = params[0].lower() if algo == CH_FAIL and count == 3: block_size = int(params[1]) @@ -186,47 +186,37 @@ def ChunkerParams(s): # or in-memory chunk management. # choose the block (chunk) size wisely: if you have a lot of data and you cut # it into very small chunks, you are asking for trouble! - raise argparse.ArgumentTypeError("block_size must not be less than 64 Bytes") + raise ArgumentTypeError("block_size must not be less than 64 Bytes") if block_size > MAX_DATA_SIZE or header_size > MAX_DATA_SIZE: - raise argparse.ArgumentTypeError( - "block_size and header_size must not exceed MAX_DATA_SIZE [%d]" % MAX_DATA_SIZE - ) + raise ArgumentTypeError("block_size and header_size must not exceed MAX_DATA_SIZE [%d]" % MAX_DATA_SIZE) return algo, block_size, header_size if algo == "default" and count == 1: # default return CHUNKER_PARAMS if algo == CH_BUZHASH64 and count == 5: # buzhash64, chunk_min, chunk_max, chunk_mask, window_size chunk_min, chunk_max, chunk_mask, window_size = (int(p) for p in params[1:]) if not (chunk_min <= chunk_mask <= chunk_max): - raise argparse.ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max") + raise ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max") if chunk_min < 6: # see comment in 'fixed' algo check - raise argparse.ArgumentTypeError( - "min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)" - ) + raise ArgumentTypeError("min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)") if chunk_max > 23: - raise argparse.ArgumentTypeError( - "max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)" - ) + raise ArgumentTypeError("max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)") # note that for buzhash64, there is no problem with even window_size. return CH_BUZHASH64, chunk_min, chunk_max, chunk_mask, window_size # this must stay last as it deals with old-style compat mode (no algorithm, 4 params, buzhash): if algo == CH_BUZHASH and count == 5 or count == 4: # [buzhash, ]chunk_min, chunk_max, chunk_mask, window_size chunk_min, chunk_max, chunk_mask, window_size = (int(p) for p in params[count - 4 :]) if not (chunk_min <= chunk_mask <= chunk_max): - raise argparse.ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max") + raise ArgumentTypeError("required: chunk_min <= chunk_mask <= chunk_max") if chunk_min < 6: # see comment in 'fixed' algo check - raise argparse.ArgumentTypeError( - "min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)" - ) + raise ArgumentTypeError("min. chunk size exponent must not be less than 6 (2^6 = 64B min. chunk size)") if chunk_max > 23: - raise argparse.ArgumentTypeError( - "max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)" - ) + raise ArgumentTypeError("max. chunk size exponent must not be more than 23 (2^23 = 8MiB max. chunk size)") if window_size % 2 == 0: - raise argparse.ArgumentTypeError("window_size must be an uneven (odd) number") + raise ArgumentTypeError("window_size must be an uneven (odd) number") return CH_BUZHASH, chunk_min, chunk_max, chunk_mask, window_size - raise argparse.ArgumentTypeError("invalid chunker params") + raise ArgumentTypeError("invalid chunker params") def FilesCacheMode(s): @@ -236,13 +226,11 @@ def FilesCacheMode(s): return s entries = set(s.strip().split(",")) if not entries <= set(ENTRIES_MAP): - raise argparse.ArgumentTypeError( - "cache mode must be a comma-separated list of: %s" % ",".join(sorted(ENTRIES_MAP)) - ) + raise ArgumentTypeError("cache mode must be a comma-separated list of: %s" % ",".join(sorted(ENTRIES_MAP))) short_entries = {ENTRIES_MAP[entry] for entry in entries} mode = "".join(sorted(short_entries)) if mode not in VALID_MODES: - raise argparse.ArgumentTypeError("cache mode short must be one of: %s" % ",".join(VALID_MODES)) + raise ArgumentTypeError("cache mode short must be one of: %s" % ",".join(VALID_MODES)) return mode @@ -338,13 +326,13 @@ def __call__(self, text, overrides=None): def PathSpec(text): if not text: - raise argparse.ArgumentTypeError("Empty strings are not accepted as paths.") + raise ArgumentTypeError("Empty strings are not accepted as paths.") return text def FilesystemPathSpec(text): if not text: - raise argparse.ArgumentTypeError("Empty strings are not accepted as paths.") + raise ArgumentTypeError("Empty strings are not accepted as paths.") return slashify(text) @@ -353,7 +341,7 @@ def SortBySpec(text): for sort_key in text.split(","): if sort_key not in AI_HUMAN_SORT_KEYS and sort_key != "ts": # idempotency: do not reject ts - raise argparse.ArgumentTypeError("Invalid sort key: %s" % sort_key) + raise ArgumentTypeError("Invalid sort key: %s" % sort_key) return text.replace("timestamp", "ts").replace("archive", "name") @@ -643,12 +631,12 @@ def validator(text): try: loc = Location(text, other=other) except ValueError as err: - raise argparse.ArgumentTypeError(str(err)) from None + raise ArgumentTypeError(str(err)) from None if proto is not None and loc.proto != proto: if proto == "file": - raise argparse.ArgumentTypeError('"%s": Repository must be local' % text) + raise ArgumentTypeError('"%s": Repository must be local' % text) else: - raise argparse.ArgumentTypeError('"%s": Repository must be remote' % text) + raise ArgumentTypeError('"%s": Repository must be remote' % text) return loc return validator @@ -658,7 +646,7 @@ def relative_time_marker_validator(text: str): time_marker_regex = r"^\d+[ymwdHMS]$" match = re.compile(time_marker_regex).search(text) if not match: - raise argparse.ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S") + raise ArgumentTypeError(f"Invalid relative time marker used: {text}, choose from y, m, w, d, H, M, S") else: return text @@ -667,22 +655,20 @@ def text_validator(*, name, max_length, min_length=0, invalid_ctrl_chars="\0", i def validator(text): assert isinstance(text, str) if len(text) < min_length: - raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length < {min_length}]') + raise ArgumentTypeError(f'Invalid {name}: "{text}" [length < {min_length}]') if len(text) > max_length: - raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [length > {max_length}]') + raise ArgumentTypeError(f'Invalid {name}: "{text}" [length > {max_length}]') if invalid_ctrl_chars and re.search(f"[{re.escape(invalid_ctrl_chars)}]", text): - raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [invalid control chars detected]') + raise ArgumentTypeError(f'Invalid {name}: "{text}" [invalid control chars detected]') if invalid_chars and re.search(f"[{re.escape(invalid_chars)}]", text): - raise argparse.ArgumentTypeError( - f'Invalid {name}: "{text}" [invalid chars detected matching "{invalid_chars}"]' - ) + raise ArgumentTypeError(f'Invalid {name}: "{text}" [invalid chars detected matching "{invalid_chars}"]') if no_blanks and (text.startswith(" ") or text.endswith(" ")): - raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [leading or trailing blanks detected]') + raise ArgumentTypeError(f'Invalid {name}: "{text}" [leading or trailing blanks detected]') try: text.encode("utf-8", errors="strict") except UnicodeEncodeError: # looks like text contains surrogate-escapes - raise argparse.ArgumentTypeError(f'Invalid {name}: "{text}" [contains non-unicode characters]') + raise ArgumentTypeError(f'Invalid {name}: "{text}" [contains non-unicode characters]') return text return validator @@ -1318,7 +1304,7 @@ def decode(d): return decode(d) -class Highlander(argparse.Action): +class Highlander(Action): """make sure some option is only given once""" def __init__(self, *args, **kwargs): @@ -1327,7 +1313,7 @@ def __init__(self, *args, **kwargs): def __call__(self, parser, namespace, values, option_string=None): if self.__called: - raise argparse.ArgumentError(self, "There can be only one.") + raise ArgumentError(self, "There can be only one.") self.__called = True setattr(namespace, self.dest, values) @@ -1337,7 +1323,7 @@ def __call__(self, parser, namespace, path, option_string=None): try: sanitized_path = make_path_safe(path) except ValueError as e: - raise argparse.ArgumentError(self, e) + raise ArgumentError(self, e) if sanitized_path == ".": - raise argparse.ArgumentError(self, f"{path!r} is not a valid file name") + raise ArgumentError(self, f"{path!r} is not a valid file name") setattr(namespace, self.dest, sanitized_path) diff --git a/src/borg/patterns.py b/src/borg/patterns.py index 8c03987b77..e382e53143 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -1,4 +1,3 @@ -import argparse import fnmatch import posixpath import re @@ -8,6 +7,7 @@ from enum import Enum from .helpers import clean_lines, shellpattern +from .helpers.jap_helpers import Action, ArgumentTypeError from .helpers.errors import Error @@ -36,7 +36,7 @@ def load_exclude_file(fileobj, patterns): patterns.append(parse_exclude_pattern(patternstr)) -class ArgparsePatternAction(argparse.Action): +class ArgparsePatternAction(Action): def __init__(self, nargs=1, **kw): super().__init__(nargs=nargs, **kw) @@ -44,7 +44,7 @@ def __call__(self, parser, args, values, option_string=None): parse_patternfile_line(values[0], args.pattern_roots, args.patterns, ShellPattern) -class ArgparsePatternFileAction(argparse.Action): +class ArgparsePatternFileAction(Action): def __init__(self, nargs=1, **kw): super().__init__(nargs=nargs, **kw) @@ -357,16 +357,16 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): "p": IECommand.PatternStyle, } if not cmd_line_str: - raise argparse.ArgumentTypeError("A pattern/command must not be empty.") + raise ArgumentTypeError("A pattern/command must not be empty.") cmd = cmd_prefix_map.get(cmd_line_str[0]) if cmd is None: - raise argparse.ArgumentTypeError("A pattern/command must start with any of: %s" % ", ".join(cmd_prefix_map)) + raise ArgumentTypeError("A pattern/command must start with any of: %s" % ", ".join(cmd_prefix_map)) # remaining text on command-line following the command character remainder_str = cmd_line_str[1:].lstrip() if not remainder_str: - raise argparse.ArgumentTypeError("A pattern/command must have a value part.") + raise ArgumentTypeError("A pattern/command must have a value part.") if cmd is IECommand.RootPath: # TODO: validate string? @@ -376,7 +376,7 @@ def parse_inclexcl_command(cmd_line_str, fallback=ShellPattern): try: val = get_pattern_class(remainder_str) except ValueError: - raise argparse.ArgumentTypeError(f"Invalid pattern style: {remainder_str}") + raise ArgumentTypeError(f"Invalid pattern style: {remainder_str}") else: # determine recurse_dir based on command type recurse_dir = command_recurses_dir(cmd) diff --git a/src/borg/testsuite/archiver/argparsing_test.py b/src/borg/testsuite/archiver/argparsing_test.py index 58d09076d6..afc706b2c5 100644 --- a/src/borg/testsuite/archiver/argparsing_test.py +++ b/src/borg/testsuite/archiver/argparsing_test.py @@ -1,9 +1,7 @@ -import argparse import pytest -from jsonargparse import ArgumentParser - from . import Archiver, RK_ENCRYPTION, cmd +from ...helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter, flatten_namespace def test_bad_filters(archiver): @@ -118,14 +116,12 @@ def common_parser(self, parser): @pytest.fixture def parse_vars_from_line(self, parser, subcommands, common_parser): - from ...helpers.jap_helpers import flatten_namespace - subparser = ArgumentParser( parents=[common_parser], add_help=False, description="foo", epilog="bar", - formatter_class=argparse.RawDescriptionHelpFormatter, + formatter_class=RawDescriptionHelpFormatter, ) subparser.add_argument("--foo-bar", dest="foo_bar", action="store_true") subcommands.add_subcommand("subcmd", subparser, help="baz") diff --git a/src/borg/testsuite/compress_test.py b/src/borg/testsuite/compress_test.py index 9ec9f1046e..ea90d76dc6 100644 --- a/src/borg/testsuite/compress_test.py +++ b/src/borg/testsuite/compress_test.py @@ -1,4 +1,3 @@ -import argparse import os import zlib @@ -6,6 +5,7 @@ from ..compress import get_compressor, Compressor, CompressionSpec, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto from ..constants import ROBJ_FILE_STREAM, ROBJ_ARCHIVE_META +from ..helpers.jap_helpers import ArgumentTypeError DATA = b"fooooooooobaaaaaaaar" * 10 params = dict(name="zlib", level=6) @@ -209,7 +209,7 @@ def test_specified_compression_level(c_type, c_name, c_levels): @pytest.mark.parametrize("invalid_spec", ["", "lzma,9,invalid", "invalid"]) def test_invalid_compression_level(invalid_spec): - with pytest.raises(argparse.ArgumentTypeError): + with pytest.raises(ArgumentTypeError): CompressionSpec(invalid_spec) diff --git a/src/borg/testsuite/helpers/parseformat_test.py b/src/borg/testsuite/helpers/parseformat_test.py index c90c55920b..9c356fe263 100644 --- a/src/borg/testsuite/helpers/parseformat_test.py +++ b/src/borg/testsuite/helpers/parseformat_test.py @@ -1,11 +1,12 @@ import base64 import os -from argparse import ArgumentTypeError + from datetime import datetime, timezone import pytest from ...constants import * # NOQA +from ...helpers.jap_helpers import ArgumentTypeError from ...helpers.parseformat import ( bin_to_hex, binary_to_json, diff --git a/src/borg/testsuite/patterns_test.py b/src/borg/testsuite/patterns_test.py index f6fd602c57..b434e4540b 100644 --- a/src/borg/testsuite/patterns_test.py +++ b/src/borg/testsuite/patterns_test.py @@ -1,10 +1,10 @@ -import argparse import io import os.path import sys import pytest +from ..helpers.jap_helpers import ArgumentTypeError from ..patterns import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..patterns import load_exclude_file, load_pattern_file from ..patterns import parse_pattern, PatternMatcher @@ -491,7 +491,7 @@ def test_load_invalid_patterns_from_file(tmpdir, lines): with patternfile.open("wt") as fh: fh.write("\n".join(lines)) filename = str(patternfile) - with pytest.raises(argparse.ArgumentTypeError): + with pytest.raises(ArgumentTypeError): roots = [] inclexclpatterns = [] with open(filename) as f: From 7250fd947e3b1d717558212d623cd25401b13d95 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 26 Feb 2026 00:41:40 +0100 Subject: [PATCH 12/27] helpers.jap_helper -> helpers.argparsing --- src/borg/archiver/__init__.py | 2 +- src/borg/archiver/_common.py | 2 +- src/borg/archiver/analyze_cmd.py | 2 +- src/borg/archiver/benchmark_cmd.py | 2 +- src/borg/archiver/check_cmd.py | 2 +- src/borg/archiver/compact_cmd.py | 2 +- src/borg/archiver/completion_cmd.py | 2 +- src/borg/archiver/create_cmd.py | 2 +- src/borg/archiver/debug_cmd.py | 2 +- src/borg/archiver/delete_cmd.py | 2 +- src/borg/archiver/diff_cmd.py | 2 +- src/borg/archiver/extract_cmd.py | 2 +- src/borg/archiver/help_cmd.py | 2 +- src/borg/archiver/info_cmd.py | 2 +- src/borg/archiver/key_cmds.py | 2 +- src/borg/archiver/list_cmd.py | 2 +- src/borg/archiver/lock_cmds.py | 2 +- src/borg/archiver/mount_cmds.py | 2 +- src/borg/archiver/prune_cmd.py | 2 +- src/borg/archiver/recreate_cmd.py | 2 +- src/borg/archiver/rename_cmd.py | 2 +- src/borg/archiver/repo_compress_cmd.py | 2 +- src/borg/archiver/repo_create_cmd.py | 2 +- src/borg/archiver/repo_delete_cmd.py | 2 +- src/borg/archiver/repo_info_cmd.py | 2 +- src/borg/archiver/repo_list_cmd.py | 2 +- src/borg/archiver/repo_space_cmd.py | 2 +- src/borg/archiver/serve_cmd.py | 2 +- src/borg/archiver/tag_cmd.py | 2 +- src/borg/archiver/tar_cmds.py | 2 +- src/borg/archiver/transfer_cmd.py | 2 +- src/borg/archiver/undelete_cmd.py | 2 +- src/borg/archiver/version_cmd.py | 2 +- src/borg/compress.pyx | 2 +- src/borg/helpers/{jap_helpers.py => argparsing.py} | 0 src/borg/helpers/parseformat.py | 2 +- src/borg/patterns.py | 2 +- src/borg/testsuite/archiver/argparsing_test.py | 2 +- src/borg/testsuite/compress_test.py | 2 +- src/borg/testsuite/helpers/parseformat_test.py | 2 +- src/borg/testsuite/patterns_test.py | 2 +- 41 files changed, 40 insertions(+), 40 deletions(-) rename src/borg/helpers/{jap_helpers.py => argparsing.py} (100%) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 6329221b60..7415b4f94b 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -39,7 +39,7 @@ from ..helpers import format_file_size from ..helpers import remove_surrogates, text_to_json from ..helpers import DatetimeWrapper, replace_placeholders - from ..helpers.jap_helpers import flatten_namespace, ArgumentTypeError, ArgumentParser, Namespace, SUPPRESS + from ..helpers.argparsing import flatten_namespace, ArgumentTypeError, ArgumentParser, Namespace, SUPPRESS from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from ..helpers import ErrorIgnoringTextIOWrapper diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index ad2817d6db..ebba27ae55 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -9,7 +9,7 @@ from ..helpers import Error from ..helpers import SortBySpec, positive_int_validator, location_validator, Location, relative_time_marker_validator from ..helpers import Highlander -from ..helpers.jap_helpers import SUPPRESS +from ..helpers.argparsing import SUPPRESS from ..helpers.nanorst import rst_to_terminal from ..manifest import Manifest, AI_HUMAN_SORT_KEYS from ..patterns import PatternMatcher diff --git a/src/borg/archiver/analyze_cmd.py b/src/borg/archiver/analyze_cmd.py index 6ea240a5a7..662b3b03bd 100644 --- a/src/borg/archiver/analyze_cmd.py +++ b/src/borg/archiver/analyze_cmd.py @@ -6,7 +6,7 @@ from ..constants import * # NOQA from ..helpers import bin_to_hex, Error from ..helpers import ProgressIndicatorPercent -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..remote import RemoteRepository from ..repository import Repository diff --git a/src/borg/archiver/benchmark_cmd.py b/src/borg/archiver/benchmark_cmd.py index abfd85b9e5..d152ff8342 100644 --- a/src/borg/archiver/benchmark_cmd.py +++ b/src/borg/archiver/benchmark_cmd.py @@ -11,7 +11,7 @@ from ..helpers import json_print from ..helpers import msgpack from ..helpers import get_reset_ec -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..item import Item from ..platform import SyncFile diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index ac48febcd7..0889e4ce02 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -3,7 +3,7 @@ from ..constants import * # NOQA from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError from ..helpers import yes -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..logger import create_logger diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 113f3f23de..4efdcd01b2 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -5,7 +5,7 @@ from ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo from ..cache import files_cache_name, discover_files_cache_names from ..helpers import get_cache_dir -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..constants import * # NOQA from ..hashindex import ChunkIndex, ChunkIndexEntry from ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 12a1b04b2e..22b7208bfb 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -67,7 +67,7 @@ relative_time_marker_validator, parse_file_size, ) -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..helpers.time import timestamp from ..compress import CompressionSpec from ..helpers.parseformat import partial_format diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index a6184832c6..e888ff9753 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -30,7 +30,7 @@ from ..helpers import iter_separated from ..helpers import MakePathSafeAction from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..patterns import PatternMatcher from ..platform import is_win32 diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index d8feb8d9d4..4e47f1e5ce 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -11,7 +11,7 @@ from ..helpers import StableDict from ..helpers import archivename_validator from ..helpers import CommandError, RTError -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..platform import get_process_id from ..repository import Repository, LIST_SCAN_LIMIT, repo_lister diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index 5298e5e3e7..e7cfea17fc 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/diff_cmd.py b/src/borg/archiver/diff_cmd.py index c3a993b64f..3430461d0c 100644 --- a/src/borg/archiver/diff_cmd.py +++ b/src/borg/archiver/diff_cmd.py @@ -8,7 +8,7 @@ from ..constants import * # NOQA from ..helpers import BaseFormatter, DiffFormatter, archivename_validator, PathSpec, BorgJsonEncoder from ..helpers import IncludePatternNeverMatchedWarning, remove_surrogates -from ..helpers.jap_helpers import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter from ..item import ItemDiff from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index c6fb5743d0..b61585a0b3 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -11,7 +11,7 @@ from ..helpers import HardLinkManager from ..helpers import ProgressIndicatorPercent from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/help_cmd.py b/src/borg/archiver/help_cmd.py index 68e138cdc7..7b486847f7 100644 --- a/src/borg/archiver/help_cmd.py +++ b/src/borg/archiver/help_cmd.py @@ -1,7 +1,7 @@ import collections import textwrap -from ..helpers.jap_helpers import ArgumentParser +from ..helpers.argparsing import ArgumentParser from ..constants import * # NOQA from ..helpers.nanorst import rst_to_terminal diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index 97c769c92a..eaab7531b8 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -5,7 +5,7 @@ from ..archive import Archive from ..constants import * # NOQA from ..helpers import format_timedelta, json_print, basic_json_data, archivename_validator -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index bd8161470f..4f3b620e9e 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -5,7 +5,7 @@ from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey from ..crypto.keymanager import KeyManager from ..helpers import PathSpec, CommandError -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ._common import with_repository diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index 19a4d44fc6..0099831fde 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -7,7 +7,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..helpers import ItemFormatter, BaseFormatter, archivename_validator, PathSpec -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index 17cf90e0ba..4a8bcae051 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -4,7 +4,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter, REMAINDER +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter, REMAINDER from ..logger import create_logger diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index 9174e3fd7f..4fba328d39 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -5,7 +5,7 @@ from ..helpers import RTError from ..helpers import PathSpec from ..helpers import umount -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..remote import cache_if_remote diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index eeb20a8666..4498981cb1 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -8,7 +8,7 @@ from ..constants import * # NOQA from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error from ..helpers import archivename_validator -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 12d3f4a3d7..5a26e55577 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -5,7 +5,7 @@ from ..compress import CompressionSpec from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex from ..helpers import timestamp -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index 9fadaabf5e..13fe9fa510 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -1,7 +1,7 @@ from ._common import with_repository, with_archive from ..constants import * # NOQA from ..helpers import archivename_validator -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/repo_compress_cmd.py b/src/borg/archiver/repo_compress_cmd.py index 7c547ac79c..769dd8ac19 100644 --- a/src/borg/archiver/repo_compress_cmd.py +++ b/src/borg/archiver/repo_compress_cmd.py @@ -5,7 +5,7 @@ from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE from ..hashindex import ChunkIndex from ..helpers import sig_int, ProgressIndicatorPercent, Error -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..repository import Repository from ..remote import RemoteRepository from ..manifest import Manifest diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index 97b2c5a8ef..3deb404b16 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -4,7 +4,7 @@ from ..crypto.key import key_creator, key_argument_names from ..helpers import CancelledByUser from ..helpers import location_validator, Location -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/repo_delete_cmd.py b/src/borg/archiver/repo_delete_cmd.py index 7d0638e5fc..8d8470c841 100644 --- a/src/borg/archiver/repo_delete_cmd.py +++ b/src/borg/archiver/repo_delete_cmd.py @@ -5,7 +5,7 @@ from ..helpers import format_archive from ..helpers import bin_to_hex from ..helpers import yes -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest, NoManifestError from ..logger import create_logger diff --git a/src/borg/archiver/repo_info_cmd.py b/src/borg/archiver/repo_info_cmd.py index 914cf433dd..419337fa2f 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA from ..helpers import bin_to_hex, json_print, basic_json_data -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/repo_list_cmd.py b/src/borg/archiver/repo_list_cmd.py index 39487ac4f6..fa93a2d5bf 100644 --- a/src/borg/archiver/repo_list_cmd.py +++ b/src/borg/archiver/repo_list_cmd.py @@ -5,7 +5,7 @@ from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/repo_space_cmd.py b/src/borg/archiver/repo_space_cmd.py index 329f5715da..3a2e326555 100644 --- a/src/borg/archiver/repo_space_cmd.py +++ b/src/borg/archiver/repo_space_cmd.py @@ -6,7 +6,7 @@ from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import parse_file_size, format_file_size -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..logger import create_logger diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index 68b4d78f86..744eedb603 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -2,7 +2,7 @@ from ..remote import RepositoryServer from ..logger import create_logger -from ..helpers.jap_helpers import RawDescriptionHelpFormatter, ArgumentParser +from ..helpers.argparsing import RawDescriptionHelpFormatter, ArgumentParser logger = create_logger() diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py index e6de40a434..d842e65a27 100644 --- a/src/borg/archiver/tag_cmd.py +++ b/src/borg/archiver/tag_cmd.py @@ -2,7 +2,7 @@ from ..archive import Archive from ..constants import * # NOQA from ..helpers import bin_to_hex, archivename_validator, tag_validator, Error -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index 416b55fc2c..7702c24497 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -18,7 +18,7 @@ from ..helpers import timestamp, archive_ts_now from ..helpers import basic_json_data, json_print from ..helpers import log_multi -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ._common import with_repository, with_archive, Highlander, define_exclusion_group diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index 62001f0808..dab3abc66c 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -8,7 +8,7 @@ from ..helpers import location_validator, Location, archivename_validator, comment_validator from ..helpers import format_file_size, bin_to_hex from ..helpers import ChunkerParams, ChunkIteratorFileWrapper -from ..helpers.jap_helpers import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter from ..item import ChunkListEntry from ..manifest import Manifest from ..legacyrepository import LegacyRepository diff --git a/src/borg/archiver/undelete_cmd.py b/src/borg/archiver/undelete_cmd.py index e0f5da7f0d..7e4720b541 100644 --- a/src/borg/archiver/undelete_cmd.py +++ b/src/borg/archiver/undelete_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator -from ..helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest from ..logger import create_logger diff --git a/src/borg/archiver/version_cmd.py b/src/borg/archiver/version_cmd.py index 4cd8d0ddda..30081e1a4d 100644 --- a/src/borg/archiver/version_cmd.py +++ b/src/borg/archiver/version_cmd.py @@ -1,6 +1,6 @@ from .. import __version__ from ..constants import * # NOQA -from ..helpers.jap_helpers import RawDescriptionHelpFormatter, ArgumentParser +from ..helpers.argparsing import RawDescriptionHelpFormatter, ArgumentParser from ..remote import RemoteRepository from ..logger import create_logger diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 33a7720439..733828720c 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -28,7 +28,7 @@ except ImportError: from .constants import MAX_DATA_SIZE, ROBJ_FILE_STREAM from .helpers import Buffer, DecompressionError -from .helpers.jap_helpers import ArgumentTypeError +from .helpers.argparsing import ArgumentTypeError if sys.version_info >= (3, 14): from compression import zstd diff --git a/src/borg/helpers/jap_helpers.py b/src/borg/helpers/argparsing.py similarity index 100% rename from src/borg/helpers/jap_helpers.py rename to src/borg/helpers/argparsing.py diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index aaee4816a6..844cd3da49 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -23,7 +23,7 @@ from .errors import Error from .fs import get_keys_dir, make_path_safe, slashify -from .jap_helpers import Action, ArgumentError, ArgumentTypeError +from .argparsing import Action, ArgumentError, ArgumentTypeError from .msgpack import Timestamp from .time import OutputTimestamp, format_time, safe_timestamp from .. import __version__ as borg_version diff --git a/src/borg/patterns.py b/src/borg/patterns.py index e382e53143..bfbf69712b 100644 --- a/src/borg/patterns.py +++ b/src/borg/patterns.py @@ -7,7 +7,7 @@ from enum import Enum from .helpers import clean_lines, shellpattern -from .helpers.jap_helpers import Action, ArgumentTypeError +from .helpers.argparsing import Action, ArgumentTypeError from .helpers.errors import Error diff --git a/src/borg/testsuite/archiver/argparsing_test.py b/src/borg/testsuite/archiver/argparsing_test.py index afc706b2c5..dbaa306406 100644 --- a/src/borg/testsuite/archiver/argparsing_test.py +++ b/src/borg/testsuite/archiver/argparsing_test.py @@ -1,7 +1,7 @@ import pytest from . import Archiver, RK_ENCRYPTION, cmd -from ...helpers.jap_helpers import ArgumentParser, RawDescriptionHelpFormatter, flatten_namespace +from ...helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter, flatten_namespace def test_bad_filters(archiver): diff --git a/src/borg/testsuite/compress_test.py b/src/borg/testsuite/compress_test.py index ea90d76dc6..7dbc7574d9 100644 --- a/src/borg/testsuite/compress_test.py +++ b/src/borg/testsuite/compress_test.py @@ -5,7 +5,7 @@ from ..compress import get_compressor, Compressor, CompressionSpec, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto from ..constants import ROBJ_FILE_STREAM, ROBJ_ARCHIVE_META -from ..helpers.jap_helpers import ArgumentTypeError +from ..helpers.argparsing import ArgumentTypeError DATA = b"fooooooooobaaaaaaaar" * 10 params = dict(name="zlib", level=6) diff --git a/src/borg/testsuite/helpers/parseformat_test.py b/src/borg/testsuite/helpers/parseformat_test.py index 9c356fe263..82026f0b0f 100644 --- a/src/borg/testsuite/helpers/parseformat_test.py +++ b/src/borg/testsuite/helpers/parseformat_test.py @@ -6,7 +6,7 @@ import pytest from ...constants import * # NOQA -from ...helpers.jap_helpers import ArgumentTypeError +from ...helpers.argparsing import ArgumentTypeError from ...helpers.parseformat import ( bin_to_hex, binary_to_json, diff --git a/src/borg/testsuite/patterns_test.py b/src/borg/testsuite/patterns_test.py index b434e4540b..be2bc4c07e 100644 --- a/src/borg/testsuite/patterns_test.py +++ b/src/borg/testsuite/patterns_test.py @@ -4,7 +4,7 @@ import pytest -from ..helpers.jap_helpers import ArgumentTypeError +from ..helpers.argparsing import ArgumentTypeError from ..patterns import PathFullPattern, PathPrefixPattern, FnmatchPattern, ShellPattern, RegexPattern from ..patterns import load_exclude_file, load_pattern_file from ..patterns import parse_pattern, PatternMatcher From 83b8935abf48c97d666dbef67ad19aa9dd43496d Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 26 Feb 2026 01:11:51 +0100 Subject: [PATCH 13/27] move compress.CompressionSpec to helpers.parseformat.CompressionSpec other validators / specs are also there and it is easier to maintain as Python code. the compress module is Cython code. --- src/borg/archive.py | 3 +- src/borg/archiver/benchmark_cmd.py | 4 +- src/borg/archiver/completion_cmd.py | 2 +- src/borg/archiver/create_cmd.py | 3 +- src/borg/archiver/debug_cmd.py | 3 +- src/borg/archiver/recreate_cmd.py | 3 +- src/borg/archiver/repo_compress_cmd.py | 4 +- src/borg/archiver/tar_cmds.py | 3 +- src/borg/archiver/transfer_cmd.py | 3 +- src/borg/compress.pyi | 6 --- src/borg/compress.pyx | 63 ------------------------ src/borg/helpers/__init__.py | 1 + src/borg/helpers/parseformat.py | 66 ++++++++++++++++++++++++++ src/borg/testsuite/compress_test.py | 3 +- 14 files changed, 79 insertions(+), 88 deletions(-) diff --git a/src/borg/archive.py b/src/borg/archive.py index 509c46ddb3..61f1f0bfa0 100644 --- a/src/borg/archive.py +++ b/src/borg/archive.py @@ -25,7 +25,6 @@ from .chunkers import get_chunker, Chunk from .cache import ChunkListEntry, build_chunkindex_from_repo, delete_chunkindex_cache from .crypto.key import key_factory, UnsupportedPayloadError -from .compress import CompressionSpec from .constants import * # NOQA from .crypto.low_level import IntegrityError as IntegrityErrorBase from .helpers import BackupError, BackupRaceConditionError, BackupItemExcluded @@ -35,7 +34,7 @@ from .helpers import ChunkIteratorFileWrapper, open_item from .helpers import Error, IntegrityError, set_ec from .platform import uid2user, user2uid, gid2group, group2gid, get_birthtime_ns -from .helpers import parse_timestamp, archive_ts_now +from .helpers import parse_timestamp, archive_ts_now, CompressionSpec from .helpers import OutputTimestamp, format_timedelta, format_file_size, file_status, FileSize from .helpers import safe_encode, make_path_safe, remove_surrogates, text_to_json, join_cmd, remove_dotdot_prefixes from .helpers import StableDict diff --git a/src/borg/archiver/benchmark_cmd.py b/src/borg/archiver/benchmark_cmd.py index d152ff8342..c5fac73e3a 100644 --- a/src/borg/archiver/benchmark_cmd.py +++ b/src/borg/archiver/benchmark_cmd.py @@ -7,7 +7,7 @@ from ..constants import * # NOQA from ..crypto.key import FlexiKey -from ..helpers import format_file_size +from ..helpers import format_file_size, CompressionSpec from ..helpers import json_print from ..helpers import msgpack from ..helpers import get_reset_ec @@ -302,8 +302,6 @@ def chunkit(ch): else: print(f"{spec:<24} {number_kdf:<10} {dt:.3f}s") - from ..compress import CompressionSpec - if not args.json: print("Compression ====================================================") else: diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 22b7208bfb..1268abf844 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -63,13 +63,13 @@ FilesCacheMode, PathSpec, ChunkerParams, + CompressionSpec, tag_validator, relative_time_marker_validator, parse_file_size, ) from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..helpers.time import timestamp -from ..compress import CompressionSpec from ..helpers.parseformat import partial_format from ..manifest import AI_HUMAN_SORT_KEYS diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index e888ff9753..b5c3bf2c65 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -15,8 +15,7 @@ from ..archive import FilesystemObjectProcessors, MetadataCollector, ChunksProcessor from ..cache import Cache from ..constants import * # NOQA -from ..compress import CompressionSpec -from ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec +from ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec, CompressionSpec from ..helpers import archivename_validator, FilesCacheMode from ..helpers import eval_escapes from ..helpers import timestamp, archive_ts_now diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 4e47f1e5ce..366c20aa0c 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -2,14 +2,13 @@ import textwrap from ..archive import Archive -from ..compress import CompressionSpec from ..constants import * # NOQA from ..helpers import msgpack from ..helpers import sysinfo from ..helpers import bin_to_hex, hex_to_bin, prepare_dump_dict from ..helpers import dash_open from ..helpers import StableDict -from ..helpers import archivename_validator +from ..helpers import archivename_validator, CompressionSpec from ..helpers import CommandError, RTError from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index 5a26e55577..effb6d3d90 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -2,8 +2,7 @@ from ._common import build_matcher from ..archive import ArchiveRecreater from ..constants import * # NOQA -from ..compress import CompressionSpec -from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex +from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex, CompressionSpec from ..helpers import timestamp from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..manifest import Manifest diff --git a/src/borg/archiver/repo_compress_cmd.py b/src/borg/archiver/repo_compress_cmd.py index 769dd8ac19..58f3ea4029 100644 --- a/src/borg/archiver/repo_compress_cmd.py +++ b/src/borg/archiver/repo_compress_cmd.py @@ -2,9 +2,9 @@ from ._common import with_repository, Highlander from ..constants import * # NOQA -from ..compress import CompressionSpec, ObfuscateSize, Auto, COMPRESSOR_TABLE +from ..compress import ObfuscateSize, Auto, COMPRESSOR_TABLE from ..hashindex import ChunkIndex -from ..helpers import sig_int, ProgressIndicatorPercent, Error +from ..helpers import sig_int, ProgressIndicatorPercent, Error, CompressionSpec from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter from ..repository import Repository from ..remote import RemoteRepository diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index 7702c24497..c3718c0efc 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -5,7 +5,6 @@ import tarfile from ..archive import Archive, TarfileObjectProcessors, ChunksProcessor -from ..compress import CompressionSpec from ..constants import * # NOQA from ..helpers import HardLinkManager, IncludePatternNeverMatchedWarning from ..helpers import ProgressIndicatorPercent @@ -13,7 +12,7 @@ from ..helpers import msgpack from ..helpers import create_filter_process from ..helpers import ChunkIteratorFileWrapper -from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams +from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, CompressionSpec from ..helpers import remove_surrogates from ..helpers import timestamp, archive_ts_now from ..helpers import basic_json_data, json_print diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index dab3abc66c..8e868c88a1 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -1,13 +1,12 @@ from ._common import with_repository, with_other_repository, Highlander from ..archive import Archive, cached_hash, DownloadPipeline from ..chunkers import get_chunker -from ..compress import CompressionSpec from ..constants import * # NOQA from ..crypto.key import uses_same_id_hash, uses_same_chunker_secret from ..helpers import Error from ..helpers import location_validator, Location, archivename_validator, comment_validator from ..helpers import format_file_size, bin_to_hex -from ..helpers import ChunkerParams, ChunkIteratorFileWrapper +from ..helpers import ChunkerParams, ChunkIteratorFileWrapper, CompressionSpec from ..helpers.argparsing import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter from ..item import ChunkListEntry from ..manifest import Manifest diff --git a/src/borg/compress.pyi b/src/borg/compress.pyi index c8a271a1c6..d627e6e202 100644 --- a/src/borg/compress.pyi +++ b/src/borg/compress.pyi @@ -2,12 +2,6 @@ from typing import Any, Type, Dict, Tuple def get_compressor(name: str, **kwargs) -> Any: ... -class CompressionSpec: - def __init__(self, spec: str) -> None: ... - @property - def compressor(self) -> Any: ... - inner: CompressionSpec - class Compressor: def __init__(self, name: Any = ..., **kwargs) -> None: ... def compress(self, meta: Dict, data: bytes) -> Tuple[Dict, bytes]: ... diff --git a/src/borg/compress.pyx b/src/borg/compress.pyx index 733828720c..faf3031273 100644 --- a/src/borg/compress.pyx +++ b/src/borg/compress.pyx @@ -614,66 +614,3 @@ class Compressor: return cls, (255 if cls.name == 'zlib_legacy' else level) else: raise ValueError('No decompressor for this data found: %r.', data[:2]) - -class CompressionSpec: - def __init__(self, s): - if isinstance(s, CompressionSpec): - self.__dict__.update(s.__dict__) - return - values = s.split(',') - count = len(values) - if count < 1: - raise ArgumentTypeError("not enough arguments") - # --compression algo[,level] - self.name = values[0] - if self.name in ('none', 'lz4', ): - return - elif self.name in ('zlib', 'lzma', 'zlib_legacy'): # zlib_legacy just for testing - if count < 2: - level = 6 # default compression level in py stdlib - elif count == 2: - level = int(values[1]) - if not 0 <= level <= 9: - raise ArgumentTypeError("level must be >= 0 and <= 9") - else: - raise ArgumentTypeError("too many arguments") - self.level = level - elif self.name in ('zstd', ): - if count < 2: - level = 3 # default compression level in zstd - elif count == 2: - level = int(values[1]) - if not 1 <= level <= 22: - raise ArgumentTypeError("level must be >= 1 and <= 22") - else: - raise ArgumentTypeError("too many arguments") - self.level = level - elif self.name == 'auto': - if 2 <= count <= 3: - compression = ','.join(values[1:]) - else: - raise ArgumentTypeError("bad arguments") - self.inner = CompressionSpec(compression) - elif self.name == 'obfuscate': - if 3 <= count <= 5: - level = int(values[1]) - if not ((1 <= level <= 6) or (110 <= level <= 123) or (level == 250)): - raise ArgumentTypeError("level must be (inclusively) within 1...6, 110...123 or equal to 250") - self.level = level - compression = ','.join(values[2:]) - else: - raise ArgumentTypeError("bad arguments") - self.inner = CompressionSpec(compression) - else: - raise ArgumentTypeError("unsupported compression type") - - @property - def compressor(self): - if self.name in ('none', 'lz4', ): - return get_compressor(self.name) - elif self.name in ('zlib', 'lzma', 'zstd', 'zlib_legacy'): - return get_compressor(self.name, level=self.level) - elif self.name == 'auto': - return get_compressor(self.name, compressor=self.inner.compressor) - elif self.name == 'obfuscate': - return get_compressor(self.name, level=self.level, compressor=self.inner.compressor) diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index 7902d5bb67..d9e90f5ce9 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -32,6 +32,7 @@ PathSpec, FilesystemPathSpec, SortBySpec, + CompressionSpec, ChunkerParams, FilesCacheMode, partial_format, diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index 844cd3da49..c843269416 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -165,6 +165,72 @@ def interval(s): return seconds +class CompressionSpec: + def __init__(self, s): + if isinstance(s, CompressionSpec): + self.__dict__.update(s.__dict__) + return + values = s.split(",") + count = len(values) + if count < 1: + raise ArgumentTypeError("not enough arguments") + # --compression algo[,level] + self.name = values[0] + if self.name in ("none", "lz4"): + return + elif self.name in ("zlib", "lzma", "zlib_legacy"): # zlib_legacy just for testing + if count < 2: + level = 6 # default compression level in py stdlib + elif count == 2: + level = int(values[1]) + if not 0 <= level <= 9: + raise ArgumentTypeError("level must be >= 0 and <= 9") + else: + raise ArgumentTypeError("too many arguments") + self.level = level + elif self.name in ("zstd",): + if count < 2: + level = 3 # default compression level in zstd + elif count == 2: + level = int(values[1]) + if not 1 <= level <= 22: + raise ArgumentTypeError("level must be >= 1 and <= 22") + else: + raise ArgumentTypeError("too many arguments") + self.level = level + elif self.name == "auto": + if 2 <= count <= 3: + compression = ",".join(values[1:]) + else: + raise ArgumentTypeError("bad arguments") + self.inner = CompressionSpec(compression) + elif self.name == "obfuscate": + if 3 <= count <= 5: + level = int(values[1]) + if not ((1 <= level <= 6) or (110 <= level <= 123) or (level == 250)): + raise ArgumentTypeError("level must be (inclusively) within 1...6, 110...123 or equal to 250") + self.level = level + compression = ",".join(values[2:]) + else: + raise ArgumentTypeError("bad arguments") + self.inner = CompressionSpec(compression) + else: + raise ArgumentTypeError("unsupported compression type") + + @property + def compressor(self): + from ..compress import get_compressor + + if self.name in ("none", "lz4"): + return get_compressor(self.name) + elif self.name in ("zlib", "lzma", "zstd", "zlib_legacy"): + return get_compressor(self.name, level=self.level) + elif self.name == "auto": + return get_compressor(self.name, compressor=self.inner.compressor) + elif self.name == "obfuscate": + return get_compressor(self.name, level=self.level, compressor=self.inner.compressor) + + def ChunkerParams(s): if isinstance(s, tuple): return s diff --git a/src/borg/testsuite/compress_test.py b/src/borg/testsuite/compress_test.py index 7dbc7574d9..62ef59f51b 100644 --- a/src/borg/testsuite/compress_test.py +++ b/src/borg/testsuite/compress_test.py @@ -3,7 +3,8 @@ import pytest -from ..compress import get_compressor, Compressor, CompressionSpec, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto +from ..compress import get_compressor, Compressor, CNONE, ZLIB, LZ4, LZMA, ZSTD, Auto +from ..helpers import CompressionSpec from ..constants import ROBJ_FILE_STREAM, ROBJ_ARCHIVE_META from ..helpers.argparsing import ArgumentTypeError From e91f4efab1d0cf439c247ad759c14480df85620f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 26 Feb 2026 01:49:23 +0100 Subject: [PATCH 14/27] make build_usage / build_man: do not show options with SUPPRESS --- scripts/make.py | 52 ++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/scripts/make.py b/scripts/make.py index a8877f1c4a..3b71b01c6e 100644 --- a/scripts/make.py +++ b/scripts/make.py @@ -9,6 +9,7 @@ from collections import OrderedDict from datetime import datetime, timezone import time +import argparse # do not change to jsonargparse, shall not require 3rd party pkgs def format_metavar(option): @@ -100,17 +101,18 @@ def generate_level(self, prefix, parser, Archiver, extra_choices=None): return is_subcommand def write_usage(self, parser, fp): - if any(len(o.option_strings) for o in parser._actions): + actions = [o for o in parser._actions if getattr(o, "help", None) != argparse.SUPPRESS] + if any(len(o.option_strings) for o in actions): fp.write(" [options]") - for option in parser._actions: + for option in actions: if option.option_strings: continue fp.write(" " + format_metavar(option)) fp.write("\n\n") def write_options(self, parser, fp): - def is_positional_group(group): - return any(not o.option_strings for o in group._group_actions) + def is_positional_group(actions): + return any(not o.option_strings for o in actions) # HTML output: # A table using some column-spans @@ -121,17 +123,18 @@ def is_positional_group(group): # (no of columns used, columns, ...) rows.append((1, ".. class:: borg-common-opt-ref\n\n:ref:`common_options`")) else: - if not group._group_actions: + actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS] + if not actions: continue group_header = "**%s**" % group.title if group.description: group_header += " — " + group.description rows.append((1, group_header)) - if is_positional_group(group): - for option in group._group_actions: + if is_positional_group(actions): + for option in actions: rows.append((3, "", "``%s``" % option.metavar, option.help or "")) else: - for option in group._group_actions: + for option in actions: if option.metavar: option_fmt = "``%s " + option.metavar + "``" else: @@ -218,18 +221,19 @@ def write_row_separator(): ) def write_options_group(self, group, fp, with_title=True, base_indent=4): - def is_positional_group(group): - return any(not o.option_strings for o in group._group_actions) + def is_positional_group(actions): + return any(not o.option_strings for o in actions) indent = " " * base_indent + actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS] - if is_positional_group(group): - for option in group._group_actions: + if is_positional_group(actions): + for option in actions: fp.write(option.metavar + "\n") fp.write(textwrap.indent(option.help or "", " " * base_indent) + "\n") return - if not group._group_actions: + if not actions: return if with_title: @@ -238,7 +242,7 @@ def is_positional_group(group): opts = OrderedDict() - for option in group._group_actions: + for option in actions: if option.metavar: option_fmt = "%s " + option.metavar else: @@ -503,34 +507,38 @@ def ref_role(name, rawtext, text, lineno, inliner, options={}, content=[]): fd.write(man_page) def write_usage(self, write, parser): - if any(len(o.option_strings) for o in parser._actions): + actions = [o for o in parser._actions if getattr(o, "help", None) != argparse.SUPPRESS] + if any(len(o.option_strings) for o in actions): write(" [options] ", end="") - for option in parser._actions: + for option in actions: if option.option_strings: continue write(format_metavar(option), end=" ") def write_options(self, write, parser): for group in parser._action_groups: - if group.title == "Common options" or not group._group_actions: + actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS] + if group.title == "Common options" or not actions: continue title = "arguments" if group.title == "positional arguments" else group.title self.write_heading(write, title, "+") self.write_options_group(write, group) def write_options_group(self, write, group): - def is_positional_group(group): - return any(not o.option_strings for o in group._group_actions) + def is_positional_group(actions): + return any(not o.option_strings for o in actions) - if is_positional_group(group): - for option in group._group_actions: + actions = [o for o in group._group_actions if getattr(o, "help", None) != argparse.SUPPRESS] + + if is_positional_group(actions): + for option in actions: write(option.metavar) write(textwrap.indent(option.help or "", " " * 4)) return opts = OrderedDict() - for option in group._group_actions: + for option in actions: if option.metavar: option_fmt = "%s " + option.metavar else: From ee58cb4e44172b453af0c3d3272ace0a01a15f13 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 26 Feb 2026 09:15:11 +0100 Subject: [PATCH 15/27] reorg imports --- src/borg/archiver/completion_cmd.py | 5 ++--- src/borg/helpers/argparsing.py | 5 +++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 1268abf844..152e3ae932 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -52,9 +52,6 @@ import shtab -from jsonargparse._actions import _ActionSubCommands -from jsonargparse._completions import prepare_actions_context, shtab_prepare_actions, bash_compgen_typehint - from ._common import process_epilog from ..constants import * # NOQA from ..helpers import ( @@ -69,6 +66,8 @@ parse_file_size, ) from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import _ActionSubCommands +from ..helpers.argparsing import prepare_actions_context, shtab_prepare_actions, bash_compgen_typehint from ..helpers.time import timestamp from ..helpers.parseformat import partial_format from ..manifest import AI_HUMAN_SORT_KEYS diff --git a/src/borg/helpers/argparsing.py b/src/borg/helpers/argparsing.py index a726a6d8e1..67d2026a26 100644 --- a/src/borg/helpers/argparsing.py +++ b/src/borg/helpers/argparsing.py @@ -5,6 +5,11 @@ from argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter # noqa: F401 from jsonargparse import ArgumentParser, Namespace, SUPPRESS, REMAINDER # noqa: F401 +# borg completion uses these private symbols, so we need to import them: +from jsonargparse._actions import _ActionSubCommands # noqa: F401 +from jsonargparse._completions import prepare_actions_context, shtab_prepare_actions # noqa: F401 +from jsonargparse._completions import bash_compgen_typehint # noqa: F401 + def flatten_namespace(ns: Any) -> Namespace: """ From 71212d2d7f147d7f96a46c028903f93fbcb7248f Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Thu, 26 Feb 2026 09:30:20 +0100 Subject: [PATCH 16/27] add octal_int validator --- src/borg/archiver/_common.py | 4 ++-- src/borg/archiver/create_cmd.py | 4 ++-- src/borg/helpers/__init__.py | 2 +- src/borg/helpers/parseformat.py | 6 ++++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/borg/archiver/_common.py b/src/borg/archiver/_common.py index ebba27ae55..0d058e9b0b 100644 --- a/src/borg/archiver/_common.py +++ b/src/borg/archiver/_common.py @@ -8,7 +8,7 @@ from ..cache import Cache, assert_secure from ..helpers import Error from ..helpers import SortBySpec, positive_int_validator, location_validator, Location, relative_time_marker_validator -from ..helpers import Highlander +from ..helpers import Highlander, octal_int from ..helpers.argparsing import SUPPRESS from ..helpers.nanorst import rst_to_terminal from ..manifest import Manifest, AI_HUMAN_SORT_KEYS @@ -510,7 +510,7 @@ def define_common_options(add_common_option): "--umask", metavar="M", dest="umask", - type=lambda s: s if isinstance(s, int) else int(s, 8), + type=octal_int, default=UMASK_DEFAULT, action=Highlander, help="set umask to M (local only, default: %(default)04o)", diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index b5c3bf2c65..266bcefb78 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -16,7 +16,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec, CompressionSpec -from ..helpers import archivename_validator, FilesCacheMode +from ..helpers import archivename_validator, FilesCacheMode, octal_int from ..helpers import eval_escapes from ..helpers import timestamp, archive_ts_now from ..helpers import get_cache_dir, os_stat, get_strip_prefix, slashify @@ -827,7 +827,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): "--stdin-mode", metavar="M", dest="stdin_mode", - type=lambda s: s if isinstance(s, int) else int(s, 8), + type=octal_int, default=STDIN_MODE_DEFAULT, action=Highlander, help="set mode to M in archive for stdin data (default: %(default)04o)", diff --git a/src/borg/helpers/__init__.py b/src/borg/helpers/__init__.py index d9e90f5ce9..2e2c30709b 100644 --- a/src/borg/helpers/__init__.py +++ b/src/borg/helpers/__init__.py @@ -25,7 +25,7 @@ from .fs import HardLinkManager from .misc import sysinfo, log_multi, consume from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper -from .parseformat import bin_to_hex, hex_to_bin, safe_encode, safe_decode +from .parseformat import octal_int, bin_to_hex, hex_to_bin, safe_encode, safe_decode from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval from .parseformat import ( diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index c843269416..f372827e46 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -35,6 +35,12 @@ from ..item import ItemDiff +def octal_int(s): + if isinstance(s, int): + return s + return int(s, 8) + + def bin_to_hex(binary): return binascii.hexlify(binary).decode("ascii") From fac47c6785b789584ae2f330371372605803d875 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 27 Feb 2026 12:23:59 +0100 Subject: [PATCH 17/27] archiver: replace CommonOptions suffix hack with jsonargparse-native approach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old code worked around argparse's flat namespace by appending _maincommand / _midcommand / _subcommand suffixes to every common option's dest (e.g. log_level_subcommand), then resolving them with CommonOptions.resolve() after parsing. This polluted config key names and env var names (BORG_LOG_LEVEL_SUBCOMMAND instead of BORG_LOG_LEVEL). jsonargparse nests subcommand arguments automatically, so the workaround is no longer needed. Each parser level now registers common options with their clean dest name. flatten_namespace() is updated to a two-pass depth-first walk so the most-specific (innermost) value wins naturally: borg --info create --debug → log_level = "debug" (subcommand wins) borg --info create → log_level = "info" (top-level fills gap) For append-action options (--debug-topic) values from all levels are merged (outer + inner) to preserve the accumulation behaviour. --- src/borg/archiver/__init__.py | 120 ++++-------------- src/borg/helpers/argparsing.py | 24 +++- .../testsuite/archiver/argparsing_test.py | 9 +- 3 files changed, 47 insertions(+), 106 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 7415b4f94b..18a0363b08 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -39,7 +39,7 @@ from ..helpers import format_file_size from ..helpers import remove_surrogates, text_to_json from ..helpers import DatetimeWrapper, replace_placeholders - from ..helpers.argparsing import flatten_namespace, ArgumentTypeError, ArgumentParser, Namespace, SUPPRESS + from ..helpers.argparsing import flatten_namespace, ArgumentTypeError, ArgumentParser, SUPPRESS from ..helpers import is_slow_msgpack, is_supported_msgpack, sysinfo from ..helpers import signal_handler, raising_signal_handler, SigHup, SigTerm from ..helpers import ErrorIgnoringTextIOWrapper @@ -178,62 +178,45 @@ def preprocess_args(self, args): class CommonOptions: """ - Support class to allow specifying common options directly after the top-level command. + Support class to allow specifying common options at multiple levels of the command hierarchy. - Normally options can only be specified on the parser defining them, which means - that generally speaking *all* options go after all sub-commands. This is annoying - for common options in scripts, e.g. --remote-path or logging options. + Common options (e.g. --log-level, --repo) can be placed anywhere in the command line: - This class allows adding the same set of options to both the top-level parser - and the final sub-command parsers (but not intermediary sub-commands, at least for now). + borg --info create ... # before the subcommand + borg create --info ... # after the subcommand + borg --info debug info --debug # at both levels of a two-level command - It does so by giving every option's target name ("dest") a suffix indicating its level - -- no two options in the parser hierarchy can have the same target -- - then, after parsing the command line, multiple definitions are resolved. + Each parser level registers the same options with the same dest names. + Defaults are only provided on the top-level parser; all sub-parsers use SUPPRESS so + that unset options don't appear in the namespace at all. - Defaults are handled by only setting them on the top-level parser and setting - a sentinel object in all sub-parsers, which then allows one to discern which parser - supplied the option. + flatten_namespace() handles precedence: it walks sub-namespaces depth-first, so the + most-specific (innermost) value wins. For append-action options (e.g. --debug-topic) + it merges lists from all levels. """ - def __init__(self, define_common_options, suffix_precedence): + def __init__(self, define_common_options): """ *define_common_options* should be a callable taking one argument, which - will be a argparse.Parser.add_argument-like function. + will be an argparse.Parser.add_argument-like function. *define_common_options* will be called multiple times, and should call the passed function to define common options exactly the same way each time. - - *suffix_precedence* should be a tuple of the suffixes that will be used. - It is ordered from lowest precedence to highest precedence: - An option specified on the parser belonging to index 0 is overridden if the - same option is specified on any parser with a higher index. """ self.define_common_options = define_common_options - self.suffix_precedence = suffix_precedence - - # Maps suffixes to sets of target names. - # E.g. common_options["_subcommand"] = {..., "log_level", ...} - self.common_options = dict() - # Set of options with the 'append' action. - self.append_options = set() # This is the sentinel object that replaces all default values in parsers # below the top-level parser. self.default_sentinel = object() - def add_common_group(self, parser, suffix, provide_defaults=False): + def add_common_group(self, parser, provide_defaults=False): """ Add common options to *parser*. - *provide_defaults* must only be True exactly once in a parser hierarchy, - at the top level, and False on all lower levels. The default is chosen - accordingly. - - *suffix* indicates the suffix to use internally. It also indicates - which precedence the *parser* has for common options. See *suffix_precedence* - of __init__. + *provide_defaults* must be True exactly once in a parser hierarchy (the top-level + parser) and False on all sub-parsers. Sub-parsers get SUPPRESS as the default so + that an unspecified option produces no attribute, leaving the top-level default intact + after flatten_namespace() merges the namespaces. """ - assert suffix in self.suffix_precedence def add_argument(*args, **kwargs): if "dest" in kwargs: @@ -248,20 +231,9 @@ def add_argument(*args, **kwargs): "append", ) is_append = kwargs["action"] == "append" - if is_append: - self.append_options.add(kwargs["dest"]) - assert ( - kwargs["default"] == [] - ), "The default is explicitly constructed as an empty list in resolve()" - else: - self.common_options.setdefault(suffix, set()).add(kwargs["dest"]) - kwargs["dest"] += suffix if not provide_defaults: - # Interpolate help now, in case the %(default)d (or so) is mentioned, + # Interpolate help now, in case %(default)d (or similar) is mentioned, # to avoid producing incorrect help output. - # Assumption: Interpolated output can safely be interpolated again, - # which should always be the case. - # Note: We control all inputs. kwargs["help"] = kwargs["help"] % kwargs if not is_append: kwargs["default"] = SUPPRESS @@ -271,66 +243,23 @@ def add_argument(*args, **kwargs): common_group = parser.add_argument_group("Common options") self.define_common_options(add_argument) - def resolve(self, args: Namespace): # Namespace has "in" but otherwise is not like a dict. - """ - Resolve the multiple definitions of each common option to the final value. - """ - for suffix in self.suffix_precedence: - # From highest level to lowest level, so the "most-specific" option wins, e.g. - # "borg --debug create --info" shall result in --info being effective. - for dest in self.common_options.get(suffix, []): - # map_from is this suffix' option name, e.g. log_level_subcommand - # map_to is the target name, e.g. log_level - map_from = dest + suffix - map_to = dest - # Retrieve value; depending on the action it may not exist, but usually does - # (store_const/store_true/store_false), either because the action implied a default - # or a default is explicitly supplied. - # Note that defaults on lower levels are replaced with default_sentinel. - # Only the top level has defaults. - value = getattr(args, map_from, self.default_sentinel) - if value is not self.default_sentinel: - # value was indeed specified on this level. Transfer value to target, - # and un-clobber the args (for tidiness - you *cannot* use the suffixed - # names for other purposes, obviously). - setattr(args, map_to, value) - try: - delattr(args, map_from) - except AttributeError: - pass - - # Options with an "append" action need some special treatment. Instead of - # overriding values, all specified values are merged together. - for dest in self.append_options: - option_value = [] - for suffix in self.suffix_precedence: - # Find values of this suffix, if any, and add them to the final list - extend_from = dest + suffix - if extend_from in args: - values = getattr(args, extend_from) - delattr(args, extend_from) - option_value.extend(values) - setattr(args, dest, option_value) - def build_parser(self): from ._common import define_common_options parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False) # paths and patterns must have an empty list as default everywhere - parser.common_options = self.CommonOptions( - define_common_options, suffix_precedence=("_maincommand", "_midcommand", "_subcommand") - ) + parser.common_options = self.CommonOptions(define_common_options) parser.add_argument( "-V", "--version", action="version", version="%(prog)s " + __version__, help="show version number and exit" ) parser.add_argument("--cockpit", dest="cockpit", action="store_true", help="Start the Borg TUI") - parser.common_options.add_common_group(parser, "_maincommand", provide_defaults=True) + parser.common_options.add_common_group(parser, provide_defaults=True) common_parser = ArgumentParser(add_help=False, prog=self.prog) - parser.common_options.add_common_group(common_parser, "_subcommand") + parser.common_options.add_common_group(common_parser) mid_common_parser = ArgumentParser(add_help=False, prog=self.prog) - parser.common_options.add_common_group(mid_common_parser, "_midcommand") + parser.common_options.add_common_group(mid_common_parser) if parser.prog == "borgfs": return self.build_parser_borgfs(parser) @@ -415,7 +344,6 @@ def parse_args(self, args=None): if getattr(args, list_attr, None) is None: setattr(args, list_attr, []) - parser.common_options.resolve(args) func = self.get_func(args, parser) if func == self.do_create and args.paths and args.paths_from_stdin: parser.error("Must not pass PATH with --paths-from-stdin.") diff --git a/src/borg/helpers/argparsing.py b/src/borg/helpers/argparsing.py index 67d2026a26..27ffe91628 100644 --- a/src/borg/helpers/argparsing.py +++ b/src/borg/helpers/argparsing.py @@ -16,6 +16,10 @@ def flatten_namespace(ns: Any) -> Namespace: Recursively flattens a nested namespace into a single-level namespace. JSONArgparse uses nested namespaces for subcommands, whereas borg's internal dispatch and logic expect a flat namespace. + + Inner (subcommand) values take precedence over outer (top-level) values. + For list-typed values (append-action options like --debug-topic) that appear + at multiple levels, the lists are merged: outer values first, inner values last. """ flat = Namespace() @@ -30,15 +34,27 @@ def flatten_namespace(ns: Any) -> Namespace: flat.subcommand = " ".join(subcmds) def _flatten(source, target): - items = ( + items = list( vars(source).items() if hasattr(source, "__dict__") else source.items() if hasattr(source, "items") else [] ) + # First pass: recurse into sub-namespaces so inner (subcommand) values are set first. for k, v in items: if isinstance(v, Namespace) or type(v).__name__ == "Namespace": _flatten(v, target) - else: - if k != "subcommand" and not hasattr(target, k): - setattr(target, k, v) + # Second pass: apply this level's plain values. + # - If not yet set: set it (inner already won via the first pass). + # - If already set and both are lists: merge outer + inner (for append-action options). + for k, v in items: + if isinstance(v, Namespace) or type(v).__name__ == "Namespace": + continue + if k == "subcommand": + continue + existing = getattr(target, k, None) + if existing is None: + setattr(target, k, v) + elif isinstance(existing, list) and isinstance(v, list): + # Append-action options (e.g. --debug-topic): outer values come first. + setattr(target, k, list(v) + list(existing)) _flatten(ns, flat) return flat diff --git a/src/borg/testsuite/archiver/argparsing_test.py b/src/borg/testsuite/archiver/argparsing_test.py index dbaa306406..0f6244c490 100644 --- a/src/borg/testsuite/archiver/argparsing_test.py +++ b/src/borg/testsuite/archiver/argparsing_test.py @@ -94,9 +94,7 @@ def define_common_options(add_common_option): @pytest.fixture def basic_parser(self): parser = ArgumentParser(prog="test", description="test parser", add_help=False) - parser.common_options = Archiver.CommonOptions( - self.define_common_options, suffix_precedence=("_level0", "_level1") - ) + parser.common_options = Archiver.CommonOptions(self.define_common_options) return parser @pytest.fixture @@ -105,13 +103,13 @@ def subcommands(self, basic_parser): @pytest.fixture def parser(self, basic_parser): - basic_parser.common_options.add_common_group(basic_parser, "_level0", provide_defaults=True) + basic_parser.common_options.add_common_group(basic_parser, provide_defaults=True) return basic_parser @pytest.fixture def common_parser(self, parser): common_parser = ArgumentParser(add_help=False, prog="test") - parser.common_options.add_common_group(common_parser, "_level1") + parser.common_options.add_common_group(common_parser) return common_parser @pytest.fixture @@ -130,7 +128,6 @@ def parse_vars_from_line(*line): print(line) args = parser.parse_args(line) args = flatten_namespace(args) - parser.common_options.resolve(args) return vars(args) return parse_vars_from_line From 975212bbec5731d1281a0d7f3998ac3f32ce6e35 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 27 Feb 2026 12:43:49 +0100 Subject: [PATCH 18/27] simplify flatten_namespace, add docstring for argparsing --- src/borg/helpers/argparsing.py | 145 ++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 28 deletions(-) diff --git a/src/borg/helpers/argparsing.py b/src/borg/helpers/argparsing.py index 27ffe91628..ace4dfa295 100644 --- a/src/borg/helpers/argparsing.py +++ b/src/borg/helpers/argparsing.py @@ -1,3 +1,99 @@ +""" +Borg argument-parsing layer +=========================== + +All imports of ``ArgumentParser``, ``Namespace``, ``SUPPRESS``, etc. come +from this module. It is the single seam between borg and the underlying +parser library (jsonargparse). + +Library choice +-------------- +Borg uses **jsonargparse** instead of plain argparse. jsonargparse is a +superset of argparse that additionally supports: + +* reading arguments from YAML/JSON config files (``--config``) +* reading arguments from environment variables +* nested namespaces for subcommands (each subcommand's arguments live in + their own ``Namespace`` object rather than the flat top-level namespace) + +Parser hierarchy +---------------- +Borg's command line has up to three levels:: + + borg [common-opts] [common-opts] [ [common-opts] [args]] + + e.g. borg --info create ... + borg create --info ... + borg debug info --debug ... + +Three ``ArgumentParser`` instances are constructed in ``build_parser()``: + +``parser`` (top-level) + The root parser. Common options are registered here **with real + defaults** (``provide_defaults=True``). + +``common_parser`` + A helper parser (``add_help=False``) passed as ``parents=[common_parser]`` + to every *leaf* subcommand parser (e.g. ``create``, ``repo-create``, …). + Common options are registered here **with** ``default=SUPPRESS`` so that + an option not given on the command line leaves no attribute at all in the + subcommand namespace. + +``mid_common_parser`` + Same as ``common_parser`` but used as the parent for *group* subcommand + parsers that introduce a second level (e.g. ``debug``, ``key``, + ``benchmark``). Their *leaf* subcommand parsers also use + ``mid_common_parser`` as a parent. + +Common options (``--info``, ``--debug``, ``--repo``, ``--lock-wait``, …) +are managed by ``Archiver.CommonOptions``, which calls +``define_common_options()`` once per parser so the same options appear at +every level with identical ``dest`` names. + +Namespace flattening and precedence +------------------------------------- +jsonargparse stores each subcommand's parsed values in a nested +``Namespace`` object:: + + # borg --info create --debug ... + Namespace( + log_level = "info", # top-level + subcommand = "create", + create = Namespace( + log_level = "debug", # subcommand level + ... + ) + ) + +After ``parser.parse_args()`` returns, ``flatten_namespace()`` collapses +this tree into a single ``Namespace`` that borg's dispatch and command +implementations expect. + +Precedence rule: the **most-specific** (innermost) value wins. +``flatten_namespace`` uses ``Namespace.as_flat()`` (provided by jsonargparse) +to linearise the nested tree into a flat dict with dotted keys encoding +depth, for example:: + + log_level = "info" # top-level (0 dots) + create.log_level = "debug" # one level deep (1 dot) + debug.info.log_level = "critical" # two levels deep (2 dots) + +The entries are then sorted deepest-first so the most-specific value is +encountered first and wins. Shallower values only fill in if the key +has not been set yet. + +Special case — append-action options (e.g. ``--debug-topic``): +If a key already holds a list and the outer level also supplies a list, +the two lists are **merged** (outer values first, inner values last) so +that ``borg --debug-topic foo create --debug-topic bar`` accumulates +``["foo", "bar"]`` rather than losing one of the values. + +The ``SUPPRESS`` default on sub-parsers is essential: if a common option +is not given at the subcommand level, it simply produces no attribute in +the subcommand namespace and the outer (top-level) default flows through +unchanged. +""" + from typing import Any # here are the only imports from argparse and jsonargparse, @@ -13,9 +109,8 @@ def flatten_namespace(ns: Any) -> Namespace: """ - Recursively flattens a nested namespace into a single-level namespace. - JSONArgparse uses nested namespaces for subcommands, whereas borg's - internal dispatch and logic expect a flat namespace. + Flattens the nested namespace jsonargparse produces for subcommands into a + single-level namespace that borg's dispatch and command implementations expect. Inner (subcommand) values take precedence over outer (top-level) values. For list-typed values (append-action options like --debug-topic) that appear @@ -23,7 +118,7 @@ def flatten_namespace(ns: Any) -> Namespace: """ flat = Namespace() - # Extract the nested path of subcommands + # Extract the joined subcommand path from the nested namespace tree. subcmds = [] current = ns while current and hasattr(current, "subcommand") and current.subcommand: @@ -33,28 +128,22 @@ def flatten_namespace(ns: Any) -> Namespace: if subcmds: flat.subcommand = " ".join(subcmds) - def _flatten(source, target): - items = list( - vars(source).items() if hasattr(source, "__dict__") else source.items() if hasattr(source, "items") else [] - ) - # First pass: recurse into sub-namespaces so inner (subcommand) values are set first. - for k, v in items: - if isinstance(v, Namespace) or type(v).__name__ == "Namespace": - _flatten(v, target) - # Second pass: apply this level's plain values. - # - If not yet set: set it (inner already won via the first pass). - # - If already set and both are lists: merge outer + inner (for append-action options). - for k, v in items: - if isinstance(v, Namespace) or type(v).__name__ == "Namespace": - continue - if k == "subcommand": - continue - existing = getattr(target, k, None) - if existing is None: - setattr(target, k, v) - elif isinstance(existing, list) and isinstance(v, list): - # Append-action options (e.g. --debug-topic): outer values come first. - setattr(target, k, list(v) + list(existing)) - - _flatten(ns, flat) + # as_flat() linearises the nested tree into dotted-key entries, e.g.: + # log_level='info' (outer, 0 dots) + # create.log_level='debug' (subcommand, 1 dot) + # debug.info.log_level='crit' (two-level subcommand, 2 dots) + # Sorting deepest-first ensures the most-specific value is processed first and therefore wins ("inner wins" rule). + all_items = sorted(vars(ns.as_flat()).items(), key=lambda kv: kv[0].count("."), reverse=True) + + for dotted_key, value in all_items: + dest = dotted_key.rsplit(".", 1)[-1] # e.g. "create.log_level" -> "log_level" + if dest == "subcommand": + continue + existing = getattr(flat, dest, None) + if existing is None: + setattr(flat, dest, value) + elif isinstance(existing, list) and isinstance(value, list): + # Append-action options (e.g. --debug-topic): outer values come first. + setattr(flat, dest, list(value) + list(existing)) + return flat From 2f778b974bcb16f2f612686a70834bfd48f66e32 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 27 Feb 2026 13:54:56 +0100 Subject: [PATCH 19/27] argparsing: add ArgumentParser subclass with borg's usual defaults Borg's ArgumentParser (in borg.helpers.argparsing) now subclasses jsonargparse's ArgumentParser and pre-sets two defaults that every borg parser uses: formatter_class = RawDescriptionHelpFormatter add_help = False --- src/borg/archiver/__init__.py | 6 +- src/borg/archiver/analyze_cmd.py | 10 +--- src/borg/archiver/benchmark_cmd.py | 20 ++----- src/borg/archiver/check_cmd.py | 10 +--- src/borg/archiver/compact_cmd.py | 10 +--- src/borg/archiver/completion_cmd.py | 8 +-- src/borg/archiver/create_cmd.py | 10 +--- src/borg/archiver/debug_cmd.py | 58 +++---------------- src/borg/archiver/delete_cmd.py | 10 +--- src/borg/archiver/diff_cmd.py | 10 +--- src/borg/archiver/extract_cmd.py | 10 +--- src/borg/archiver/help_cmd.py | 2 +- src/borg/archiver/info_cmd.py | 10 +--- src/borg/archiver/key_cmds.py | 32 ++-------- src/borg/archiver/list_cmd.py | 10 +--- src/borg/archiver/lock_cmds.py | 14 +---- src/borg/archiver/mount_cmds.py | 19 +----- src/borg/archiver/prune_cmd.py | 10 +--- src/borg/archiver/recreate_cmd.py | 8 +-- src/borg/archiver/rename_cmd.py | 10 +--- src/borg/archiver/repo_compress_cmd.py | 8 +-- src/borg/archiver/repo_create_cmd.py | 8 +-- src/borg/archiver/repo_delete_cmd.py | 8 +-- src/borg/archiver/repo_info_cmd.py | 8 +-- src/borg/archiver/repo_list_cmd.py | 8 +-- src/borg/archiver/repo_space_cmd.py | 8 +-- src/borg/archiver/serve_cmd.py | 10 +--- src/borg/archiver/tag_cmd.py | 10 +--- src/borg/archiver/tar_cmds.py | 14 +---- src/borg/archiver/transfer_cmd.py | 8 +-- src/borg/archiver/undelete_cmd.py | 8 +-- src/borg/archiver/version_cmd.py | 10 +--- src/borg/helpers/argparsing.py | 9 ++- .../testsuite/archiver/argparsing_test.py | 14 ++--- 34 files changed, 91 insertions(+), 317 deletions(-) diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index 18a0363b08..de2437bf93 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -246,7 +246,7 @@ def add_argument(*args, **kwargs): def build_parser(self): from ._common import define_common_options - parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups", add_help=False) + parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups") # paths and patterns must have an empty list as default everywhere parser.common_options = self.CommonOptions(define_common_options) parser.add_argument( @@ -255,10 +255,10 @@ def build_parser(self): parser.add_argument("--cockpit", dest="cockpit", action="store_true", help="Start the Borg TUI") parser.common_options.add_common_group(parser, provide_defaults=True) - common_parser = ArgumentParser(add_help=False, prog=self.prog) + common_parser = ArgumentParser(prog=self.prog) parser.common_options.add_common_group(common_parser) - mid_common_parser = ArgumentParser(add_help=False, prog=self.prog) + mid_common_parser = ArgumentParser(prog=self.prog) parser.common_options.add_common_group(mid_common_parser) if parser.prog == "borgfs": diff --git a/src/borg/archiver/analyze_cmd.py b/src/borg/archiver/analyze_cmd.py index 662b3b03bd..3db076aaa1 100644 --- a/src/borg/archiver/analyze_cmd.py +++ b/src/borg/archiver/analyze_cmd.py @@ -6,7 +6,7 @@ from ..constants import * # NOQA from ..helpers import bin_to_hex, Error from ..helpers import ProgressIndicatorPercent -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..remote import RemoteRepository from ..repository import Repository @@ -126,12 +126,6 @@ def build_parser_analyze(self, subparsers, common_parser, mid_common_parser): to recreate existing archives without them. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_analyze.__doc__, - epilog=analyze_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_analyze.__doc__, epilog=analyze_epilog) subparsers.add_subcommand("analyze", subparser, help="analyze archives") define_archive_filters_group(subparser) diff --git a/src/borg/archiver/benchmark_cmd.py b/src/borg/archiver/benchmark_cmd.py index c5fac73e3a..1903909b84 100644 --- a/src/borg/archiver/benchmark_cmd.py +++ b/src/borg/archiver/benchmark_cmd.py @@ -11,7 +11,7 @@ from ..helpers import json_print from ..helpers import msgpack from ..helpers import get_reset_ec -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..item import Item from ..platform import SyncFile @@ -353,11 +353,7 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): benchmark_epilog = process_epilog("These commands do various benchmarks.") subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description="benchmark command", - epilog=benchmark_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description="benchmark command", epilog=benchmark_epilog ) subparsers.add_subcommand("benchmark", subparser, help="benchmark command") @@ -405,11 +401,7 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_benchmark_crud.__doc__, - epilog=bench_crud_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_benchmark_crud.__doc__, epilog=bench_crud_epilog ) benchmark_parsers.add_subcommand( "crud", subparser, help="benchmarks Borg CRUD (create, extract, update, delete)." @@ -430,11 +422,7 @@ def build_parser_benchmarks(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_benchmark_cpu.__doc__, - epilog=bench_cpu_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_benchmark_cpu.__doc__, epilog=bench_cpu_epilog ) benchmark_parsers.add_subcommand("cpu", subparser, help="benchmarks Borg CPU-bound operations.") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/check_cmd.py b/src/borg/archiver/check_cmd.py index 0889e4ce02..83d1f6e294 100644 --- a/src/borg/archiver/check_cmd.py +++ b/src/borg/archiver/check_cmd.py @@ -3,7 +3,7 @@ from ..constants import * # NOQA from ..helpers import set_ec, EXIT_WARNING, CancelledByUser, CommandError, IntegrityError from ..helpers import yes -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..logger import create_logger @@ -182,13 +182,7 @@ def build_parser_check(self, subparsers, common_parser, mid_common_parser): ``borg compact`` would remove the archives' data completely. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_check.__doc__, - epilog=check_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_check.__doc__, epilog=check_epilog) subparsers.add_subcommand("check", subparser, help="verify the repository") subparser.add_argument( "--repository-only", dest="repo_only", action="store_true", help="only perform repository checks" diff --git a/src/borg/archiver/compact_cmd.py b/src/borg/archiver/compact_cmd.py index 4efdcd01b2..c25bb03e17 100644 --- a/src/borg/archiver/compact_cmd.py +++ b/src/borg/archiver/compact_cmd.py @@ -5,7 +5,7 @@ from ..cache import write_chunkindex_to_repo_cache, build_chunkindex_from_repo from ..cache import files_cache_name, discover_files_cache_names from ..helpers import get_cache_dir -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..constants import * # NOQA from ..hashindex import ChunkIndex, ChunkIndexEntry from ..helpers import set_ec, EXIT_ERROR, format_file_size, bin_to_hex @@ -257,13 +257,7 @@ def build_parser_compact(self, subparsers, common_parser, mid_common_parser): thus it cannot compute before/after compaction size statistics). """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_compact.__doc__, - epilog=compact_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_compact.__doc__, epilog=compact_epilog) subparsers.add_subcommand("compact", subparser, help="compact the repository") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py index 152e3ae932..2f6a46eb77 100644 --- a/src/borg/archiver/completion_cmd.py +++ b/src/borg/archiver/completion_cmd.py @@ -65,7 +65,7 @@ relative_time_marker_validator, parse_file_size, ) -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..helpers.argparsing import _ActionSubCommands from ..helpers.argparsing import prepare_actions_context, shtab_prepare_actions, bash_compgen_typehint from ..helpers.time import timestamp @@ -762,11 +762,7 @@ def build_parser_completion(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_completion.__doc__, - epilog=completion_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_completion.__doc__, epilog=completion_epilog ) subparsers.add_subcommand("completion", subparser, help="output shell completion script") subparser.add_argument( diff --git a/src/borg/archiver/create_cmd.py b/src/borg/archiver/create_cmd.py index 266bcefb78..6114193fd4 100644 --- a/src/borg/archiver/create_cmd.py +++ b/src/borg/archiver/create_cmd.py @@ -29,7 +29,7 @@ from ..helpers import iter_separated from ..helpers import MakePathSafeAction from ..helpers import Error, CommandError, BackupWarning, FileChangedWarning -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..patterns import PatternMatcher from ..platform import is_win32 @@ -770,13 +770,7 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser): """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_create.__doc__, - epilog=create_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_create.__doc__, epilog=create_epilog) subparsers.add_subcommand("create", subparser, help="create a backup") # note: --dry-run and --stats are mutually exclusive, but we do not want to abort when diff --git a/src/borg/archiver/debug_cmd.py b/src/borg/archiver/debug_cmd.py index 366c20aa0c..723d413af5 100644 --- a/src/borg/archiver/debug_cmd.py +++ b/src/borg/archiver/debug_cmd.py @@ -10,7 +10,7 @@ from ..helpers import StableDict from ..helpers import archivename_validator, CompressionSpec from ..helpers import CommandError, RTError -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..platform import get_process_id from ..repository import Repository, LIST_SCAN_LIMIT, repo_lister @@ -319,10 +319,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description="debugging command (not intended for normal use)", epilog=debug_epilog, - formatter_class=RawDescriptionHelpFormatter, ) subparsers.add_subcommand("debug", subparser, help="debugging command (not intended for normal use)") @@ -336,11 +334,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_info.__doc__, - epilog=debug_info_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_info.__doc__, epilog=debug_info_epilog ) debug_parsers.add_subcommand("info", subparser, help="show system infos for debugging / bug reports (debug)") @@ -351,10 +345,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description=self.do_debug_dump_archive_items.__doc__, epilog=debug_dump_archive_items_epilog, - formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-archive-items", subparser, help="dump archive items (metadata) (debug)") subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") @@ -366,10 +358,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description=self.do_debug_dump_archive.__doc__, epilog=debug_dump_archive_epilog, - formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-archive", subparser, help="dump decoded archive metadata (debug)") subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name") @@ -382,10 +372,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description=self.do_debug_dump_manifest.__doc__, epilog=debug_dump_manifest_epilog, - formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-manifest", subparser, help="dump decoded repository metadata (debug)") subparser.add_argument("path", metavar="PATH", type=str, help="file to dump data into") @@ -397,10 +385,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description=self.do_debug_dump_repo_objs.__doc__, epilog=debug_dump_repo_objs_epilog, - formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("dump-repo-objs", subparser, help="dump repo objects (debug)") @@ -411,10 +397,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description=self.do_debug_search_repo_objs.__doc__, epilog=debug_search_repo_objs_epilog, - formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand("search-repo-objs", subparser, help="search repo objects (debug)") subparser.add_argument( @@ -430,11 +414,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_id_hash.__doc__, - epilog=debug_id_hash_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_id_hash.__doc__, epilog=debug_id_hash_epilog ) debug_parsers.add_subcommand("id-hash", subparser, help="compute id-hash for some file content (debug)") subparser.add_argument( @@ -448,11 +428,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_parse_obj.__doc__, - epilog=debug_parse_obj_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_parse_obj.__doc__, epilog=debug_parse_obj_epilog ) debug_parsers.add_subcommand("parse-obj", subparser, help="parse borg object file into meta dict and data") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") @@ -473,11 +449,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_format_obj.__doc__, - epilog=debug_format_obj_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_format_obj.__doc__, epilog=debug_format_obj_epilog ) debug_parsers.add_subcommand("format-obj", subparser, help="format file and metadata into a Borg object file") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") @@ -510,11 +482,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_get_obj.__doc__, - epilog=debug_get_obj_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_get_obj.__doc__, epilog=debug_get_obj_epilog ) debug_parsers.add_subcommand("get-obj", subparser, help="get object from repository (debug)") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to get from the repo") @@ -526,11 +494,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_put_obj.__doc__, - epilog=debug_put_obj_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_put_obj.__doc__, epilog=debug_put_obj_epilog ) debug_parsers.add_subcommand("put-obj", subparser, help="put object to repository (debug)") subparser.add_argument("id", metavar="ID", type=str, help="hex object ID to put into the repo") @@ -542,11 +506,7 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description=self.do_debug_delete_obj.__doc__, - epilog=debug_delete_obj_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description=self.do_debug_delete_obj.__doc__, epilog=debug_delete_obj_epilog ) debug_parsers.add_subcommand("delete-obj", subparser, help="delete object from repository (debug)") subparser.add_argument( @@ -560,10 +520,8 @@ def build_parser_debug(self, subparsers, common_parser, mid_common_parser): ) subparser = ArgumentParser( parents=[mid_common_parser], - add_help=False, description=self.do_debug_convert_profile.__doc__, epilog=debug_convert_profile_epilog, - formatter_class=RawDescriptionHelpFormatter, ) debug_parsers.add_subcommand( "convert-profile", subparser, help="convert Borg profile to Python profile (debug)" diff --git a/src/borg/archiver/delete_cmd.py b/src/borg/archiver/delete_cmd.py index e7cfea17fc..985abb475c 100644 --- a/src/borg/archiver/delete_cmd.py +++ b/src/borg/archiver/delete_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -80,13 +80,7 @@ def build_parser_delete(self, subparsers, common_parser, mid_common_parser): patterns, see :ref:`borg_patterns`). """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_delete.__doc__, - epilog=delete_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_delete.__doc__, epilog=delete_epilog) subparsers.add_subcommand("delete", subparser, help="delete archives") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" diff --git a/src/borg/archiver/diff_cmd.py b/src/borg/archiver/diff_cmd.py index 3430461d0c..22d4243758 100644 --- a/src/borg/archiver/diff_cmd.py +++ b/src/borg/archiver/diff_cmd.py @@ -8,7 +8,7 @@ from ..constants import * # NOQA from ..helpers import BaseFormatter, DiffFormatter, archivename_validator, PathSpec, BorgJsonEncoder from ..helpers import IncludePatternNeverMatchedWarning, remove_surrogates -from ..helpers.argparsing import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, ArgumentTypeError from ..item import ItemDiff from ..manifest import Manifest from ..logger import create_logger @@ -293,13 +293,7 @@ def diff_sort_spec_validator(s): raise ArgumentTypeError(f"unsupported sort field: {field}") return ",".join(parts) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_diff.__doc__, - epilog=diff_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_diff.__doc__, epilog=diff_epilog) subparsers.add_subcommand("diff", subparser, help="find differences in archive contents") subparser.add_argument( "--numeric-ids", diff --git a/src/borg/archiver/extract_cmd.py b/src/borg/archiver/extract_cmd.py index b61585a0b3..eaa6c40491 100644 --- a/src/borg/archiver/extract_cmd.py +++ b/src/borg/archiver/extract_cmd.py @@ -11,7 +11,7 @@ from ..helpers import HardLinkManager from ..helpers import ProgressIndicatorPercent from ..helpers import BackupWarning, IncludePatternNeverMatchedWarning -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -155,13 +155,7 @@ def build_parser_extract(self, subparsers, common_parser, mid_common_parser): group, permissions, etc. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_extract.__doc__, - epilog=extract_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_extract.__doc__, epilog=extract_epilog) subparsers.add_subcommand("extract", subparser, help="extract archive contents") subparser.add_argument( "--list", dest="output_list", action="store_true", help="output a verbose list of items (files, dirs, ...)" diff --git a/src/borg/archiver/help_cmd.py b/src/borg/archiver/help_cmd.py index 7b486847f7..d0a240b4b3 100644 --- a/src/borg/archiver/help_cmd.py +++ b/src/borg/archiver/help_cmd.py @@ -553,7 +553,7 @@ def do_subcommand_help(self, parser, args): do_maincommand_help = do_subcommand_help def build_parser_help(self, subparsers, common_parser, mid_common_parser, parser): - subparser = ArgumentParser(parents=[common_parser], add_help=False, description="Extra help") + subparser = ArgumentParser(parents=[common_parser], description="Extra help") subparsers.add_subcommand("help", subparser, help="Extra help") subparser.add_argument("--epilog-only", dest="epilog_only", action="store_true") subparser.add_argument("--usage-only", dest="usage_only", action="store_true") diff --git a/src/borg/archiver/info_cmd.py b/src/borg/archiver/info_cmd.py index eaab7531b8..d88493a420 100644 --- a/src/borg/archiver/info_cmd.py +++ b/src/borg/archiver/info_cmd.py @@ -5,7 +5,7 @@ from ..archive import Archive from ..constants import * # NOQA from ..helpers import format_timedelta, json_print, basic_json_data, archivename_validator -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -78,13 +78,7 @@ def build_parser_info(self, subparsers, common_parser, mid_common_parser): = all chunks in the repository. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_info.__doc__, - epilog=info_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_info.__doc__, epilog=info_epilog) subparsers.add_subcommand("info", subparser, help="show repository or archive information") subparser.add_argument("--json", action="store_true", help="format output as JSON") define_archive_filters_group(subparser) diff --git a/src/borg/archiver/key_cmds.py b/src/borg/archiver/key_cmds.py index 4f3b620e9e..5bdfc0b5dc 100644 --- a/src/borg/archiver/key_cmds.py +++ b/src/borg/archiver/key_cmds.py @@ -5,7 +5,7 @@ from ..crypto.key import AESOCBKeyfileKey, CHPOKeyfileKey, Blake2AESOCBKeyfileKey, Blake2CHPOKeyfileKey from ..crypto.keymanager import KeyManager from ..helpers import PathSpec, CommandError -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ._common import with_repository @@ -120,11 +120,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): from ._common import process_epilog subparser = ArgumentParser( - parents=[mid_common_parser], - add_help=False, - description="Manage the keyfile or repokey of a repository", - epilog="", - formatter_class=RawDescriptionHelpFormatter, + parents=[mid_common_parser], description="Manage the keyfile or repokey of a repository", epilog="" ) subparsers.add_subcommand("key", subparser, help="manage the repository key") @@ -162,11 +158,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_key_export.__doc__, - epilog=key_export_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_key_export.__doc__, epilog=key_export_epilog ) key_parsers.add_subcommand("export", subparser, help="export the repository key for backup") subparser.add_argument("path", metavar="PATH", nargs="?", type=PathSpec, help="where to store the backup") @@ -202,11 +194,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_key_import.__doc__, - epilog=key_import_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_key_import.__doc__, epilog=key_import_epilog ) key_parsers.add_subcommand("import", subparser, help="import the repository key from backup") subparser.add_argument( @@ -231,11 +219,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_key_change_passphrase.__doc__, - epilog=change_passphrase_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_key_change_passphrase.__doc__, epilog=change_passphrase_epilog ) key_parsers.add_subcommand("change-passphrase", subparser, help="change the repository passphrase") @@ -253,11 +237,7 @@ def build_parser_keys(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_key_change_location.__doc__, - epilog=change_location_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_key_change_location.__doc__, epilog=change_location_epilog ) key_parsers.add_subcommand("change-location", subparser, help="change the key location") subparser.add_argument( diff --git a/src/borg/archiver/list_cmd.py b/src/borg/archiver/list_cmd.py index 0099831fde..98abd18c3e 100644 --- a/src/borg/archiver/list_cmd.py +++ b/src/borg/archiver/list_cmd.py @@ -7,7 +7,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..helpers import ItemFormatter, BaseFormatter, archivename_validator, PathSpec -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -102,13 +102,7 @@ def build_parser_list(self, subparsers, common_parser, mid_common_parser): ) + ItemFormatter.keys_help() ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_list.__doc__, - epilog=list_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_list.__doc__, epilog=list_epilog) subparsers.add_subcommand("list", subparser, help="list archive contents") subparser.add_argument( "--short", dest="short", action="store_true", help="only print file/directory names, nothing else" diff --git a/src/borg/archiver/lock_cmds.py b/src/borg/archiver/lock_cmds.py index 4a8bcae051..971fb653e8 100644 --- a/src/borg/archiver/lock_cmds.py +++ b/src/borg/archiver/lock_cmds.py @@ -4,7 +4,7 @@ from ..cache import Cache from ..constants import * # NOQA from ..helpers import prepare_subprocess_env, set_ec, CommandError, ThreadRunner -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter, REMAINDER +from ..helpers.argparsing import ArgumentParser, REMAINDER from ..logger import create_logger @@ -46,11 +46,7 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_break_lock.__doc__, - epilog=break_lock_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_break_lock.__doc__, epilog=break_lock_epilog ) subparsers.add_subcommand("break-lock", subparser, help="break the repository and cache locks") @@ -76,11 +72,7 @@ def build_parser_locks(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_with_lock.__doc__, - epilog=with_lock_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_with_lock.__doc__, epilog=with_lock_epilog ) subparsers.add_subcommand("with-lock", subparser, help="run a user command with the lock held") subparser.add_argument("command", metavar="COMMAND", help="command to run") diff --git a/src/borg/archiver/mount_cmds.py b/src/borg/archiver/mount_cmds.py index 4fba328d39..6111df006c 100644 --- a/src/borg/archiver/mount_cmds.py +++ b/src/borg/archiver/mount_cmds.py @@ -5,7 +5,7 @@ from ..helpers import RTError from ..helpers import PathSpec from ..helpers import umount -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..remote import cache_if_remote @@ -151,13 +151,7 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser the logger to output to a file. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_mount.__doc__, - epilog=mount_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_mount.__doc__, epilog=mount_epilog) subparsers.add_subcommand("mount", subparser, help="mount a repository") self._define_borg_mount(subparser) @@ -169,13 +163,7 @@ def build_parser_mount_umount(self, subparsers, common_parser, mid_common_parser command - usually this is either umount or fusermount -u. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_umount.__doc__, - epilog=umount_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_umount.__doc__, epilog=umount_epilog) subparsers.add_subcommand("umount", subparser, help="unmount a repository") subparser.add_argument( "mountpoint", metavar="MOUNTPOINT", type=str, help="mountpoint of the filesystem to unmount" @@ -185,7 +173,6 @@ def build_parser_borgfs(self, parser): assert parser.prog == "borgfs" parser.description = self.do_mount.__doc__ parser.epilog = "For more information, see borg mount --help." - parser.formatter_class = RawDescriptionHelpFormatter parser.help = "mount a repository" self._define_borg_mount(parser) return parser diff --git a/src/borg/archiver/prune_cmd.py b/src/borg/archiver/prune_cmd.py index 4498981cb1..19b8426c97 100644 --- a/src/borg/archiver/prune_cmd.py +++ b/src/borg/archiver/prune_cmd.py @@ -8,7 +8,7 @@ from ..constants import * # NOQA from ..helpers import ArchiveFormatter, interval, sig_int, ProgressIndicatorPercent, CommandError, Error from ..helpers import archivename_validator -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -273,13 +273,7 @@ def build_parser_prune(self, subparsers, common_parser, mid_common_parser): the ``borg repo-list`` description for more details about the format string). """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_prune.__doc__, - epilog=prune_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_prune.__doc__, epilog=prune_epilog) subparsers.add_subcommand("prune", subparser, help="prune archives") subparser.add_argument( "-n", "--dry-run", dest="dry_run", action="store_true", help="do not change the repository" diff --git a/src/borg/archiver/recreate_cmd.py b/src/borg/archiver/recreate_cmd.py index effb6d3d90..49b7b9cfac 100644 --- a/src/borg/archiver/recreate_cmd.py +++ b/src/borg/archiver/recreate_cmd.py @@ -4,7 +4,7 @@ from ..constants import * # NOQA from ..helpers import archivename_validator, comment_validator, PathSpec, ChunkerParams, bin_to_hex, CompressionSpec from ..helpers import timestamp -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -101,11 +101,7 @@ def build_parser_recreate(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_recreate.__doc__, - epilog=recreate_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_recreate.__doc__, epilog=recreate_epilog ) subparsers.add_subcommand("recreate", subparser, help=self.do_recreate.__doc__) subparser.add_argument( diff --git a/src/borg/archiver/rename_cmd.py b/src/borg/archiver/rename_cmd.py index 13fe9fa510..b1d4829616 100644 --- a/src/borg/archiver/rename_cmd.py +++ b/src/borg/archiver/rename_cmd.py @@ -1,7 +1,7 @@ from ._common import with_repository, with_archive from ..constants import * # NOQA from ..helpers import archivename_validator -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -27,13 +27,7 @@ def build_parser_rename(self, subparsers, common_parser, mid_common_parser): This results in a different archive ID. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_rename.__doc__, - epilog=rename_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_rename.__doc__, epilog=rename_epilog) subparsers.add_subcommand("rename", subparser, help="rename an archive") subparser.add_argument( "name", metavar="OLDNAME", type=archivename_validator, help="specify the current archive name" diff --git a/src/borg/archiver/repo_compress_cmd.py b/src/borg/archiver/repo_compress_cmd.py index 58f3ea4029..8fb26e2f7e 100644 --- a/src/borg/archiver/repo_compress_cmd.py +++ b/src/borg/archiver/repo_compress_cmd.py @@ -5,7 +5,7 @@ from ..compress import ObfuscateSize, Auto, COMPRESSOR_TABLE from ..hashindex import ChunkIndex from ..helpers import sig_int, ProgressIndicatorPercent, Error, CompressionSpec -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..repository import Repository from ..remote import RemoteRepository from ..manifest import Manifest @@ -181,11 +181,7 @@ def build_parser_repo_compress(self, subparsers, common_parser, mid_common_parse """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_repo_compress.__doc__, - epilog=repo_compress_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_repo_compress.__doc__, epilog=repo_compress_epilog ) subparsers.add_subcommand("repo-compress", subparser, help=self.do_repo_compress.__doc__) diff --git a/src/borg/archiver/repo_create_cmd.py b/src/borg/archiver/repo_create_cmd.py index 3deb404b16..05235ce9d6 100644 --- a/src/borg/archiver/repo_create_cmd.py +++ b/src/borg/archiver/repo_create_cmd.py @@ -4,7 +4,7 @@ from ..crypto.key import key_creator, key_argument_names from ..helpers import CancelledByUser from ..helpers import location_validator, Location -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -190,11 +190,7 @@ def build_parser_repo_create(self, subparsers, common_parser, mid_common_parser) """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_repo_create.__doc__, - epilog=repo_create_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_repo_create.__doc__, epilog=repo_create_epilog ) subparsers.add_subcommand("repo-create", subparser, help="create a new, empty repository") subparser.add_argument( diff --git a/src/borg/archiver/repo_delete_cmd.py b/src/borg/archiver/repo_delete_cmd.py index 8d8470c841..d9774ee350 100644 --- a/src/borg/archiver/repo_delete_cmd.py +++ b/src/borg/archiver/repo_delete_cmd.py @@ -5,7 +5,7 @@ from ..helpers import format_archive from ..helpers import bin_to_hex from ..helpers import yes -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest, NoManifestError from ..logger import create_logger @@ -102,11 +102,7 @@ def build_parser_repo_delete(self, subparsers, common_parser, mid_common_parser) """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_repo_delete.__doc__, - epilog=repo_delete_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_repo_delete.__doc__, epilog=repo_delete_epilog ) subparsers.add_subcommand("repo-delete", subparser, help="delete a repository") subparser.add_argument( diff --git a/src/borg/archiver/repo_info_cmd.py b/src/borg/archiver/repo_info_cmd.py index 419337fa2f..d8feeaf8c3 100644 --- a/src/borg/archiver/repo_info_cmd.py +++ b/src/borg/archiver/repo_info_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA from ..helpers import bin_to_hex, json_print, basic_json_data -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -64,11 +64,7 @@ def build_parser_repo_info(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_repo_info.__doc__, - epilog=repo_info_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_repo_info.__doc__, epilog=repo_info_epilog ) subparsers.add_subcommand("repo-info", subparser, help="show repository information") subparser.add_argument("--json", action="store_true", help="format output as JSON") diff --git a/src/borg/archiver/repo_list_cmd.py b/src/borg/archiver/repo_list_cmd.py index fa93a2d5bf..5ae41ea958 100644 --- a/src/borg/archiver/repo_list_cmd.py +++ b/src/borg/archiver/repo_list_cmd.py @@ -5,7 +5,7 @@ from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import BaseFormatter, ArchiveFormatter, json_print, basic_json_data -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -85,11 +85,7 @@ def build_parser_repo_list(self, subparsers, common_parser, mid_common_parser): + ArchiveFormatter.keys_help() ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_repo_list.__doc__, - epilog=repo_list_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_repo_list.__doc__, epilog=repo_list_epilog ) subparsers.add_subcommand("repo-list", subparser, help="list repository contents") subparser.add_argument( diff --git a/src/borg/archiver/repo_space_cmd.py b/src/borg/archiver/repo_space_cmd.py index 3a2e326555..37ed12d884 100644 --- a/src/borg/archiver/repo_space_cmd.py +++ b/src/borg/archiver/repo_space_cmd.py @@ -6,7 +6,7 @@ from ._common import with_repository, Highlander from ..constants import * # NOQA from ..helpers import parse_file_size, format_file_size -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..logger import create_logger @@ -86,11 +86,7 @@ def build_parser_repo_space(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_repo_space.__doc__, - epilog=repo_space_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_repo_space.__doc__, epilog=repo_space_epilog ) subparsers.add_subcommand("repo-space", subparser, help="manage reserved space in a repository") subparser.add_argument( diff --git a/src/borg/archiver/serve_cmd.py b/src/borg/archiver/serve_cmd.py index 744eedb603..36661758e3 100644 --- a/src/borg/archiver/serve_cmd.py +++ b/src/borg/archiver/serve_cmd.py @@ -2,7 +2,7 @@ from ..remote import RepositoryServer from ..logger import create_logger -from ..helpers.argparsing import RawDescriptionHelpFormatter, ArgumentParser +from ..helpers.argparsing import ArgumentParser logger = create_logger() @@ -51,13 +51,7 @@ def build_parser_serve(self, subparsers, common_parser, mid_common_parser): Existing archives can be read, but no archives can be created or deleted. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_serve.__doc__, - epilog=serve_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_serve.__doc__, epilog=serve_epilog) subparsers.add_subcommand("serve", subparser, help="start the repository server process") subparser.add_argument( "--restrict-to-path", diff --git a/src/borg/archiver/tag_cmd.py b/src/borg/archiver/tag_cmd.py index d842e65a27..f57b89f34e 100644 --- a/src/borg/archiver/tag_cmd.py +++ b/src/borg/archiver/tag_cmd.py @@ -2,7 +2,7 @@ from ..archive import Archive from ..constants import * # NOQA from ..helpers import bin_to_hex, archivename_validator, tag_validator, Error -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -79,13 +79,7 @@ def build_parser_tag(self, subparsers, common_parser, mid_common_parser): removed). """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_tag.__doc__, - epilog=tag_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_tag.__doc__, epilog=tag_epilog) subparsers.add_subcommand("tag", subparser, help="tag archives") subparser.add_argument("--set", dest="set_tags", metavar="TAG", type=tag_validator, nargs="+", help="set tags") subparser.add_argument("--add", dest="add_tags", metavar="TAG", type=tag_validator, nargs="+", help="add tags") diff --git a/src/borg/archiver/tar_cmds.py b/src/borg/archiver/tar_cmds.py index c3718c0efc..bb3ac41e3c 100644 --- a/src/borg/archiver/tar_cmds.py +++ b/src/borg/archiver/tar_cmds.py @@ -17,7 +17,7 @@ from ..helpers import timestamp, archive_ts_now from ..helpers import basic_json_data, json_print from ..helpers import log_multi -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ._common import with_repository, with_archive, Highlander, define_exclusion_group @@ -384,11 +384,7 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_export_tar.__doc__, - epilog=export_tar_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_export_tar.__doc__, epilog=export_tar_epilog ) subparsers.add_subcommand("export-tar", subparser, help="create tarball from archive") subparser.add_argument( @@ -458,11 +454,7 @@ def build_parser_tar(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_import_tar.__doc__, - epilog=import_tar_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_import_tar.__doc__, epilog=import_tar_epilog ) subparsers.add_subcommand("import-tar", subparser, help=self.do_import_tar.__doc__) subparser.add_argument( diff --git a/src/borg/archiver/transfer_cmd.py b/src/borg/archiver/transfer_cmd.py index 8e868c88a1..71088f8ded 100644 --- a/src/borg/archiver/transfer_cmd.py +++ b/src/borg/archiver/transfer_cmd.py @@ -7,7 +7,7 @@ from ..helpers import location_validator, Location, archivename_validator, comment_validator from ..helpers import format_file_size, bin_to_hex from ..helpers import ChunkerParams, ChunkIteratorFileWrapper, CompressionSpec -from ..helpers.argparsing import ArgumentParser, ArgumentTypeError, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser, ArgumentTypeError from ..item import ChunkListEntry from ..manifest import Manifest from ..legacyrepository import LegacyRepository @@ -330,11 +330,7 @@ def build_parser_transfer(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_transfer.__doc__, - epilog=transfer_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_transfer.__doc__, epilog=transfer_epilog ) subparsers.add_subcommand("transfer", subparser, help="transfer of archives from another repository") subparser.add_argument( diff --git a/src/borg/archiver/undelete_cmd.py b/src/borg/archiver/undelete_cmd.py index 7e4720b541..8a37e6fb5f 100644 --- a/src/borg/archiver/undelete_cmd.py +++ b/src/borg/archiver/undelete_cmd.py @@ -3,7 +3,7 @@ from ._common import with_repository from ..constants import * # NOQA from ..helpers import format_archive, CommandError, bin_to_hex, archivename_validator -from ..helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter +from ..helpers.argparsing import ArgumentParser from ..manifest import Manifest from ..logger import create_logger @@ -73,11 +73,7 @@ def build_parser_undelete(self, subparsers, common_parser, mid_common_parser): """ ) subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_undelete.__doc__, - epilog=undelete_epilog, - formatter_class=RawDescriptionHelpFormatter, + parents=[common_parser], description=self.do_undelete.__doc__, epilog=undelete_epilog ) subparsers.add_subcommand("undelete", subparser, help="undelete archives") subparser.add_argument( diff --git a/src/borg/archiver/version_cmd.py b/src/borg/archiver/version_cmd.py index 30081e1a4d..409baaeb78 100644 --- a/src/borg/archiver/version_cmd.py +++ b/src/borg/archiver/version_cmd.py @@ -1,6 +1,6 @@ from .. import __version__ from ..constants import * # NOQA -from ..helpers.argparsing import RawDescriptionHelpFormatter, ArgumentParser +from ..helpers.argparsing import ArgumentParser from ..remote import RemoteRepository from ..logger import create_logger @@ -50,11 +50,5 @@ def build_parser_version(self, subparsers, common_parser, mid_common_parser): You can also use ``borg --version`` to display a potentially more precise client version. """ ) - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description=self.do_version.__doc__, - epilog=version_epilog, - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description=self.do_version.__doc__, epilog=version_epilog) subparsers.add_subcommand("version", subparser, help="display the Borg client and server versions") diff --git a/src/borg/helpers/argparsing.py b/src/borg/helpers/argparsing.py index ace4dfa295..4d68af0f25 100644 --- a/src/borg/helpers/argparsing.py +++ b/src/borg/helpers/argparsing.py @@ -99,7 +99,8 @@ # here are the only imports from argparse and jsonargparse, # all other imports of these names import them from here: from argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter # noqa: F401 -from jsonargparse import ArgumentParser, Namespace, SUPPRESS, REMAINDER # noqa: F401 +from jsonargparse import ArgumentParser as _ArgumentParser # we subclass that to add custom behavior +from jsonargparse import Namespace, SUPPRESS, REMAINDER # noqa: F401 # borg completion uses these private symbols, so we need to import them: from jsonargparse._actions import _ActionSubCommands # noqa: F401 @@ -107,6 +108,12 @@ from jsonargparse._completions import bash_compgen_typehint # noqa: F401 +class ArgumentParser(_ArgumentParser): + # the borg code always uses RawDescriptionHelpFormatter and add_help=False: + def __init__(self, *args, formatter_class=RawDescriptionHelpFormatter, add_help=False, **kwargs): + super().__init__(*args, formatter_class=formatter_class, add_help=add_help, **kwargs) + + def flatten_namespace(ns: Any) -> Namespace: """ Flattens the nested namespace jsonargparse produces for subcommands into a diff --git a/src/borg/testsuite/archiver/argparsing_test.py b/src/borg/testsuite/archiver/argparsing_test.py index 0f6244c490..1819d9ca73 100644 --- a/src/borg/testsuite/archiver/argparsing_test.py +++ b/src/borg/testsuite/archiver/argparsing_test.py @@ -1,7 +1,7 @@ import pytest from . import Archiver, RK_ENCRYPTION, cmd -from ...helpers.argparsing import ArgumentParser, RawDescriptionHelpFormatter, flatten_namespace +from ...helpers.argparsing import ArgumentParser, flatten_namespace def test_bad_filters(archiver): @@ -93,7 +93,7 @@ def define_common_options(add_common_option): @pytest.fixture def basic_parser(self): - parser = ArgumentParser(prog="test", description="test parser", add_help=False) + parser = ArgumentParser(prog="test", description="test parser") parser.common_options = Archiver.CommonOptions(self.define_common_options) return parser @@ -108,19 +108,13 @@ def parser(self, basic_parser): @pytest.fixture def common_parser(self, parser): - common_parser = ArgumentParser(add_help=False, prog="test") + common_parser = ArgumentParser(prog="test") parser.common_options.add_common_group(common_parser) return common_parser @pytest.fixture def parse_vars_from_line(self, parser, subcommands, common_parser): - subparser = ArgumentParser( - parents=[common_parser], - add_help=False, - description="foo", - epilog="bar", - formatter_class=RawDescriptionHelpFormatter, - ) + subparser = ArgumentParser(parents=[common_parser], description="foo", epilog="bar") subparser.add_argument("--foo-bar", dest="foo_bar", action="store_true") subcommands.add_subcommand("subcmd", subparser, help="baz") From 63a45c6c21c09d0ce93d7cf8d6bcf8e818da7359 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 27 Feb 2026 14:52:20 +0100 Subject: [PATCH 20/27] add support for yaml config files, default config --- docs/usage/general/config.rst.inc | 58 +++++++++++++++++++++++++++++ docs/usage/usage_general.rst.inc | 4 ++ pyproject.toml | 3 +- requirements.d/development.lock.txt | 1 + requirements.d/development.txt | 1 + src/borg/archiver/__init__.py | 8 +++- src/borg/helpers/argparsing.py | 1 + src/borg/helpers/parseformat.py | 32 ++++++++++++++-- 8 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 docs/usage/general/config.rst.inc diff --git a/docs/usage/general/config.rst.inc b/docs/usage/general/config.rst.inc new file mode 100644 index 0000000000..178b3c3bc9 --- /dev/null +++ b/docs/usage/general/config.rst.inc @@ -0,0 +1,58 @@ +Configuration files +~~~~~~~~~~~~~~~~~~~ + +Borg supports reading options from YAML configuration files. This is +implemented via `jsonargparse `_ +and works for all options that can also be set on the command line. + +Default configuration file + ``$BORG_CONFIG_DIR/default.yaml`` is loaded automatically on every Borg + invocation if it exists. You do not need to pass ``--config`` explicitly + for this file. + +``--config PATH`` + Load additional options from the YAML file at *PATH*. + Options in this file take precedence over the default config file but are + overridden by explicit command-line arguments. This option can be used + multiple times, with later files overriding earlier ones. + +``--print_config`` + Print the current effective configuration (all options in YAML format) to + stdout and exit. This reflects the merged result of the default config + file, any ``--config`` file, environment variables, and command-line + arguments given before ``--print_config``. The output can be used as a + starting point for a config file. + +File format + Config files are YAML documents. Top-level keys are option names + (without leading ``--`` and with ``-`` replaced by ``_``). + Nested keys correspond to subcommands. + + Example ``default.yaml``:: + + # apply to all borg commands: + log_level: info + show_rc: true + + # options specific to "borg create": + create: + compression: zstd,3 + stats: true + + The top-level keys set options that are common to all commands (equivalent + to placing them before the subcommand on the command line). Keys nested + under a subcommand name (e.g. ``create:``) are only applied when that + subcommand is invoked. + +Precedence (lowest to highest) + 1. Default config file (``$BORG_CONFIG_DIR/default.yaml``) + 2. ``--config`` file(s) (in the order given) + 3. Environment variables (e.g. ``BORG_REPO``) + 4. Command-line arguments + +.. note:: + ``--print_config`` shows the merged effective configuration and is a + convenient way to check what values Borg will actually use, and to + generate contents for your borg config file(s):: + + borg --repo /backup/main create --compression zstd,3 --print_config diff --git a/docs/usage/usage_general.rst.inc b/docs/usage/usage_general.rst.inc index 3cd396f051..ec61ef6b5e 100644 --- a/docs/usage/usage_general.rst.inc +++ b/docs/usage/usage_general.rst.inc @@ -10,6 +10,10 @@ .. include:: general/return-codes.rst.inc +.. _config: + +.. include:: general/config.rst.inc + .. _env_vars: .. include:: general/environment.rst.inc diff --git a/pyproject.toml b/pyproject.toml index ad6539a60b..4baa9086e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ "backports-zstd; python_version < '3.14'", # for python < 3.14. "xxhash>=2.0.0", "jsonargparse @ git+https://github.com/omni-us/jsonargparse.git@main", + "PyYAML>=6.0.2", # we need to register our types with yaml, jsonargparse uses yaml for config files ] [project.optional-dependencies] @@ -260,7 +261,7 @@ deps = ["ruff"] commands = [["ruff", "check", "."]] [tool.tox.env.mypy] -deps = ["pytest", "mypy", "pkgconfig"] +deps = ["pytest", "mypy", "pkgconfig", "types-PyYAML"] commands = [["mypy", "--ignore-missing-imports"]] [tool.tox.env.docs] diff --git a/requirements.d/development.lock.txt b/requirements.d/development.lock.txt index e5cd36cd16..cc2ada4ff3 100644 --- a/requirements.d/development.lock.txt +++ b/requirements.d/development.lock.txt @@ -14,3 +14,4 @@ pytest-cov==7.0.0 pytest-benchmark==5.2.3 Cython==3.2.4 pre-commit==4.5.1 +types-PyYAML==6.0.12.20250915 diff --git a/requirements.d/development.txt b/requirements.d/development.txt index 5da779e80b..51317ec4c3 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -15,3 +15,4 @@ pytest-benchmark Cython pre-commit bandit[toml] +types-PyYAML diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py index de2437bf93..f8c6a576bf 100644 --- a/src/borg/archiver/__init__.py +++ b/src/borg/archiver/__init__.py @@ -45,6 +45,7 @@ from ..helpers import ErrorIgnoringTextIOWrapper from ..helpers import msgpack from ..helpers import sig_int + from ..helpers import get_config_dir from ..remote import RemoteRepository from ..selftest import selftest except BaseException: @@ -246,7 +247,12 @@ def add_argument(*args, **kwargs): def build_parser(self): from ._common import define_common_options - parser = ArgumentParser(prog=self.prog, description="Borg - Deduplicated Backups") + parser = ArgumentParser( + prog=self.prog, + description="Borg - Deduplicated Backups", + default_config_files=[os.path.join(get_config_dir(), "default.yaml")], + ) + parser.add_argument("--config", action="config") # paths and patterns must have an empty list as default everywhere parser.common_options = self.CommonOptions(define_common_options) parser.add_argument( diff --git a/src/borg/helpers/argparsing.py b/src/borg/helpers/argparsing.py index 4d68af0f25..69214ca589 100644 --- a/src/borg/helpers/argparsing.py +++ b/src/borg/helpers/argparsing.py @@ -101,6 +101,7 @@ from argparse import Action, ArgumentError, ArgumentTypeError, RawDescriptionHelpFormatter # noqa: F401 from jsonargparse import ArgumentParser as _ArgumentParser # we subclass that to add custom behavior from jsonargparse import Namespace, SUPPRESS, REMAINDER # noqa: F401 +from jsonargparse.typing import register_type # noqa: F401 # borg completion uses these private symbols, so we need to import them: from jsonargparse._actions import _ActionSubCommands # noqa: F401 diff --git a/src/borg/helpers/parseformat.py b/src/borg/helpers/parseformat.py index f372827e46..fbc580c3f0 100644 --- a/src/borg/helpers/parseformat.py +++ b/src/borg/helpers/parseformat.py @@ -21,9 +21,11 @@ logger = create_logger() +import yaml + from .errors import Error from .fs import get_keys_dir, make_path_safe, slashify -from .argparsing import Action, ArgumentError, ArgumentTypeError +from .argparsing import Action, ArgumentError, ArgumentTypeError, register_type from .msgpack import Timestamp from .time import OutputTimestamp, format_time, safe_timestamp from .. import __version__ as borg_version @@ -236,10 +238,22 @@ def compressor(self): elif self.name == "obfuscate": return get_compressor(self.name, level=self.level, compressor=self.inner.compressor) + def __str__(self): + if self.name in ("none", "lz4"): + return f"{self.name}" + elif self.name in ("zlib", "lzma", "zstd", "zlib_legacy"): + return f"{self.name},{self.level}" + elif self.name == "auto": + return f"auto,{self.inner}" + elif self.name == "obfuscate": + return f"obfuscate,{self.level},{self.inner}" + else: + raise ValueError(f"unsupported compression type: {self.name}") + def ChunkerParams(s): - if isinstance(s, tuple): - return s + if isinstance(s, (list, tuple)): + return tuple(s) params = s.strip().split(",") count = len(params) if count == 0: @@ -714,6 +728,18 @@ def validator(text): return validator +# Register types with jsonargparse so they can be represented in config files +# (e.g. for --print_config). Two things are needed: +# 1. A YAML representer so yaml.safe_dump can serialize Location objects to strings. +# 2. A jsonargparse register_type so it knows how to deserialize strings back to Location. + +yaml.SafeDumper.add_representer(Location, lambda dumper, loc: dumper.represent_str(loc.raw or "")) +register_type(Location, serializer=lambda loc: loc.raw or "") + +yaml.SafeDumper.add_representer(CompressionSpec, lambda dumper, cs: dumper.represent_str(str(cs))) +register_type(CompressionSpec) + + def relative_time_marker_validator(text: str): time_marker_regex = r"^\d+[ymwdHMS]$" match = re.compile(time_marker_regex).search(text) From 678df16bad710d3dbf7f50872683b75402eb2056 Mon Sep 17 00:00:00 2001 From: Thomas Waldmann Date: Fri, 27 Feb 2026 19:33:25 +0100 Subject: [PATCH 21/27] add support for auto-generated environment variables (jsonargparse) --- docs/usage/general/environment.rst.inc | 26 ++++++++++++++++++++++++++ src/borg/archiver/__init__.py | 2 ++ 2 files changed, 28 insertions(+) diff --git a/docs/usage/general/environment.rst.inc b/docs/usage/general/environment.rst.inc index 10455252e6..d684012ec3 100644 --- a/docs/usage/general/environment.rst.inc +++ b/docs/usage/general/environment.rst.inc @@ -284,3 +284,29 @@ Please note: .. _INI: https://docs.python.org/3/library/logging.config.html#configuration-file-format .. _tempfile: https://docs.python.org/3/library/tempfile.html#tempfile.gettempdir + + +Automatically generated Environment Variables (jsonargparse) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Borg uses jsonargparse_ with ``default_env=True``, which means that every +command-line option can also be set via an environment variable. + +The environment variable name is derived from the program name (``borg``), +the subcommand (if any), and the option name, all converted to uppercase +with dashes replaced by underscores. + +For **top-level options** (not specific to a subcommand), the pattern is:: + + BORG_