migrate to jsonargparse

This commit is contained in:
Thomas Waldmann 2026-02-24 22:40:22 +01:00
parent e3dcf98775
commit 2cff41d894
No known key found for this signature in database
GPG key ID: 243ACFA951F78E01
37 changed files with 287 additions and 267 deletions

View file

@ -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]

View file

@ -27,6 +27,8 @@ try:
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 @@ try:
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 @@ STATS_HEADER = " Original size Deduplicated size"
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 @@ class Archiver(
# 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 @@ class Archiver(
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 @@ class Archiver(
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="<command>")
subparsers = parser.add_subcommands(required=False, title="required arguments", metavar="<command>")
self.build_parser_analyze(subparsers, common_parser, mid_common_parser)
self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)
@ -424,8 +415,15 @@ class Archiver(
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 @@ class Archiver(
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 @@ class Archiver(
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

View file

@ -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)",

View file

@ -2,6 +2,8 @@ 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
@ -126,14 +128,12 @@ class AnalyzeMixIn:
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)

View file

@ -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 @@ class BenchmarkMixIn:
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="<command>")
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
benchmark_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="<command>")
bench_crud_epilog = process_epilog(
"""
@ -409,16 +408,14 @@ class BenchmarkMixIn:
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 @@ class BenchmarkMixIn:
- 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")

View file

@ -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 @@ class CheckMixIn:
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 @@ class CheckMixIn:
``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"
)

View file

@ -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 @@ class CompactMixIn:
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"
)

View file

@ -54,6 +54,8 @@ import argparse
import shtab
from jsonargparse import ArgumentParser
from ._common import process_epilog
from ..constants import * # NOQA
from ..helpers import (
@ -750,16 +752,14 @@ class CompletionMixIn:
"""
)
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)"
)

View file

@ -9,6 +9,8 @@ import subprocess
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 @@ class CreateMixIn:
"""
)
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 @@ class CreateMixIn:
"--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)",

View file

@ -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 @@ class DebugMixIn:
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="<command>")
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
debug_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="<command>")
debug_info_epilog = process_epilog(
"""
@ -339,32 +338,28 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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 @@ class DebugMixIn:
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")

View file

@ -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 @@ class DeleteMixIn:
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"
)

View file

@ -4,6 +4,8 @@ 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
@ -294,16 +296,14 @@ class DiffMixIn:
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",

View file

@ -3,6 +3,8 @@ 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
@ -155,16 +157,14 @@ class ExtractMixIn:
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, ...)"
)

View file

@ -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 @@ class HelpMixIn:
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")

View file

@ -2,6 +2,8 @@ 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
@ -78,16 +80,14 @@ class InfoMixIn:
= 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(

View file

@ -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 @@ class KeysMixIn:
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="<command>")
subparser.set_defaults(fallback_func=functools.partial(self.do_subcommand_help, subparser))
key_parsers = subparser.add_subcommands(required=False, title="required arguments", metavar="<command>")
key_export_epilog = process_epilog(
"""
@ -164,16 +163,14 @@ class KeysMixIn:
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 @@ class KeysMixIn:
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 @@ class KeysMixIn:
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 @@ class KeysMixIn:
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"
)

View file

@ -3,6 +3,8 @@ 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
@ -103,16 +105,14 @@ class ListMixIn:
)
+ 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"
)

View file

@ -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 @@ class LocksMixIn:
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 @@ class LocksMixIn:
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")

View file

@ -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 @@ class MountMixIn:
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 @@ class MountMixIn:
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 @@ class MountMixIn:
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"

View file

@ -5,6 +5,8 @@ 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
@ -273,16 +275,14 @@ class PruneMixIn:
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"
)

View file

@ -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 @@ class RecreateMixIn:
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, ...)"
)

View file

@ -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 @@ class RenameMixIn:
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"
)

View file

@ -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 @@ class RepoCompressMixIn:
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",

View file

@ -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 @@ class RepoCreateMixIn:
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",

View file

@ -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 @@ class RepoDeleteMixIn:
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"
)

View file

@ -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 @@ class RepoInfoMixIn:
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")

View file

@ -3,6 +3,8 @@ 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
@ -85,16 +87,14 @@ class RepoListMixIn:
)
+ 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"
)

View file

@ -2,6 +2,8 @@ import argparse
import math
import os
from jsonargparse import ArgumentParser
from borgstore.store import ItemInfo
from ._common import with_repository, Highlander
@ -86,16 +88,14 @@ class RepoSpaceMixIn:
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",

View file

@ -19,6 +19,7 @@ class ServeMixIn:
).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 @@ class ServeMixIn:
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",

View file

@ -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 @@ class TagMixIn:
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(

View file

@ -5,6 +5,8 @@ import os
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 @@ class TarMixIn:
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 @@ class TarMixIn:
``--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",

View file

@ -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 @@ class TransferMixIn:
"""
)
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"
)

View file

@ -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 @@ class UnDeleteMixIn:
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"
)

View file

@ -23,6 +23,7 @@ class VersionMixIn:
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 @@ class VersionMixIn:
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")

View file

@ -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

View file

@ -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")

View file

@ -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