1+ """
2+ Borg argument-parsing layer
3+ ===========================
4+
5+ All imports of ``ArgumentParser``, ``Namespace``, ``SUPPRESS``, etc. come
6+ from this module. It is the single seam between borg and the underlying
7+ parser library (jsonargparse).
8+
9+ Library choice
10+ --------------
11+ Borg uses **jsonargparse** instead of plain argparse. jsonargparse is a
12+ superset of argparse that additionally supports:
13+
14+ * reading arguments from YAML/JSON config files (``--config``)
15+ * reading arguments from environment variables
16+ * nested namespaces for subcommands (each subcommand's arguments live in
17+ their own ``Namespace`` object rather than the flat top-level namespace)
18+
19+ Parser hierarchy
20+ ----------------
21+ Borg's command line has up to three levels::
22+
23+ borg [common-opts] <command> [common-opts] [<subcommand> [common-opts] [args]]
24+
25+ e.g. borg --info create ...
26+ borg create --info ...
27+ borg debug info --debug ...
28+
29+ Three ``ArgumentParser`` instances are constructed in ``build_parser()``:
30+
31+ ``parser`` (top-level)
32+ The root parser. Common options are registered here **with real
33+ defaults** (``provide_defaults=True``).
34+
35+ ``common_parser``
36+ A helper parser (``add_help=False``) passed as ``parents=[common_parser]``
37+ to every *leaf* subcommand parser (e.g. ``create``, ``repo-create``, …).
38+ Common options are registered here **with** ``default=SUPPRESS`` so that
39+ an option not given on the command line leaves no attribute at all in the
40+ subcommand namespace.
41+
42+ ``mid_common_parser``
43+ Same as ``common_parser`` but used as the parent for *group* subcommand
44+ parsers that introduce a second level (e.g. ``debug``, ``key``,
45+ ``benchmark``). Their *leaf* subcommand parsers also use
46+ ``mid_common_parser`` as a parent.
47+
48+ Common options (``--info``, ``--debug``, ``--repo``, ``--lock-wait``, …)
49+ are managed by ``Archiver.CommonOptions``, which calls
50+ ``define_common_options()`` once per parser so the same options appear at
51+ every level with identical ``dest`` names.
52+
53+ Namespace flattening and precedence
54+ -------------------------------------
55+ jsonargparse stores each subcommand's parsed values in a nested
56+ ``Namespace`` object::
57+
58+ # borg --info create --debug ...
59+ Namespace(
60+ log_level = "info", # top-level
61+ subcommand = "create",
62+ create = Namespace(
63+ log_level = "debug", # subcommand level
64+ ...
65+ )
66+ )
67+
68+ After ``parser.parse_args()`` returns, ``flatten_namespace()`` collapses
69+ this tree into a single ``Namespace`` that borg's dispatch and command
70+ implementations expect.
71+
72+ Precedence rule: the **most-specific** (innermost) value wins.
73+ ``flatten_namespace`` uses ``Namespace.as_flat()`` (provided by jsonargparse)
74+ to linearise the nested tree into a flat dict with dotted keys encoding
75+ depth, for example::
76+
77+ log_level = "info" # top-level (0 dots)
78+ create.log_level = "debug" # one level deep (1 dot)
79+ debug.info.log_level = "critical" # two levels deep (2 dots)
80+
81+ The entries are then sorted deepest-first so the most-specific value is
82+ encountered first and wins. Shallower values only fill in if the key
83+ has not been set yet.
84+
85+ Special case — append-action options (e.g. ``--debug-topic``):
86+ If a key already holds a list and the outer level also supplies a list,
87+ the two lists are **merged** (outer values first, inner values last) so
88+ that ``borg --debug-topic foo create --debug-topic bar`` accumulates
89+ ``["foo", "bar"]`` rather than losing one of the values.
90+
91+ The ``SUPPRESS`` default on sub-parsers is essential: if a common option
92+ is not given at the subcommand level, it simply produces no attribute in
93+ the subcommand namespace and the outer (top-level) default flows through
94+ unchanged.
95+ """
96+
197from typing import Any
298
399# here are the only imports from argparse and jsonargparse,
13109
14110def flatten_namespace (ns : Any ) -> Namespace :
15111 """
16- Recursively flattens a nested namespace into a single-level namespace.
17- JSONArgparse uses nested namespaces for subcommands, whereas borg's
18- internal dispatch and logic expect a flat namespace.
112+ Flattens the nested namespace jsonargparse produces for subcommands into a
113+ single-level namespace that borg's dispatch and command implementations expect.
19114
20115 Inner (subcommand) values take precedence over outer (top-level) values.
21116 For list-typed values (append-action options like --debug-topic) that appear
22117 at multiple levels, the lists are merged: outer values first, inner values last.
23118 """
24119 flat = Namespace ()
25120
26- # Extract the nested path of subcommands
121+ # Extract the joined subcommand path from the nested namespace tree.
27122 subcmds = []
28123 current = ns
29124 while current and hasattr (current , "subcommand" ) and current .subcommand :
@@ -33,28 +128,22 @@ def flatten_namespace(ns: Any) -> Namespace:
33128 if subcmds :
34129 flat .subcommand = " " .join (subcmds )
35130
36- def _flatten (source , target ):
37- items = list (
38- vars (source ).items () if hasattr (source , "__dict__" ) else source .items () if hasattr (source , "items" ) else []
39- )
40- # First pass: recurse into sub-namespaces so inner (subcommand) values are set first.
41- for k , v in items :
42- if isinstance (v , Namespace ) or type (v ).__name__ == "Namespace" :
43- _flatten (v , target )
44- # Second pass: apply this level's plain values.
45- # - If not yet set: set it (inner already won via the first pass).
46- # - If already set and both are lists: merge outer + inner (for append-action options).
47- for k , v in items :
48- if isinstance (v , Namespace ) or type (v ).__name__ == "Namespace" :
49- continue
50- if k == "subcommand" :
51- continue
52- existing = getattr (target , k , None )
53- if existing is None :
54- setattr (target , k , v )
55- elif isinstance (existing , list ) and isinstance (v , list ):
56- # Append-action options (e.g. --debug-topic): outer values come first.
57- setattr (target , k , list (v ) + list (existing ))
58-
59- _flatten (ns , flat )
131+ # as_flat() linearises the nested tree into dotted-key entries, e.g.:
132+ # log_level='info' (outer, 0 dots)
133+ # create.log_level='debug' (subcommand, 1 dot)
134+ # debug.info.log_level='crit' (two-level subcommand, 2 dots)
135+ # Sorting deepest-first ensures the most-specific value is processed first and therefore wins ("inner wins" rule).
136+ all_items = sorted (vars (ns .as_flat ()).items (), key = lambda kv : kv [0 ].count ("." ), reverse = True )
137+
138+ for dotted_key , value in all_items :
139+ dest = dotted_key .rsplit ("." , 1 )[- 1 ] # e.g. "create.log_level" -> "log_level"
140+ if dest == "subcommand" :
141+ continue
142+ existing = getattr (flat , dest , None )
143+ if existing is None :
144+ setattr (flat , dest , value )
145+ elif isinstance (existing , list ) and isinstance (value , list ):
146+ # Append-action options (e.g. --debug-topic): outer values come first.
147+ setattr (flat , dest , list (value ) + list (existing ))
148+
60149 return flat
0 commit comments