Skip to content

Args

duho.args

AUTO = _AutoVersion() module-attribute

Arg = _ty.Annotated module-attribute

Factory = _ty.Callable[[str], _T] module-attribute

NOT_DEFINED = _inspect.NOT_DEFINED module-attribute

NS = _argparse.Namespace module-attribute

__all__ = ['Append', 'Argument', 'ArgumentBuilder', 'ArgumentMeta', 'Args', 'Arg', 'Choice', 'Cli', 'Cmd', 'command', 'Const', 'Count', 'Extend', 'Factory', 'main', 'Meta', 'NS', 'NOT_DEFINED', 'parse', 'parse_globals', 'print_agent_help', 'print_completion', 'UpdateAction', 'value_sources'] module-attribute

Args(**kwargs)

Bases: Namespace

Source code in src/duho/args.py
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
def __init__(self, **kwargs):
    # Namespace.__init__ only setattrs what's passed, so a directly-built
    # instance (or the self-cloning `type(self)(**self._get_kwargs())`
    # pattern) would be missing any declared field not supplied -- notably
    # `store_true` bools, whose default only materializes via argparse.
    # Seed each declared field to its effective default when absent, so a
    # direct instance has the same attribute surface as a parsed one. Only
    # GAPS are filled: passed kwargs (incl. parsed values) always win, and a
    # required field with no default (NOT_DEFINED) is left unset.
    super().__init__(**kwargs)
    for builder in type(self)._getargs_():
        name = builder.name
        if name in kwargs or hasattr(self, name):
            continue
        default = builder._effective_default_()
        if default is not NOT_DEFINED:
            setattr(self, name, default)

Argument

Bases: Protocol

from_type(factory, **kwargs) classmethod

Source code in src/duho/args.py
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
@classmethod
def from_type(cls, factory: _ty.Callable[[str], _T], **kwargs):
    _factory = factory

    class Arg(cls):

        @classmethod
        def _argbuilder_(
            cls,
            name: str,
            decl: _inspect.ClsArgDeclaration,
            factory: "Factory | None" = _factory,
        ):
            builder = super()._argbuilder_(name, decl, factory or _factory)
            for k, v in kwargs.items():
                setattr(builder, k, v)
            return builder

    return Arg

ArgumentBuilder

Bases: Namespace

action = None class-attribute instance-attribute

choices = None class-attribute instance-attribute

collection = None class-attribute instance-attribute

const = NOT_DEFINED class-attribute instance-attribute

default instance-attribute

env = None class-attribute instance-attribute

flags instance-attribute

help instance-attribute

metavar = None class-attribute instance-attribute

name instance-attribute

nargs = None class-attribute instance-attribute

required = None class-attribute instance-attribute

type instance-attribute

version = None class-attribute instance-attribute

add_to_parser(parser)

Source code in src/duho/args.py
1122
1123
1124
1125
1126
1127
1128
1129
1130
def add_to_parser(self, parser: _argparse.ArgumentParser):
    help = self.help
    if callable(help):  # type:ignore
        help = help()
    return parser.add_argument(
        *self.flags,
        help=help,
        **self._kwargs(),
    )

convert_layered(raw, *, source)

Convert a raw env/config layer value to this field's Python value.

The env/config layers feed parser.set_defaults directly, bypassing argparse's own type=/action= handling -- so a layered value must be converted here to match what CLI parsing of the same field yields. Three field shapes are handled:

  • bool (self.type is bool or a store_true/BooleanOptionalAction effective action): real bools pass through; strings map via the strict :data:_BOOL_TRUE/:data:_BOOL_FALSE sets (unknown -> error).
  • collection (self.collection set): a string raw becomes a single element wrapped in the collection (FILES=a.txt -> ["a.txt"], matching one CLI occurrence); a list/tuple/set raw (a TOML array) converts element-wise then coerces to the collection.
  • scalar: via :meth:_convert_single.

source names the layer ("env"/"config") for error messages; the calling resolver wraps any ValueError/TypeError with the field and variable name.

Source code in src/duho/args.py
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
def convert_layered(self, raw, *, source: str):
    """Convert a raw env/config *layer* value to this field's Python value.

    The env/config layers feed ``parser.set_defaults`` directly, bypassing
    argparse's own ``type=``/``action=`` handling -- so a layered value must
    be converted here to match what CLI parsing of the same field yields.
    Three field shapes are handled:

    * **bool** (``self.type is bool`` or a store_true/BooleanOptionalAction
      effective action): real bools pass through; strings map via the
      strict :data:`_BOOL_TRUE`/:data:`_BOOL_FALSE` sets (unknown -> error).
    * **collection** (``self.collection`` set): a *string* raw becomes a
      single element wrapped in the collection (``FILES=a.txt`` ->
      ``["a.txt"]``, matching one CLI occurrence); a *list/tuple/set* raw
      (a TOML array) converts element-wise then coerces to the collection.
    * **scalar**: via :meth:`_convert_single`.

    ``source`` names the layer ("env"/"config") for error messages; the
    calling resolver wraps any ``ValueError``/``TypeError`` with the field
    and variable name.
    """
    is_bool = (
        self.type is bool
        or self.action in ("store_true", "store_false")
        or self.action is _argparse.BooleanOptionalAction
    )
    if is_bool:
        if isinstance(raw, bool):
            return raw
        if isinstance(raw, str):
            low = raw.strip().lower()
            if low in self._BOOL_TRUE:
                return True
            if low in self._BOOL_FALSE:
                return False
            raise ValueError(
                f"{raw!r} is not a valid boolean "
                f"(expected one of {sorted(self._BOOL_TRUE | self._BOOL_FALSE)})"
            )
        raise ValueError(f"cannot interpret {raw!r} ({source}) as a boolean")

    if self.collection is dict:
        # A dict field: a *string* raw ("k=v") runs the KV factory (one-pair
        # dict); a TOML *table* (Mapping) converts each value through the
        # value factory (strings only; already-typed TOML values pass
        # through, matching :meth:`_convert_single`).
        if isinstance(raw, _ty.Mapping):
            value_factory = getattr(self.type, "value_factory", str)
            result: dict = {}
            for k, v in raw.items():
                result[str(k)] = value_factory(v) if isinstance(v, str) else v
            return result
        return self._convert_single(raw)

    if self.collection is not None:
        if isinstance(raw, (list, tuple, set)):
            return self.collection(self._convert_single(e) for e in raw)
        return self.collection([self._convert_single(raw)])

    return self._convert_single(raw)

ArgumentMeta

Bases: _ProtocolMeta

__instancecheck__(instance)

Source code in src/duho/args.py
757
758
759
def __instancecheck__(self, instance) -> bool:
    builder_factory = getattr(instance, "_argbuilder_", None)
    return callable(builder_factory)

Cli(**kwargs)

Bases: Cmd

Application-root layer: an opt-in mixin over Cmd.

A leaf Cmd is lean -- it declares its own CLI fields and a __call__. A Cli root is the top of an app and additionally exposes the app-wide, sandwich-named configuration attributes a plain Cmd does not declare (--version, shell completion, a config file, a subcommand tree). Opt in by subclassing Cli::

class MyApp(LoggingArgs, Cli):
    _version_ = "1.2.3"
    _completion_ = True

Cli adds no new runtime behavior for running -- it inherits Cmd.__call__ unchanged (a data-only Cli that never overrides __call__ still fails loud when dispatched, exactly like a Cmd). What it adds is two things:

  1. Typed, documented app-root class attrs. Every one of these is already read elsewhere via getattr(cls, "_x_", default) (args.py/runtime.py), so declaring them here changes no reader -- it only gives them a typed home and a class-level default where one exists. A plain Cmd leaves them undeclared; a Cli root is where they belong.
  2. Self-registration (_register_subcmd_ / @subcommand): a leaf command file can attach itself to the root's subcommand tree instead of the root listing every child centrally in _subcommands_. The two mechanisms compose (union + dedup).

LoggingArgs stays orthogonal (a separate data mixin) -- the batteries-included recipe is class MyApp(LoggingArgs, Cli) (data mixin first, executable/root base last), NOT a forced bundle. Every member Cli adds is sandwich-named or dunder, so a Cli subclass's field namespace stays 100% user-owned (annotated non-underscore attrs still become CLI fields).

Source code in src/duho/args.py
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
def __init__(self, **kwargs):
    # Namespace.__init__ only setattrs what's passed, so a directly-built
    # instance (or the self-cloning `type(self)(**self._get_kwargs())`
    # pattern) would be missing any declared field not supplied -- notably
    # `store_true` bools, whose default only materializes via argparse.
    # Seed each declared field to its effective default when absent, so a
    # direct instance has the same attribute surface as a parsed one. Only
    # GAPS are filled: passed kwargs (incl. parsed values) always win, and a
    # required field with no default (NOT_DEFINED) is left unset.
    super().__init__(**kwargs)
    for builder in type(self)._getargs_():
        name = builder.name
        if name in kwargs or hasattr(self, name):
            continue
        default = builder._effective_default_()
        if default is not NOT_DEFINED:
            setattr(self, name, default)

subcommand(child) classmethod

Decorator form of :meth:_register_subcmd_.

Lets a command file self-attach to the root::

@MyApp.subcommand
class Deploy(Cmd):
    ...

Returns child unchanged, so the decorated class keeps its identity. Equivalent to calling MyApp._register_subcmd_(Deploy).

Source code in src/duho/args.py
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
@classmethod
def subcommand(cls, child: "type[Cmd]") -> "type[Cmd]":
    """Decorator form of :meth:`_register_subcmd_`.

    Lets a command file self-attach to the root::

        @MyApp.subcommand
        class Deploy(Cmd):
            ...

    Returns ``child`` unchanged, so the decorated class keeps its
    identity. Equivalent to calling ``MyApp._register_subcmd_(Deploy)``.
    """
    return cls._register_subcmd_(child)

Cmd(**kwargs)

Bases: Args

An executable command: a data Args plus the command contract.

Args (Plan 13) is pure data -- a Namespace of parsed values, not required to run. Cmd adds the executable contract on top: __call__(self) is the command entrypoint (instance() runs the command).

The entrypoint is __call__ -- a dunder -- deliberately. A Cmd subclass's namespace is user-owned: annotated non-underscore attributes become CLI fields, so a plain method name like main would collide with a user field main: str (--main). __call__ lives in the dunder namespace duho's field introspection already skips, so it can never clash with a declared flag.

A Cmd subclass that does not override __call__ raises NotImplementedError naming the class when dispatched -- the same loud-failure spirit as Plan 04's earlier "missing __call__". Data-only Args subclasses stay non-runnable by design: duho.main rejects them with a clear error rather than silently no-op'ing (the whole point of the split is that "runnable" is explicit).

Base-order for the LoggingArgs mixin: class App(LoggingArgs, Cmd) (data mixin first, executable base last). Both orders resolve the MRO correctly since LoggingArgs defines no __call__; the recommended order reads "add logging to a command".

Source code in src/duho/args.py
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
def __init__(self, **kwargs):
    # Namespace.__init__ only setattrs what's passed, so a directly-built
    # instance (or the self-cloning `type(self)(**self._get_kwargs())`
    # pattern) would be missing any declared field not supplied -- notably
    # `store_true` bools, whose default only materializes via argparse.
    # Seed each declared field to its effective default when absent, so a
    # direct instance has the same attribute surface as a parsed one. Only
    # GAPS are filled: passed kwargs (incl. parsed values) always win, and a
    # required field with no default (NOT_DEFINED) is left unset.
    super().__init__(**kwargs)
    for builder in type(self)._getargs_():
        name = builder.name
        if name in kwargs or hasattr(self, name):
            continue
        default = builder._effective_default_()
        if default is not NOT_DEFINED:
            setattr(self, name, default)

__call__()

Run the command. Override __call__ in a Cmd subclass.

The base raises NotImplementedError naming the concrete class, so a Cmd that forgets to implement __call__ fails loud when dispatched rather than silently doing nothing.

Source code in src/duho/args.py
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
def __call__(self):  # noqa: D401 - contract stub, overridden by subclasses
    """Run the command. Override ``__call__`` in a ``Cmd`` subclass.

    The base raises ``NotImplementedError`` naming the concrete class,
    so a ``Cmd`` that forgets to implement ``__call__`` fails loud when
    dispatched rather than silently doing nothing.
    """
    raise NotImplementedError(
        f"{type(self).__name__} is a Cmd but does not implement '__call__'"
    )

Meta(help=_META_UNSET, env=_META_UNSET, conflicts=_META_UNSET, conflicts_required=_META_UNSET, group=_META_UNSET, action=_META_UNSET, nargs=_META_UNSET, const=_META_UNSET, choices=_META_UNSET, metavar=_META_UNSET, required=_META_UNSET, type=_META_UNSET, version=_META_UNSET, dest=_META_UNSET, kwargs=_META_UNSET) dataclass

Typed, typo-safe alternative to NS(...) for field metadata (F5).

NS(...) is an untyped argparse.Namespace: a misspelled key (NS(hlep="oops")) is silently dropped. Meta declares the known metadata fields as a dataclass, so an unknown keyword is a TypeError at class-definition time -- the whole point. Only the fields you set are merged (each defaults to a private sentinel); everything NS accepts, Meta accepts, and NS keeps working forever.

Use it exactly where NS goes::

level: Arg[int, Meta(help="verbosity", env="LEVEL")] = 0
("--level",)

Recommended over NS precisely because a typo fails loud instead of vanishing. The kwargs field is the same raw add_argument escape hatch NS(kwargs=...) provides.

action = _META_UNSET class-attribute instance-attribute

choices = _META_UNSET class-attribute instance-attribute

conflicts = _META_UNSET class-attribute instance-attribute

conflicts_required = _META_UNSET class-attribute instance-attribute

const = _META_UNSET class-attribute instance-attribute

dest = _META_UNSET class-attribute instance-attribute

env = _META_UNSET class-attribute instance-attribute

group = _META_UNSET class-attribute instance-attribute

help = _META_UNSET class-attribute instance-attribute

kwargs = _META_UNSET class-attribute instance-attribute

metavar = _META_UNSET class-attribute instance-attribute

nargs = _META_UNSET class-attribute instance-attribute

required = _META_UNSET class-attribute instance-attribute

type = _META_UNSET class-attribute instance-attribute

version = _META_UNSET class-attribute instance-attribute

UpdateAction

Bases: Action

Action that updates a dict instead of replacing it.

__call__(parser, namespace, values, option_string=None)

Source code in src/duho/args.py
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
def __call__(  # type:ignore
    self, parser, namespace, values: dict, option_string=None
):
    items = getattr(namespace, self.dest, None)
    # Shallow copy per occurrence: the default/prior value must not be
    # mutated in place (it may be a shared seeded default), but a deep copy
    # is wasteful and surprising -- callers want the merged mapping, not
    # clones of the values (F1). A ``None`` starting value (an explicit
    # ``= None`` default) is treated as an empty mapping.
    items = dict(items) if items else {}
    items.update(values or {})
    setattr(namespace, self.dest, items)

Append(type=str, **kw)

Create an append-action argument, accumulating repeated flag values.

Explicitly clears nargs: a bare list/list[T] annotation's implicit builder defaults to action="extend", nargs="" (space-separated), which would make append() collect a list* per occurrence instead of a scalar.

Source code in src/duho/args.py
2016
2017
2018
2019
2020
2021
2022
2023
def Append(type: "Factory" = str, **kw):
    """Create an append-action argument, accumulating repeated flag values.

    Explicitly clears nargs: a bare `list`/`list[T]` annotation's implicit
    builder defaults to action="extend", nargs="*" (space-separated), which
    would make append() collect a *list* per occurrence instead of a scalar.
    """
    return NS(action="append", type=type, nargs=None, kwargs=kw)

Choice(*choices, **kw)

Restrict an argument's accepted values to choices.

Source code in src/duho/args.py
2031
2032
2033
def Choice(*choices, **kw):
    """Restrict an argument's accepted values to `choices`."""
    return NS(choices=tuple(choices), kwargs=kw)

Const(value, **kw)

Create a store_const-action argument that stores value when present.

Source code in src/duho/args.py
2026
2027
2028
def Const(value, **kw):
    """Create a store_const-action argument that stores `value` when present."""
    return NS(action="store_const", const=value, kwargs=kw)

Count(**kw)

Create a count-action argument (e.g. -vvv -> 3).

Source code in src/duho/args.py
2011
2012
2013
def Count(**kw):
    """Create a count-action argument (e.g. `-vvv` -> 3)."""
    return NS(action="count", kwargs=kw)

Extend(split, **kwargs)

Create an extend-action argument with optional string splitting.

list[str]'s own default builder sets nargs="*" (so a plain list field accepts both --x a --x b and --x a b); combined with a type that SPLITS one token into several (as this factory's ty does), that combination double-collects: argparse gathers nargs="*" tokens first and applies type to EACH ONE individually, so a token's split result (itself a list, e.g. "a,b" -> ["a", "b"]) is appended to the destination as ONE nested element instead of being flattened -- --rcopts '!*,build' became [['!*', 'build']], not ['!*', 'build']. Explicitly overriding nargs=None here (a single string per flag occurrence, argparse's own default) avoids the double-collection: type splits that one occurrence into its own list, and argparse's built-in extend action flattens a list-valued single occurrence into the destination correctly (verified: repeated --x a,b --x c and a single --x a,b both produce a flat list, no nested sub-lists).

Source code in src/duho/args.py
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
def Extend(split: "str | _ty.Callable[[str], _ty.Iterable]", **kwargs):
    """Create an extend-action argument with optional string splitting.

    ``list[str]``'s own default builder sets ``nargs="*"`` (so a plain list
    field accepts both ``--x a --x b`` and ``--x a b``); combined with a
    ``type`` that SPLITS one token into several (as this factory's ``ty``
    does), that combination double-collects: argparse gathers ``nargs="*"``
    tokens first and applies ``type`` to EACH ONE individually, so a token's
    split result (itself a list, e.g. ``"a,b"`` -> ``["a", "b"]``) is appended
    to the destination as ONE nested element instead of being flattened --
    ``--rcopts '!*,build'`` became ``[['!*', 'build']]``, not ``['!*',
    'build']``. Explicitly overriding ``nargs=None`` here (a single string per
    flag occurrence, argparse's own default) avoids the double-collection:
    ``type`` splits that one occurrence into its own list, and argparse's
    built-in ``extend`` action flattens a list-valued single occurrence into
    the destination correctly (verified: repeated ``--x a,b --x c`` and a
    single ``--x a,b`` both produce a flat list, no nested sub-lists).
    """
    kwargs.setdefault("default", [])
    kwargs.setdefault("nargs", None)
    if isinstance(split, str):
        ty: _ty.Callable[[str], list] = lambda x: x.split(split)  # type:ignore
    else:

        def ty(text: str):
            result = split(text)
            if isinstance(result, list):
                return result
            return list(result)

    return _argparse.Namespace(type=ty, action="extend", kwargs=kwargs)

command(args_cls, func, *, name=None)

Build a Cmd subclass from a data Args class and a callable.

Lets a user attach behavior to an existing data Args without rewriting it as a Cmd subclass ("build one from Args and a method"). The returned class subclasses BOTH args_cls (to inherit its declared fields / parsing machinery) and Cmd (for the executable contract). Its __call__ calls func(self) -- the parsed instance IS the parsed args -- so command(MyArgs, f) makes f receive the parsed MyArgs-shaped instance and its return value becomes the command's result.

name (optional) sets the built class's _parsername_ (the subcommand name). When omitted, the usual _parsername_/class-name rule applies to the generated class.

Source code in src/duho/args.py
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
def command(
    args_cls: "type[Args]",
    func: "_ty.Callable[[_ty.Any], object]",
    *,
    name: "str | None" = None,
) -> "type[Cmd]":
    """Build a ``Cmd`` subclass from a data ``Args`` class and a callable.

    Lets a user attach behavior to an existing data ``Args`` without
    rewriting it as a ``Cmd`` subclass ("build one from Args and a
    method"). The returned class subclasses BOTH ``args_cls`` (to inherit
    its declared fields / parsing machinery) and ``Cmd`` (for the
    executable contract). Its ``__call__`` calls ``func(self)`` -- the parsed
    instance IS the parsed args -- so ``command(MyArgs, f)`` makes ``f``
    receive the parsed ``MyArgs``-shaped instance and its return value becomes
    the command's result.

    ``name`` (optional) sets the built class's ``_parsername_`` (the
    subcommand name). When omitted, the usual
    ``_parsername_``/class-name rule applies to the generated class.
    """
    if Cmd in getattr(args_cls, "__mro__", ()):
        bases: tuple = (args_cls,)
    else:
        bases = (args_cls, Cmd)

    def __call__(self, _func=func):
        return _func(self)

    namespace: "dict[str, object]" = {"__call__": __call__}
    if name is not None:
        namespace["_parsername_"] = name

    cls_name = name or getattr(args_cls, "__name__", "Command")
    return _ty.cast("type[Cmd]", type(cls_name, bases, namespace))

main(cls, argv=None, *, setup_logging=True, config=None)

Build a parser for cls, parse argv, and dispatch the selected Cmd.

Module-level (not a classmethod) so the Args subclass namespace stays entirely user-owned. Steps: build parser (auto-registers subcommands), apply the env/config/class-default layers (config overrides cls._config_; precedence CLI > env > config > class default), parse argv (SystemExit from argparse propagates), optionally set up stderr logging + apply verbosity when the resulting instance provides set_loglevels, then run the command and map a None return to 0.

Since Plan 13's Args/Cmd split, dispatch expects the selected class to be a Cmd (executable, defines __call__). A bare data Args -- with no __call__ -- raises a clear NotImplementedError ("Args holds data; make it a Cmd to run it") rather than silently doing nothing.

Source code in src/duho/args.py
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
def main(
    cls,
    argv: "_ty.Sequence[str] | None" = None,
    *,
    setup_logging=True,
    config: "str | _pathlib.Path | None" = None,
) -> int:
    """Build a parser for cls, parse argv, and dispatch the selected Cmd.

    Module-level (not a classmethod) so the Args subclass namespace stays
    entirely user-owned. Steps: build parser (auto-registers _subcommands_),
    apply the env/config/class-default layers (`config` overrides `cls._config_`;
    precedence CLI > env > config > class default), parse argv (SystemExit from
    argparse propagates), optionally set up stderr logging + apply verbosity
    when the resulting instance provides _set_loglevels_, then run the command
    and map a None return to 0.

    Since Plan 13's Args/Cmd split, dispatch expects the selected class to be
    a ``Cmd`` (executable, defines ``__call__``). A bare data ``Args`` -- with
    no ``__call__`` -- raises a clear ``NotImplementedError`` ("Args holds data;
    make it a Cmd to run it") rather than silently doing nothing.
    """
    parser = cls._parser_()
    _apply_default_layers(parser, cls, config)
    instance = parser.parse_args(argv)

    if setup_logging and hasattr(instance, "_set_loglevels_"):
        root = _logging_module.getLogger()
        if not root.handlers:
            _duho_logging.init_stderr_logging()
        instance._set_loglevels_()

    run = getattr(instance, "__call__", None)
    if run is None:
        raise NotImplementedError(
            f"{type(instance).__name__} holds data but is not runnable "
            f"(no '__call__'); make it a Cmd (subclass duho.Cmd or "
            f"build one with duho.command(...)) to run it"
        )

    result = _maybe_await(run())
    return 0 if result is None else result

parse(spec, argv=None, *, parser_kwargs=None, config=None)

Build a parser from spec and parse argv into a new instance.

spec may be: - An Args subclass (type): equivalent to spec._parser_().parse_args(argv), with the env/config/class-default layers applied first (see below). - An instance of an Args subclass: the instance's current field values are used as argparse defaults (via parser.set_defaults(**overrides), filtered to actual CLI fields -- not vars(spec), which would include framework attrs). CLI args still override those defaults. Returns a NEW instance of type(spec); spec itself is never mutated.

config (a path, or None to fall back to cls._config_) layers config-file and environment-variable defaults under the instance/CLI ones. Full precedence: CLI args > instance field values > env > config file > class defaults. Note this means a required field (no class default) that is supplied by any layer becomes effectively optional for this call.

Source code in src/duho/args.py
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
def parse(
    spec,
    argv: "_ty.Sequence[str] | None" = None,
    *,
    parser_kwargs=None,
    config: "str | _pathlib.Path | None" = None,
):
    """Build a parser from `spec` and parse `argv` into a new instance.

    `spec` may be:
    - An `Args` subclass (type): equivalent to `spec._parser_().parse_args(argv)`,
      with the env/config/class-default layers applied first (see below).
    - An instance of an `Args` subclass: the instance's current field values
      are used as argparse defaults (via `parser.set_defaults(**overrides)`,
      filtered to actual CLI fields -- not `vars(spec)`, which would include
      framework attrs). CLI args still override those defaults. Returns a
      NEW instance of `type(spec)`; `spec` itself is never mutated.

    `config` (a path, or None to fall back to `cls._config_`) layers config-file
    and environment-variable defaults under the instance/CLI ones. Full
    precedence: CLI args > instance field values > env > config file > class
    defaults. Note this means a required field (no class default) that is
    supplied by *any* layer becomes effectively optional for this call.
    """
    parser_kwargs = parser_kwargs or {}
    if isinstance(spec, type):
        cls = spec
        parser = cls._parser_(**parser_kwargs)
        _apply_default_layers(parser, cls, config)
        return parser.parse_args(argv)

    cls = type(spec)
    parser = cls._parser_(**parser_kwargs)
    _apply_default_layers(parser, cls, config)
    field_names = {builder.name for builder in cls._getargs_()}
    overrides = {
        name: value
        for name, value in vars(spec).items()
        if name in field_names
    }
    parser.set_defaults(**overrides)
    # set_defaults() alone doesn't satisfy argparse's required= check (it's
    # enforced independently of the default value) -- an instance-supplied
    # value for a field that has no class default (required=True) must also
    # clear the action's required flag, or parse_args([]) still raises
    # SystemExit even though a usable value is now present via the default.
    for action in parser._actions:
        if action.dest in overrides:
            action.required = False
    return parser.parse_args(argv)

parse_globals(cls, argv=None, **parser_kwargs)

Parse ONLY a root command's global args, ignoring/relaxing subcommands.

Builds cls's root parser (cls._parser_(**parser_kwargs)) and parses argv with help suppressed and subcommand validation relaxed, so a consumer can resolve config-file-driven command search paths (or any other global) BEFORE building/committing to the full subcommand parser. This is the documented, public form of the internal prepass duho.app already runs -- it wraps :func:duho.parsers.prerun_parse verbatim rather than reimplementing the _HelpAction/subparser patching (which prerun_parse performs and restores in a finally).

Returns the parsed root instance with globals set. Subcommand arguments are NOT validated in this pass: a missing subcommand does not error, and an unknown trailing token does not crash the globals parse (it is simply ignored here). A caller that also wants the leftover argv should call parser.parse_known_args directly -- parse_globals deliberately returns a single value (the globals-only instance), mirroring the shape prerun_parse yields.

**parser_kwargs are forwarded to cls._parser_ (e.g. add_help=False), mirroring :func:duho.parser.

Source code in src/duho/args.py
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
def parse_globals(cls, argv: "_ty.Sequence[str] | None" = None, **parser_kwargs):
    """Parse ONLY a root command's global args, ignoring/relaxing subcommands.

    Builds ``cls``'s root parser (``cls._parser_(**parser_kwargs)``) and parses
    ``argv`` with help suppressed and subcommand validation relaxed, so a
    consumer can resolve config-file-driven command search paths (or any other
    global) BEFORE building/committing to the full subcommand parser. This is
    the documented, public form of the internal prepass ``duho.app`` already
    runs -- it wraps :func:`duho.parsers.prerun_parse` verbatim rather than
    reimplementing the ``_HelpAction``/subparser patching (which
    ``prerun_parse`` performs and restores in a ``finally``).

    Returns the parsed root instance with globals set. Subcommand arguments are
    NOT validated in this pass: a missing subcommand does not error, and an
    unknown trailing token does not crash the globals parse (it is simply
    ignored here). A caller that also wants the leftover argv should call
    ``parser.parse_known_args`` directly -- ``parse_globals`` deliberately
    returns a single value (the globals-only instance), mirroring the shape
    ``prerun_parse`` yields.

    ``**parser_kwargs`` are forwarded to ``cls._parser_`` (e.g. ``add_help=False``),
    mirroring :func:`duho.parser`.
    """
    from .parsers import prerun_parse as _prerun_parse

    parser = cls._parser_(**parser_kwargs)
    # A globals-only parse must not descend into the subcommand tree. Building
    # cls._parser_() materializes any ``_subcommands_`` as a real subparsers
    # action; leaving it in place makes even a globals-only ``prerun_parse``
    # re-enter the root parser's patched ``parse_known_args`` for any trailing
    # token (a subcommand name OR an unknown flag after the globals), which
    # double-pops the internal "#cls" marker and raises KeyError. Dropping the
    # subparsers action makes trailing tokens plain unrecognized extras (which
    # ``prerun_parse`` discards) -- the same shape ``duho.app``'s prepass gets by
    # running before it adds subparsers.
    for action in list(parser._actions):
        if isinstance(action, _argparse._SubParsersAction):
            parser._actions.remove(action)
            subparsers_group = getattr(parser, "_subparsers", None)
            if subparsers_group is not None and action in subparsers_group._actions:
                subparsers_group._actions.remove(action)
    return _prerun_parse(parser, argv)

print_agent_help(cls, file=None)

Print a detailed, machine-readable (JSON) agent-help document for cls.

Standalone counterpart to the --help-agents flag / the AGENT_HELP env-var trigger: builds cls's parser tree fresh and describes it (independent of whether either trigger is wired up), then writes the JSON to file (default sys.stdout). Delegates to :func:duho.agenthelp.print_agent_help.

Source code in src/duho/args.py
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
def print_agent_help(cls, file=None) -> None:
    """Print a detailed, machine-readable (JSON) agent-help document for `cls`.

    Standalone counterpart to the ``--help-agents`` flag / the ``AGENT_HELP``
    env-var trigger: builds ``cls``'s parser tree fresh and describes it
    (independent of whether either trigger is wired up), then writes the JSON to
    ``file`` (default ``sys.stdout``). Delegates to
    :func:`duho.agenthelp.print_agent_help`.
    """
    from . import agenthelp as _agenthelp

    _agenthelp.print_agent_help(cls, file=file)

print_completion(cls, shell, file=None)

Print a shell completion script for cls to file (default sys.stdout).

shell is one of "bash", "zsh", "fish", or "powershell". Standalone counterpart to the --print-completion flag injected when _completion_ = True -- builds cls's parser tree fresh (independent of whether _completion_ is set) and delegates to duho.completion.<shell>.

Source code in src/duho/args.py
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
def print_completion(cls, shell: str, file=None) -> None:
    """Print a shell completion script for `cls` to `file` (default sys.stdout).

    ``shell`` is one of ``"bash"``, ``"zsh"``, ``"fish"``, or ``"powershell"``.
    Standalone counterpart to the `--print-completion` flag injected when
    `_completion_ = True` -- builds cls's parser tree fresh (independent of
    whether `_completion_` is set) and delegates to `duho.completion.<shell>`.
    """
    from . import completion as _completion

    if file is None:
        file = _sys.stdout
    parser = cls._parser_()
    emitter = getattr(_completion, shell)
    file.write(emitter(parser))

value_sources(parsed)

Report the origin layer ("cli", "env", "config", or "default") of each field on a parsed instance produced by duho.parse/duho.main.

Looks up the owning parser via the per-class _duho_last_parser_ linkage stashed during dispatch (see _initparser_). Returns {} if unavailable (e.g. the instance wasn't produced via a parser built by this framework, or no parse has happened yet for its class).

A field is "cli" if its parsed value differs from the effective default that was in effect for that parse -- the merged env/config value when the field was touched by one of those layers, else the class default. Otherwise it's whatever layer contributed that default ("env"/"config"), or "default" if no layer touched it (value == the untouched class default).

Source code in src/duho/args.py
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
def value_sources(parsed) -> "dict[str, str]":
    """Report the origin layer ("cli", "env", "config", or "default") of each
    field on a parsed instance produced by `duho.parse`/`duho.main`.

    Looks up the owning parser via the per-class `_duho_last_parser_`
    linkage stashed during dispatch (see `_initparser_`). Returns `{}` if
    unavailable (e.g. the instance wasn't produced via a parser built by
    this framework, or no parse has happened yet for its class).

    A field is "cli" if its parsed value differs from the effective default
    that was in effect for that parse -- the merged env/config value when
    the field was touched by one of those layers, else the class default.
    Otherwise it's whatever layer contributed that default ("env"/"config"),
    or "default" if no layer touched it (value == the untouched class default).
    """
    parser = getattr(type(parsed), "_duho_last_parser_", None)
    if parser is None:
        return {}
    sources: "dict[str, str]" = getattr(parser, "_duho_value_sources_", None) or {}
    merged: "dict[str, object]" = getattr(parser, "_duho_merged_defaults_", None) or {}

    result: "dict[str, str]" = {}
    for builder in type(parsed)._getargs_():
        name = builder.name
        if not hasattr(parsed, name):
            continue
        value = getattr(parsed, name)
        if name in merged:
            effective_default = merged[name]
            layer = sources.get(name, "default")
        else:
            # Use the builder's EFFECTIVE default (e.g. False for an undeclared
            # store_true bool), not the raw declared default (NOT_DEFINED), or a
            # field left at its argparse default is mislabeled "cli" (C14).
            effective_default = builder._effective_default_()
            layer = "default"
        result[name] = layer if value == effective_default else "cli"
    return result