Skip to content

Commit 822ab66

Browse files
WIP
1 parent 4cc9ca7 commit 822ab66

File tree

4 files changed

+30
-83
lines changed

4 files changed

+30
-83
lines changed

src/borg/archiver/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,9 @@ def __init__(self, define_common_options, suffix_precedence):
221221
# This is the sentinel object that replaces all default values in parsers
222222
# below the top-level parser.
223223
self.default_sentinel = object()
224+
# Maps dest names to their type functions, for options where type= was
225+
# stripped from sub-parsers to avoid jsonargparse validation of sentinel defaults.
226+
self.type_functions = dict()
224227

225228
def add_common_group(self, parser, suffix, provide_defaults=False):
226229
"""
@@ -238,9 +241,9 @@ def add_common_group(self, parser, suffix, provide_defaults=False):
238241

239242
def add_argument(*args, **kwargs):
240243
if "dest" in kwargs:
241-
kwargs.setdefault("action", "store")
242-
assert kwargs["action"] in ("help", "store_const", "store_true", "store_false", "store", "append")
243-
is_append = kwargs["action"] == "append"
244+
action = kwargs.get("action", "store")
245+
assert action in ("help", "store_const", "store_true", "store_false", "store", "append")
246+
is_append = action == "append"
244247
if is_append:
245248
self.append_options.add(kwargs["dest"])
246249
assert (
@@ -258,6 +261,11 @@ def add_argument(*args, **kwargs):
258261
kwargs["help"] = kwargs["help"] % kwargs
259262
if not is_append:
260263
kwargs["default"] = self.default_sentinel
264+
# Remove type= so jsonargparse won't validate the sentinel default.
265+
# Store the type function for manual conversion in resolve().
266+
type_fn = kwargs.pop("type", None)
267+
if type_fn is not None:
268+
self.type_functions[kwargs["dest"]] = type_fn
261269

262270
common_group.add_argument(*args, **kwargs)
263271

@@ -286,6 +294,10 @@ def resolve(self, args: argparse.Namespace): # Namespace has "in" but otherwise
286294
# value was indeed specified on this level. Transfer value to target,
287295
# and un-clobber the args (for tidiness - you *cannot* use the suffixed
288296
# names for other purposes, obviously).
297+
# Apply type conversion if type= was stripped from this sub-parser.
298+
type_fn = self.type_functions.get(map_from)
299+
if type_fn is not None and isinstance(value, str):
300+
value = type_fn(value)
289301
setattr(args, map_to, value)
290302
try:
291303
delattr(args, map_from)

src/borg/archiver/_common.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
from ..repository import Repository
1818
from ..repoobj import RepoObj, RepoObj1
1919
from ..patterns import (
20+
ArgparseExcludePatternAction,
2021
ArgparsePatternAction,
2122
ArgparseExcludeFileAction,
2223
ArgparsePatternFileAction,
23-
parse_exclude_pattern,
2424
)
2525

2626

@@ -272,8 +272,7 @@ def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components
272272
"--exclude",
273273
metavar="PATTERN",
274274
dest="patterns",
275-
type=parse_exclude_pattern,
276-
action="append",
275+
action=ArgparseExcludePatternAction,
277276
help="exclude paths matching PATTERN",
278277
)
279278
add_option(
@@ -304,7 +303,6 @@ def define_exclude_and_patterns(add_option, *, tag_files=False, strip_components
304303
metavar="NAME",
305304
dest="exclude_if_present",
306305
action="append",
307-
type=str,
308306
help="exclude directories that are tagged by containing a filesystem object with the given NAME",
309307
)
310308
add_option(

src/borg/helpers/jap_wrapper.py

Lines changed: 4 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,7 @@
33
This module provides a compatibility layer between Borg's argparse patterns
44
and jsonargparse's API. Key adaptations:
55
6-
1. type+action combination: jsonargparse forbids combining type= and action=
7-
in add_argument(). Our override strips type= from kwargs and ensures
8-
type conversion happens within the action itself:
9-
- Highlander action class: handles type via _type_fn (pops type in __init__)
10-
- Standard actions (append, store, etc.): wrapped in TypeConvertingAction
11-
- Custom action classes: type is popped and stored on the action after creation
12-
13-
2. Namespace flattening: jsonargparse creates nested namespaces for subcommands
6+
1. Namespace flattening: jsonargparse creates nested namespaces for subcommands
147
(args.create.name instead of args.name). flatten_namespace() merges these
158
into a flat namespace compatible with Borg's command handlers.
169
"""
@@ -21,78 +14,13 @@
2114
from jsonargparse._core import ArgumentGroup as _JAPArgumentGroup
2215

2316

24-
def _make_type_converting_action(base_action_name, type_fn):
25-
"""Create a custom action class that wraps a standard action and applies type conversion.
26-
27-
This is used for standard string actions (e.g. 'append', 'store') when combined with type=.
28-
jsonargparse forbids type+action, so we strip type= and wrap the action to do conversion.
29-
"""
30-
# Map action name to argparse's built-in action class
31-
_action_map = {"append": argparse._AppendAction, "store": argparse._StoreAction}
32-
33-
base_cls = _action_map.get(base_action_name)
34-
if base_cls is None:
35-
# Unknown action string - can't wrap it
36-
return None
37-
38-
class TypeConvertingAction(base_cls):
39-
def __call__(self, parser, namespace, values, option_string=None):
40-
if type_fn is not None and isinstance(values, str):
41-
try:
42-
values = type_fn(values)
43-
except argparse.ArgumentTypeError as e:
44-
raise argparse.ArgumentError(self, str(e))
45-
super().__call__(parser, namespace, values, option_string)
46-
47-
TypeConvertingAction.__name__ = f"TypeConverting{base_action_name.title()}Action"
48-
return TypeConvertingAction
49-
50-
51-
class BorgAddArgumentMixin:
52-
"""Mixin to provide Borg's add_argument logic to ArgumentParser and ArgumentGroup."""
53-
54-
def add_argument(self, *args, **kwargs):
55-
"""Handle type+action combination that jsonargparse forbids.
56-
57-
jsonargparse raises ValueError when both type= and action= are given.
58-
We strip type= from kwargs and ensure the action handles type conversion:
59-
- Standard string actions: wrapped in TypeConvertingAction
60-
- Other custom actions: type stored as _type_fn on action instance
61-
"""
62-
action = kwargs.get("action")
63-
if action is not None and "type" in kwargs:
64-
type_fn = kwargs.pop("type")
65-
66-
if isinstance(action, str):
67-
# Standard action string like 'append', 'store'
68-
wrapper = _make_type_converting_action(action, type_fn)
69-
if wrapper is not None:
70-
kwargs["action"] = wrapper
71-
return super().add_argument(*args, **kwargs)
72-
else:
73-
# Unknown standard action, put type back and try anyway
74-
kwargs["type"] = type_fn
75-
76-
elif isinstance(action, type) and issubclass(action, argparse.Action):
77-
# Custom action class - register without type, then patch the action
78-
result = super().add_argument(*args, **kwargs)
79-
# Store type_fn on the action for potential manual use
80-
if hasattr(result, "_type_fn"):
81-
pass # already handled
82-
else:
83-
result._type_fn = type_fn
84-
return result
85-
86-
return super().add_argument(*args, **kwargs)
87-
88-
89-
class ArgumentGroup(BorgAddArgumentMixin, _JAPArgumentGroup):
90-
"""ArgumentGroup that supports Borg's add_argument patterns."""
17+
class ArgumentGroup(_JAPArgumentGroup):
18+
"""ArgumentGroup for Borg."""
9119

9220
pass
9321

9422

95-
class ArgumentParser(BorgAddArgumentMixin, _JAPArgumentParser):
23+
class ArgumentParser(_JAPArgumentParser):
9624
"""ArgumentParser bridging Borg's argparse patterns with jsonargparse."""
9725

9826
def __init__(self, *args, **kwargs):

src/borg/patterns.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ def parse(self, fobj, args):
7575
load_pattern_file(fobj, ArgparsePatternFileAction.roots_from_patterns, args.patterns)
7676

7777

78+
class ArgparseExcludePatternAction(argparse.Action):
79+
"""Action for --exclude that parses and appends an exclude pattern."""
80+
81+
def __call__(self, parser, args, values, option_string=None):
82+
if args.patterns is None:
83+
args.patterns = []
84+
args.patterns.append(parse_exclude_pattern(values))
85+
86+
7887
class ArgparseExcludeFileAction(ArgparsePatternFileAction):
7988
def parse(self, fobj, args):
8089
# jsonargparse may initialize list-like attributes to None instead of []

0 commit comments

Comments
 (0)