Skip to content

Args

duho.args

AUTO = _AutoVersion() module-attribute

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

NOT_DEFINED = _introspect.NOT_DEFINED module-attribute

__all__ = ['Append', 'app_name', 'Argument', 'ArgumentBuilder', 'ArgumentMeta', 'Args', 'Arg', 'AUTO', 'Choice', 'ClsArgDeclaration', 'Cli', 'Cmd', 'command', 'Const', 'Count', 'Extend', 'Factory', 'finish_parse', 'main', 'InitParserKwargs', 'Meta', 'NOT_DEFINED', 'parse', 'parse_globals', 'print_agent_help', 'print_completion', 'subcommand', 'UpdateAction', 'ConfigSource', 'config_source', 'value_sources'] module-attribute

Args(**kwargs)

Bases: Namespace

Base class for a duho command's declared fields.

Subclass this and declare CLI fields as annotated, non-underscore class attributes. Everything duho reads or sets is sandwich-named (_x_) or a dunder, so the field namespace stays user-owned; only help, version and print_completion, which would clash with a flag duho adds, are rejected as field names (see docs/guide/arguments.md). An optional flags tuple and a trailing docstring string customize each field's add_argument call. Args itself is data-only -- it defines no __call__ (:func:duho.main/:func:duho.run_command raise a clear NotImplementedError for a bare Args); subclass :class:Cmd instead for a RUNNABLE command, or :class:Cli for an app root.

Args subclasses argparse.Namespace, whose stub declares __getattr__(self, name) -> Any -- so a type checker does NOT catch a typo'd field read (self.regoin on a class with a region field); only a real run surfaces it, as an AttributeError.

Source code in src/duho/args/_argsclass.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def __init__(self, /, **kwargs: object) -> None:
    # Seed each declared field's effective default when absent, so a direct
    # instance has the attribute surface of a parsed one (e.g. a `store_true`
    # bool). Passed kwargs win; a required field (NOT_DEFINED) stays unset.
    # Not `super().__init__(**kwargs)`: a field named `self` would collide.
    super().__init__()
    for key, value in kwargs.items():
        setattr(self, key, value)
    if "_passthrough_" not in vars(self):
        self._passthrough_ = []
    seeded: dict[str, object] = {}
    for builder in type(self)._getargs_():
        name = builder.name
        # `vars(self)`, not `hasattr`: a class-level default is `hasattr`-true
        # by inheritance, so the instance would share, and mutations would
        # leak through, the class attribute.
        if name in kwargs or name in vars(self):
            continue
        default = builder._effective_default_()
        if default is not NOT_DEFINED:
            setattr(self, name, default)
            seeded[name] = _copy.copy(default)
    # Record which fields THIS CALL passed, outside vars(self). A class that
    # is not weak-referenceable is untracked, and `duho.parse(instance)`
    # then treats every attribute as explicit.
    try:
        _key = id(self)
        _duho_explicit_instance_fields[_key] = frozenset(kwargs)
        _duho_seeded_instance_values[_key] = seeded
        _weakref.finalize(self, _duho_explicit_instance_fields.pop, _key, None)
        _weakref.finalize(self, _duho_seeded_instance_values.pop, _key, None)
    except TypeError:
        pass

Argument

Bases: Protocol

The customization protocol for a field's declared type.

Any object with a classmethod _argbuilder_(name, decl, factory=None) -> ArgumentBuilder satisfies this protocol (see :class:ArgumentMeta) and can be used as a field's type -- wrapped in Arg[CustomType, ...], or returned by :meth:from_type for a plain type -- to fully control how that field's :class:ArgumentBuilder is built. This is how duho's own type ladder (int, list[T], Literal[...], ...) is implemented, and the extension point a user-defined type (e.g. a Port value type) hooks into for the exact same treatment.

from_type(factory, **kwargs) classmethod

Wrap a plain type/text-factory as an :class:Argument.

Returns an Argument subclass whose _argbuilder_ builds via cls's own (Argument's base) logic using factory, then applies **kwargs (the Meta(...) metadata dict) onto the result via :func:_apply_argument_options. Every plain-typed field (i.e. one not ALREADY a custom :class:Argument) is built through this single path -- see Args._getargs_.

Source code in src/duho/args/_argument.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
@classmethod
def from_type(
    cls, factory: _ty.Callable[[str], _T], **kwargs: object
) -> type[Argument]:
    """Wrap a plain type/text-factory as an :class:`Argument`.

    Returns an ``Argument`` subclass whose ``_argbuilder_`` builds via
    ``cls``'s own (``Argument``'s base) logic using ``factory``, then
    applies ``**kwargs`` (the ``Meta(...)`` metadata dict) onto
    the result via :func:`_apply_argument_options`. Every plain-typed
    field (i.e. one not ALREADY a custom :class:`Argument`) is built
    through this single path -- see ``Args._getargs_``.
    """
    _factory = factory

    class Arg(cls):

        @classmethod
        def _argbuilder_(
            cls,
            name: str,
            decl: _introspect.ClsArgDeclaration,
            factory: Factory | None = _factory,
        ):
            builder = super()._argbuilder_(name, decl, factory or _factory)
            _apply_argument_options(builder, kwargs)
            return builder

    return Arg

ArgumentBuilder

Bases: Namespace

One declared field's build state: raw add_argument kwargs plus duho's own metadata (collection/split/env/conflicts/...).

Built by :meth:Argument._argbuilder_ (via :meth:Argument.from_type for a plain type) from a field's _introspect.ClsArgDeclaration, then post-processed with any Meta(...) metadata (:func:_apply_argument_options). One instance per declared field, cached on the owning class (Args._getargs_). :meth:_kwargs resolves it to the exact keyword arguments :meth:add_to_parser passes to parser.add_argument; the env/config layering (_raw_env_values/_raw_config_values/convert_layered) reads it to convert a non-CLI value the same way a CLI occurrence would.

action = None class-attribute instance-attribute

choices = None class-attribute instance-attribute

collection = None class-attribute instance-attribute

conflicts = None class-attribute instance-attribute

conflicts_required = False class-attribute instance-attribute

const = NOT_DEFINED class-attribute instance-attribute

default instance-attribute

enum_by = 'name' class-attribute instance-attribute

env = None class-attribute instance-attribute

flags instance-attribute

group = None class-attribute instance-attribute

help instance-attribute

is_bare_bool_flag property

True when this field compiles to a bare store_true/ BooleanOptionalAction flag with no value of its own -- as opposed to a bool that carries choices (a Literal[True, False] field, which must go through type=+choices= like any other Literal) or one with an explicit action= override.

is_positional property

True when this field's final, post-override flags are positional.

kwargs = None class-attribute instance-attribute

literal_value = False class-attribute instance-attribute

metavar = None class-attribute instance-attribute

name instance-attribute

nargs = None class-attribute instance-attribute

required = None class-attribute instance-attribute

split = None class-attribute instance-attribute

type instance-attribute

version = None class-attribute instance-attribute

add_to_parser(parser, *, layered=False)

Call parser.add_argument(*self.flags, ...) for this field.

layered=True when an env/config layer can ALSO supply a value for this field (see :meth:_kwargs's layered parameter -- it changes a bare bool field's action so the CLI can still turn a layer-supplied True back off). Returns the argparse.Action that was added.

Source code in src/duho/args/_argument.py
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
def add_to_parser(
    self, parser: _argparse.ArgumentParser, *, layered: bool = False
) -> _argparse.Action:
    """Call ``parser.add_argument(*self.flags, ...)`` for this field.

    ``layered=True`` when an env/config layer can ALSO supply a value for
    this field (see :meth:`_kwargs`'s ``layered`` parameter -- it changes
    a bare bool field's action so the CLI can still turn a layer-supplied
    ``True`` back off). Returns the ``argparse.Action`` that was added.
    """
    help = self.help
    if callable(help):  # type: ignore
        help = help()
    kwargs = self._kwargs(layered=layered)
    flags = self.flags
    if kwargs.get("action") == "store_true" and layered and self.is_bare_bool_flag:
        # `_kwargs` gives a False-default `no_*` bool plain store_true, which
        # cannot turn a LAYERED True back off. Add the positive counterpart
        # (`--verify`) as an EXTRA option string on this SAME action (layering
        # keys off one action per dest) that sets False; declared flags still
        # set True.
        positive_flags = tuple(
            "--" + f[len("--no-") :]
            for f in self.flags
            if f.startswith("--no-") and len(f) > len("--no-")
        )
        if positive_flags:
            kwargs["action"] = _NegatedBoolAction
            kwargs["negative"] = self.flags
            flags = self.flags + positive_flags
    kwargs.setdefault("help", help)
    action = parser.add_argument(*flags, **kwargs)
    if self.literal_value:
        if not action.option_strings or action.nargs is not None:
            raise ValueError(
                f"argument {self.name!r}: literal_value applies only to an "
                f"option that takes exactly one value"
            )
        action._duho_literal_value_ = True  # type: ignore[attr-defined]
    if isinstance(action, _argparse.BooleanOptionalAction):
        # 3.9/3.10 append " (default: %(default)s)" to any non-None help;
        # reset it so Meta(help=SUPPRESS) stays hidden and no literal
        # `%(default)s` reaches agent-help JSON.
        action.help = help
    return action

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 the strict :func:_bool_from_text table (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.

The result is checked against self.choices before it is returned, the same membership check argparse itself would apply to a CLI-supplied value.

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/_argument.py
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
def convert_layered(self, raw: object, *, source: str) -> object:
    """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
      the strict :func:`_bool_from_text` table (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`.

    The result is checked against ``self.choices`` before it is returned,
    the same membership check argparse itself would apply to a
    CLI-supplied value.

    ``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, str)):
            return _bool_from_text(raw)
        raise ValueError(f"cannot interpret {raw!r} ({source}) as a boolean")

    if self.collection is dict:
        # A *string* raw ("k=v") runs the KV factory; a TOML table converts
        # each value through the value factory (typed values widen via
        # `_convert_non_str`).
        if isinstance(raw, _ty.Mapping):
            value_factory = getattr(self.type, "value_factory", str)
            result: dict = {}
            for k, v in raw.items():
                if isinstance(v, str):
                    result[str(k)] = value_factory(v)
                else:
                    result[str(k)] = self._convert_non_str(v, value_factory)
            self._check_layered_choices(result)
            return result
        value = self._convert_single(raw)
        self._check_layered_choices(value)
        return value

    if self.collection is not None:
        extend_base = getattr(self.type, "_duho_extend_base_", None)
        if extend_base is not None:
            # duho.Extend(): `self.type` SPLITS a string, so it must not run
            # per array element. A string raw is split whole; in an array each
            # string element is split and flattened, and a non-string element
            # widens via the base element factory.
            if isinstance(raw, str):
                value = self.collection(self.type(raw))
            elif isinstance(raw, (list, tuple, set)):
                parts: list = []
                for e in raw:
                    if isinstance(e, str):
                        parts.extend(self.type(e))
                    else:
                        parts.append(self._convert_non_str(e, extend_base))
                value = self.collection(parts)
            else:
                value = self.collection([self._convert_non_str(raw, extend_base)])
        elif isinstance(raw, (list, tuple, set)):
            value = self.collection(self._convert_single(e) for e in raw)
        else:
            value = self.collection([self._convert_single(raw)])
        self._check_layered_choices(value)
        return value

    value = self._convert_single(raw)
    self._check_layered_choices(value)
    return value

ArgumentMeta

Bases: _ProtocolMeta

Metaclass backing :class:Argument's duck-typed isinstance check.

An object satisfies :class:Argument simply by having a callable _argbuilder_ -- no explicit subclassing/registration needed, so any custom type can opt in by defining that one classmethod.

__instancecheck__(instance)

Source code in src/duho/args/_argument.py
30
31
32
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) (duho.args/duho.runtime), 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 OTHER member Cli adds is sandwich-named or dunder, so a Cli subclass's field namespace stays user-owned (annotated non-underscore attrs still become CLI fields) -- with exactly ONE reserved, plain name: subcommand (the @Root.subcommand decorator). A CLI field named subcommand silently replaces the decorator instead of erroring, so avoid that one field name on a Cli subclass.

Source code in src/duho/args/_argsclass.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def __init__(self, /, **kwargs: object) -> None:
    # Seed each declared field's effective default when absent, so a direct
    # instance has the attribute surface of a parsed one (e.g. a `store_true`
    # bool). Passed kwargs win; a required field (NOT_DEFINED) stays unset.
    # Not `super().__init__(**kwargs)`: a field named `self` would collide.
    super().__init__()
    for key, value in kwargs.items():
        setattr(self, key, value)
    if "_passthrough_" not in vars(self):
        self._passthrough_ = []
    seeded: dict[str, object] = {}
    for builder in type(self)._getargs_():
        name = builder.name
        # `vars(self)`, not `hasattr`: a class-level default is `hasattr`-true
        # by inheritance, so the instance would share, and mutations would
        # leak through, the class attribute.
        if name in kwargs or name in vars(self):
            continue
        default = builder._effective_default_()
        if default is not NOT_DEFINED:
            setattr(self, name, default)
            seeded[name] = _copy.copy(default)
    # Record which fields THIS CALL passed, outside vars(self). A class that
    # is not weak-referenceable is untracked, and `duho.parse(instance)`
    # then treats every attribute as explicit.
    try:
        _key = id(self)
        _duho_explicit_instance_fields[_key] = frozenset(kwargs)
        _duho_seeded_instance_values[_key] = seeded
        _weakref.finalize(self, _duho_explicit_instance_fields.pop, _key, None)
        _weakref.finalize(self, _duho_seeded_instance_values.pop, _key, None)
    except TypeError:
        pass

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/_cmd.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
@classmethod
def subcommand(cls, child: _C) -> _C:
    """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)

ClsArgDeclaration(default, type, annotations, docstring, exprs) dataclass

annotations instance-attribute

default instance-attribute

docstring instance-attribute

exprs instance-attribute

type instance-attribute

Cmd(**kwargs)

Bases: Args

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

Args 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 an 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/_argsclass.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def __init__(self, /, **kwargs: object) -> None:
    # Seed each declared field's effective default when absent, so a direct
    # instance has the attribute surface of a parsed one (e.g. a `store_true`
    # bool). Passed kwargs win; a required field (NOT_DEFINED) stays unset.
    # Not `super().__init__(**kwargs)`: a field named `self` would collide.
    super().__init__()
    for key, value in kwargs.items():
        setattr(self, key, value)
    if "_passthrough_" not in vars(self):
        self._passthrough_ = []
    seeded: dict[str, object] = {}
    for builder in type(self)._getargs_():
        name = builder.name
        # `vars(self)`, not `hasattr`: a class-level default is `hasattr`-true
        # by inheritance, so the instance would share, and mutations would
        # leak through, the class attribute.
        if name in kwargs or name in vars(self):
            continue
        default = builder._effective_default_()
        if default is not NOT_DEFINED:
            setattr(self, name, default)
            seeded[name] = _copy.copy(default)
    # Record which fields THIS CALL passed, outside vars(self). A class that
    # is not weak-referenceable is untracked, and `duho.parse(instance)`
    # then treats every attribute as explicit.
    try:
        _key = id(self)
        _duho_explicit_instance_fields[_key] = frozenset(kwargs)
        _duho_seeded_instance_values[_key] = seeded
        _weakref.finalize(self, _duho_explicit_instance_fields.pop, _key, None)
        _weakref.finalize(self, _duho_seeded_instance_values.pop, _key, None)
    except TypeError:
        pass

__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/_cmd.py
59
60
61
62
63
64
65
66
67
68
69
70
def __call__(
    self,
) -> _ty.Any:  # 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__'"
    )

ConfigSource

Bases: NamedTuple

The config file read for a parse and where its path came from.

path instance-attribute

source instance-attribute

InitParserKwargs

Bases: TypedDict

The keywords _parser_ passes to _initparser_, every one optional.

Annotate an override's **kwargs with Unpack[InitParserKwargs] and forward them to super()._initparser_(parser, **kwargs). The base method is where they end up: it raises TypeError for a key that is not listed here and uses each key's default when it is missing.

agent_root_cls instance-attribute

explicit_prog instance-attribute

external_config instance-attribute

is_subcommand instance-attribute

parent_dests instance-attribute

Meta

Typed, typo-safe field metadata: Arg[int, Meta(help="...")].

Meta takes keyword arguments only, each one a documented metadata field; an unknown name (Meta(hlep="oops")) is a TypeError, raised as soon as the annotation is evaluated: at class-definition time on Python 3.9-3.13 with eager annotations, or at first parser build on 3.14+ (PEP 649), under string annotations, or with from __future__ import annotations. Only the fields you set are merged (each defaults to a private sentinel), so an unset field never overrides a type-derived value::

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

The kwargs field is the raw add_argument escape hatch. A plain dict is the permissive alternative: a key no Meta field and no custom argument claims is ignored with a warning.

There is deliberately no dest field: an argument's dest is always its declared field name (the parsed instance attribute), so Meta raises a dedicated TypeError for dest=.

enum_by="value" matches an Enum field against str(member.value) (command line, env and config alike) instead of the member name; the default, "name", is the rule when it is not set.

literal_value=True on an option that takes one value makes the token after it always that value, even when it looks like an option (--k --, --k -x); the default is the usual argparse rule.

split is the callable Extend sets: it turns one raw token into several values.

flags is the typed, lint-clean way to give an explicit flag tuple (equivalent to the bare ("-n", "--times") statement in the class body, which some checkers flag as an unused expression)::

times: Arg[int, Meta(flags=("-n", "--times"))] = 1

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

default = _META_UNSET class-attribute instance-attribute

enum_by = _META_UNSET class-attribute instance-attribute

env = _META_UNSET class-attribute instance-attribute

flags = _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

literal_value = _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

split = _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 merges dict occurrences, replacing any layered default on the first CLI occurrence -- the same "CLI wins" semantics _CollectionAction gives list/set/tuple fields.

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

Source code in src/duho/_fieldspec.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
def __call__(
    self,
    parser: _argparse.ArgumentParser,
    namespace: _argparse.Namespace,
    values: object,
    option_string: _ty.Optional[str] = None,
) -> None:
    sidecar = "_duho_dict_seen_" + self.dest
    if not getattr(namespace, sidecar, False):
        # First CLI occurrence of this parse: start empty so a layered
        # default is replaced, not merged onto.
        items: dict = {}
        setattr(namespace, sidecar, True)
    else:
        items = getattr(namespace, self.dest, None)
        items = dict(items) if items else {}
    if isinstance(values, (list, tuple)) and all(
        isinstance(v, _ty.Mapping) for v in values
    ):
        # ``Meta(nargs="*")`` on a dict field gives a LIST of one-pair dicts
        # (each already converted); merge them in order, since ``dict.update``
        # on the list raises. Any other update()-compatible value falls through.
        for one in values:
            items.update(one)
    else:
        # A ``None`` starting value (an explicit ``= None`` default) is
        # already normalized to ``{}`` above; a single dict occurrence
        # merges directly.
        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 to None (one scalar value per flag occurrence) regardless of the field's own declared collection kind or any ambient Meta(nargs=...), so append() always collects one scalar per occurrence instead of gathering a list of tokens per occurrence.

Source code in src/duho/args/_meta.py
313
314
315
316
317
318
319
320
321
def Append(type: Factory = str, **kw: object) -> Meta:
    """Create an append-action argument, accumulating repeated flag values.

    Explicitly clears nargs to `None` (one scalar value per flag occurrence)
    regardless of the field's own declared collection kind or any ambient
    `Meta(nargs=...)`, so `append()` always collects one scalar per occurrence
    instead of gathering a list of tokens per occurrence.
    """
    return Meta(action="append", type=type, nargs=None, **_helper_options(kw))

Choice(*choices, **kw)

Restrict an argument's accepted values to choices.

Source code in src/duho/args/_meta.py
329
330
331
def Choice(*choices: object, **kw: object) -> Meta:
    """Restrict an argument's accepted values to `choices`."""
    return Meta(choices=tuple(choices), **_helper_options(kw))

Const(value, **kw)

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

Source code in src/duho/args/_meta.py
324
325
326
def Const(value: object, **kw: object) -> Meta:
    """Create a store_const-action argument that stores `value` when present."""
    return Meta(action="store_const", const=value, **_helper_options(kw))

Count(**kw)

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

Source code in src/duho/args/_meta.py
308
309
310
def Count(**kw: object) -> Meta:
    """Create a count-action argument (e.g. `-vvv` -> 3)."""
    return Meta(action="count", **_helper_options(kw))

Extend(split, **kwargs)

Create a collection argument whose text is split on split first.

A list[str] OPTION's own default builder already takes nargs=None (one value per flag occurrence); a VARIADIC positional, or an option with an explicit Meta(nargs="*") override, instead gathers several raw tokens per occurrence. Combined with a factory that SPLITS one token into several, that shape would double-collect: argparse applies the factory to EACH gathered token individually, so a token's split result (itself a list, e.g. "a,b" -> ["a", "b"]) would be appended as ONE nested element instead of being flattened -- --rcopts '!*,build' becoming [['!*', 'build']], not ['!*', 'build']. Explicitly overriding nargs=None here (a single string per flag occurrence) avoids the double-collection regardless of what the field's own default would be.

The split parts are mapped through the field's OWN element factory (so Arg[list[int], Extend(",")] yields ints, not strings) and fed to whichever collection action the field's declared type already uses -- list, set, or tuple -- so this composes with a typed or non-list collection instead of forcing a stdlib list-only action. The field's own declared default (or the type ladder's empty collection when none is declared) is used as-is; it is NOT overridden here, so it is kept when the flag is absent and replaced -- like any other collection option -- on the first CLI occurrence.

Source code in src/duho/args/_meta.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def Extend(
    split: _ty.Union[str, _ty.Callable[[str], _ty.Iterable]], **kwargs: object
) -> Meta:
    """Create a collection argument whose text is split on ``split`` first.

    A ``list[str]`` OPTION's own default builder already takes ``nargs=None``
    (one value per flag occurrence); a VARIADIC positional, or an option with
    an explicit ``Meta(nargs="*")`` override, instead gathers several raw
    tokens per occurrence. Combined with a factory that SPLITS one token into
    several, that shape would double-collect: argparse applies the factory to
    EACH gathered token individually, so a token's split result (itself a
    list, e.g. ``"a,b"`` -> ``["a", "b"]``) would be appended as ONE nested
    element instead of being flattened -- ``--rcopts '!*,build'`` becoming
    ``[['!*', 'build']]``, not ``['!*', 'build']``. Explicitly overriding
    ``nargs=None`` here (a single string per flag occurrence) avoids the
    double-collection regardless of what the field's own default would be.

    The split parts are mapped through the field's OWN element factory (so
    ``Arg[list[int], Extend(",")]`` yields ints, not strings) and fed to
    whichever collection action the field's declared type already uses --
    ``list``, ``set``, or ``tuple`` -- so this composes with a typed
    or non-list collection instead of forcing a stdlib list-only action. The
    field's own declared default (or the type ladder's empty collection when
    none is declared) is used as-is; it is NOT overridden here, so it is kept
    when the flag is absent and replaced -- like any other collection option
    -- on the first CLI occurrence.
    """
    options = _helper_options(kwargs)
    options["kwargs"].setdefault("nargs", None)  # type: ignore[union-attr]
    if isinstance(split, str):
        splitter: _ty.Callable[[str], list] = lambda x: x.split(split)  # type: ignore
    else:

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

    return Meta(split=splitter, **options)

app_name(obj)

The name the application goes by: its prog, the same on every surface.

For an instance a parser produced (a root or a subcommand), the root parser's prog, which is app(name=...) when given. For a class, or an instance built directly, the application name of the class: its own _parsername_, else its top-level import package, else its kebab-case class name.

Source code in src/duho/args/_entry.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def app_name(obj: object) -> str:
    """The name the application goes by: its ``prog``, the same on every surface.

    For an instance a parser produced (a root or a subcommand), the root
    parser's ``prog``, which is ``app(name=...)`` when given. For a class, or an
    instance built directly, the application name of the class: its own
    ``_parsername_``, else its top-level import package, else its kebab-case
    class name.
    """
    if isinstance(obj, type):
        return _app_name(obj)
    parser = _duho_instance_last_parser_.get(id(obj))
    if parser is not None:
        return parser.prog
    return _app_name(type(obj))

command(args_cls, func, *, name=None, module=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.

module (optional): overrides the built class's __module__. type(cls_name, bases, namespace) -- with no __module__ in namespace -- otherwise makes Python fill it in from THIS function's OWN globals (duho.args), the same gotcha collections.namedtuple/ dataclasses.make_dataclass solve by resolving the CALLER's module via sys._getframe. Left as duho.args, this broke two things: (1) discovery's module-boundary filter keeps only classes whose __module__ equals the scanned module, so a command()-built class in a discovered command file was silently never registered (no warning -- it looked like "not a command" the same way a helpers-only module does); (2) _version_ = duho.AUTO derives the lookup distribution from cls.__module__, so it resolved to duho's OWN installed version instead of the app's. __qualname__ is also set to a value that can never collide with a real ClassDef in the caller's source (AST-based introspection looks classes up by __module__ + __qualname__, and could otherwise misattribute a same-named sibling class's body to this synthesized one). _duho_constants_ is seeded empty for the same reason Cmd/Cli seed it: without it, a bare command()-built class (whose __module__ now points at the CALLER's real file) would still fall through to AST-parsing that file looking for a body this synthesized class never had.

Source code in src/duho/args/_cmd.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
def command(
    args_cls: type[Args],
    func: _ty.Callable[[_ty.Any], object],
    *,
    name: _ty.Optional[str] = None,
    module: _ty.Optional[str] = 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.

    ``module`` (optional): overrides the built class's ``__module__``.
    ``type(cls_name, bases, namespace)`` -- with no ``__module__`` in
    ``namespace`` -- otherwise makes Python fill it in from THIS function's
    OWN globals (``duho.args``), the same gotcha ``collections.namedtuple``/
    ``dataclasses.make_dataclass`` solve by resolving the CALLER's module via
    ``sys._getframe``. Left as ``duho.args``, this broke two things: (1)
    discovery's module-boundary filter keeps only classes whose ``__module__``
    equals the scanned module, so a ``command()``-built class in a discovered
    command file was silently never registered (no warning -- it looked like
    "not a command" the same way a helpers-only module does); (2)
    ``_version_ = duho.AUTO`` derives the lookup distribution from
    ``cls.__module__``, so it resolved to duho's OWN installed version instead
    of the app's. ``__qualname__`` is also set to a value that can never
    collide with a real ``ClassDef`` in the caller's source (AST-based
    introspection looks classes up by ``__module__`` + ``__qualname__``, and
    could otherwise misattribute a same-named sibling class's body to this
    synthesized one). ``_duho_constants_`` is seeded empty for the same
    reason ``Cmd``/``Cli`` seed it: without it, a bare
    `command()`-built class (whose ``__module__`` now points at the CALLER's
    real file) would still fall through to AST-parsing that file looking for
    a body this synthesized class never had.
    """
    if Cmd in getattr(args_cls, "__mro__", ()):
        bases: tuple = (args_cls,)
    else:
        bases = (args_cls, Cmd)

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

    cls_name = name or getattr(args_cls, "__name__", "Command")
    namespace: dict[str, object] = {
        "__call__": __call__,
        "_duho_constants_": {},
    }
    if name is not None:
        namespace["_parsername_"] = name

    resolved_module = module
    if resolved_module is None:
        try:
            frame = _sys._getframe(1)
        except (AttributeError, ValueError):  # pragma: no cover - no _getframe
            frame = None
        resolved_module = (
            frame.f_globals.get("__name__", __package__)
            if frame is not None
            else __package__
        )
    namespace["__module__"] = resolved_module
    # Never matches a real ClassDef -- `duho.command(...)` synthesizes this
    # class, it has no body of its own for AST introspection to find.
    namespace["__qualname__"] = f"command.<locals>.{cls_name}"
    namespace.setdefault("__doc__", args_cls.__doc__)

    return _ty.cast("type[Cmd]", type(cls_name, bases, namespace))

config_source(parsed)

The config file in force for a parsed (sub)command instance, or None.

None when no file was read (none named, an empty path, an absent default file or no _config_) and for an instance that was not produced by a parse.

Source code in src/duho/_layers.py
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
def config_source(parsed: object) -> _ty.Optional[ConfigSource]:
    """The config file in force for a parsed (sub)command instance, or ``None``.

    ``None`` when no file was read (none named, an empty path, an absent default
    file or no ``_config_``) and for an instance that was not produced by a parse.
    """
    from .args._argsclass import _duho_instance_last_parser_  # lazy: circular

    # Per instance only: the per-class parser is shared and may be a later parse's.
    parser = _duho_instance_last_parser_.get(id(parsed))
    # The file is resolved on the root parser; walk up from the selected leaf.
    while parser is not None:
        path = getattr(parser, "_duho_config_path_", None)
        if path is not None:
            return ConfigSource(path, parser._duho_config_source_)
        parser = getattr(parser, "_duho_parent_parser_", None)
    return None

finish_parse(namespace)

Build the selected Cmd/Args instance from a Namespace produced by attaching a duho subparser to a PLAIN, non-duho argparse parser -- the documented "manual subparsers" recipe (Serve._parser_(subparsers, name="serve") attached to a hand-built argparse.ArgumentParser() root).

A duho root (Args._parser_'s own patched parse_known_args) does this automatically as part of normal dispatch: the internal "#cls" marker (recording which subcommand class argparse selected) is popped and turned into the real instance. A PLAIN argparse root has no such hook, so root.parse_args(...) returns a raw Namespace still carrying "#cls" -- not a Serve instance, with no _passthrough_ and no methods, and a stray "#cls" key that breaks func(**vars(args))/json.dumps(vars(args)). Call this once, right after parse_args, to get the real instance the manual recipe was always meant to produce.

Raises ValueError if namespace carries no "#cls" marker (e.g. no duho subparser was ever reached for this invocation).

Source code in src/duho/args/_entry.py
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
def finish_parse(namespace: _argparse.Namespace) -> Args:
    """Build the selected ``Cmd``/``Args`` instance from a ``Namespace``
    produced by attaching a duho subparser to a PLAIN, non-duho argparse
    parser -- the documented "manual subparsers" recipe
    (``Serve._parser_(subparsers, name="serve")`` attached to a
    hand-built ``argparse.ArgumentParser()`` root).

    A duho root (``Args._parser_``'s own patched ``parse_known_args``) does
    this automatically as part of normal dispatch: the internal ``"#cls"``
    marker (recording which subcommand class argparse selected) is popped and
    turned into the real instance. A PLAIN argparse root has no such
    hook, so ``root.parse_args(...)`` returns a raw ``Namespace`` still
    carrying ``"#cls"`` -- not a ``Serve`` instance, with no ``_passthrough_``
    and no methods, and a stray ``"#cls"`` key that breaks
    ``func(**vars(args))``/``json.dumps(vars(args))``. Call this once, right
    after ``parse_args``, to get the real instance the manual recipe was
    always meant to produce.

    Raises ``ValueError`` if `namespace` carries no ``"#cls"`` marker (e.g. no
    duho subparser was ever reached for this invocation).
    """
    ns = vars(namespace)
    if "#cls" not in ns:
        raise ValueError(
            "finish_parse: no duho command was selected -- 'namespace' has "
            "no '#cls' marker. Pass the Namespace argparse.parse_args() "
            "returned when a duho `_parser_(subparsers, ...)` subcommand was "
            "attached to a plain argparse parser."
        )
    cls = ns.pop("#cls")
    _drop_sidecars(ns)
    return cls(**ns)

main(cls, argv=None, *, setup_logging=True, config=None, utf8_stdio=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, then run the command and map a None return to 0. Logging setup runs when the resulting instance provides _set_loglevels_ directly, OR -- when the selected leaf is a plain Cmd under a LoggingArgs root -- when cls itself is a LoggingArgs (see _setup_instance_logging).

Since the 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.

Typed -> Any, not -> int: a None result maps to 0, but any OTHER value the command returns (an int, or anything else destined for sys.exit) passes straight through unchanged. Any (rather than object) keeps sys.exit(duho.main(...)) clean under a strict-mypy consumer, since sys.exit does not accept object.

UTF-8 stdio: :func:duho.utf8_stdio runs FIRST, before the MCP launch trigger and before argv is parsed, unless opted out -- utf8_stdio=False here, or cls's own _utf8_stdio_ = False when the kwarg is left at its default None. Opted out, duho does not touch stdio at all (the app may call :func:duho.utf8_stdio itself).

MCP launch trigger: checked next, before argv is even parsed -- see :func:_maybe_serve_mcp_trigger. When the trigger fires this returns the MCP server's own exit code instead of running any command; otherwise nothing about the rest of this function changes.

Opt-in MCP subcommand (cls's own _mcp_command_, mirroring duho.app(..., mcp_command=...) -- there is no separate kwarg here, since main has no other command-source parameters to sit next to). False (the default): unchanged behavior, and duho.mcp is never imported. Otherwise a fresh duho.mcp.McpCmd subclass is registered as an extra top-level subcommand under the resolved name, going through the exact same resolution/validation (:func:duho.runtime._resolve_mcp_command_name) and collision/leaf checks (:func:duho.runtime._build_mcp_command_class) app() uses, so the ValueError messages match. Running <prog> <name> serves cls's own static tree (excluding that subcommand itself) over stdio via :func:duho.mcp.serve_running_app, through the same _MCP_CONTEXT ContextVar app() sets.

Source code in src/duho/args/_entry.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
def main(
    cls: type[Args],
    argv: _ty.Optional[_ty.Sequence[str]] = None,
    *,
    setup_logging: bool = True,
    config: _ty.Optional[_ty.Union[str, _pathlib.Path]] = None,
    utf8_stdio: _ty.Optional[bool] = None,
) -> _ty.Any:
    """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,
    then run the command and map a None return to 0. Logging setup runs when
    the resulting instance provides `_set_loglevels_` directly, OR -- when the
    selected leaf is a plain `Cmd` under a `LoggingArgs` root -- when `cls`
    itself is a `LoggingArgs` (see `_setup_instance_logging`).

    Since the 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.

    Typed ``-> Any``, not ``-> int``: a ``None`` result maps to ``0``, but
    any OTHER value the command returns (an ``int``, or anything else destined
    for ``sys.exit``) passes straight through unchanged. ``Any`` (rather than
    ``object``) keeps ``sys.exit(duho.main(...))`` clean under a strict-mypy
    consumer, since ``sys.exit`` does not accept ``object``.

    **UTF-8 stdio**: :func:`duho.utf8_stdio` runs FIRST, before the MCP
    launch trigger and before ``argv`` is parsed, unless opted out --
    ``utf8_stdio=False`` here, or ``cls``'s own ``_utf8_stdio_ = False``
    when the kwarg is left at its default ``None``. Opted out, duho does not
    touch stdio at all (the app may call :func:`duho.utf8_stdio` itself).

    **MCP launch trigger**: checked next, before ``argv``
    is even parsed -- see :func:`_maybe_serve_mcp_trigger`. When the trigger
    fires this returns the MCP server's own exit code instead of running any
    command; otherwise nothing about the rest of this function changes.

    **Opt-in MCP subcommand** (``cls``'s own ``_mcp_command_``, mirroring
    ``duho.app(..., mcp_command=...)`` -- there is no separate kwarg here,
    since ``main`` has no other command-source parameters to sit next to).
    ``False`` (the default): unchanged behavior, and ``duho.mcp`` is never
    imported. Otherwise a fresh ``duho.mcp.McpCmd`` subclass is registered
    as an extra top-level subcommand under the resolved name, going through
    the exact same resolution/validation
    (:func:`duho.runtime._resolve_mcp_command_name`) and collision/leaf
    checks (:func:`duho.runtime._build_mcp_command_class`) ``app()`` uses,
    so the ``ValueError`` messages match. Running ``<prog> <name>`` serves
    ``cls``'s own static tree (excluding that subcommand itself) over
    stdio via :func:`duho.mcp.serve_running_app`, through the same
    ``_MCP_CONTEXT`` ContextVar ``app()`` sets.
    """
    _token = _duho_logging._enter_traceback_env(cls)
    try:
        if utf8_stdio if utf8_stdio is not None else getattr(cls, "_utf8_stdio_", True):
            _compat.utf8_stdio()

        served = _maybe_serve_mcp_trigger(cls)
        if served is not None:
            return served

        config_source = None
        if config is None:
            try:
                config, config_source = _class_config_location(cls, argv)
            except Exception as exc:
                _end_if_reported(exc, _app_name(cls), cls)
                raise

        root_cls = cls
        extra_cmds: list[type] = []
        completion_cls = None
        if (
            getattr(cls, "_mcp_command_", False) is not False
            or getattr(cls, "_completion_command_", False) is not False
        ):
            # Lazy: `runtime` (and `mcp`/`completion` through it) is imported only
            # for a value other than the literal `False`; `is not False`, not
            # truthiness, so an empty string still reaches the builders' validation.
            from .. import runtime as _runtime

            has_other = bool(getattr(cls, "_subcommands_", None))
            mcp_cls = _runtime._build_mcp_command_class(
                cls,
                None,
                _runtime._existing_command_names(cls, ()),
                has_other_subcommand=has_other,
            )
            if mcp_cls is not None:
                extra_cmds.append(mcp_cls)
            completion_cls = _runtime._build_completion_command_class(
                cls,
                _runtime._existing_command_names(cls, ())
                | {c._parsername_ for c in extra_cmds},
                has_other_subcommand=has_other,
            )
            if completion_cls is not None:
                extra_cmds.append(completion_cls)
        if extra_cmds:
            # A fresh per-call subclass carrying the extra subcommands, never
            # mutating `cls` (it would leak across calls/threads). `_MCP_CONTEXT`
            # below holds the ORIGINAL `cls`, whose tree has no injected commands.
            extra_attrs: dict[str, object] = {
                "__module__": cls.__module__,
                "__qualname__": cls.__qualname__,
                "_subcommands_": list(getattr(cls, "_subcommands_", None) or ())
                + extra_cmds,
                "_duho_constants_": {},
                "__doc__": cls.__doc__,
            }
            own_parsername = vars(cls).get("_parsername_")
            if own_parsername is not None:
                extra_attrs["_parsername_"] = own_parsername
            root_cls = type(cls.__name__, (cls,), extra_attrs)
            if completion_cls is not None:
                # The script describes the tree including its own subcommand.
                completion_cls._completion_tree_ = root_cls

        parser = root_cls._parser_(_inherited_config_hint_=config is not None)
        _apply_layers(parser, root_cls, config=config, config_source=config_source)
        try:
            instance = parser.parse_args(argv)
        except SystemExit as exc:
            _mark_parser_exit(exc)
            raise

        _setup_instance_logging(instance, setup_logging, cls)

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

        # Recorded so `duho.mcp.serve_running_app` can serve THIS class tree from
        # within a dispatched command.
        token = _compat._MCP_CONTEXT.set(("class", cls))
        try:
            result = _guard_dispatch(
                lambda: _maybe_await(run()), parser.prog, cls, instance
            )
        finally:
            _compat._MCP_CONTEXT.reset(token)
        return 0 if result is None else result
    finally:
        _duho_logging._exit_traceback_env(_token)

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 field values the caller EXPLICITLY passed to its __init__ (not a placeholder Args.__init__ itself seeded for an omitted field -- a bare bool/collection field left at its own default is not "an instance value" just because it materializes onto vars(spec)) are used as argparse defaults. 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. duho.value_sources reports such a field as "instance".

With subcommands the instance returned is the SELECTED LEAF command's, not an instance of spec's own class: it is typed as spec's class, but a chosen subcommand's class need not derive from it.

Source code in src/duho/args/_entry.py
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
def parse(
    spec: _ty.Union[type[_A], _A],
    argv: _ty.Optional[_ty.Sequence[str]] = None,
    *,
    parser_kwargs: _ty.Optional[_ty.Mapping[str, object]] = None,
    config: _ty.Optional[_ty.Union[str, _pathlib.Path]] = None,
) -> _A:
    """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 field values the caller EXPLICITLY
      passed to its `__init__` (not a placeholder `Args.__init__` itself seeded
      for an omitted field -- a bare `bool`/collection field left at its own
      default is not "an instance value" just because it materializes onto
      `vars(spec)`) are used as argparse defaults. 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.
    `duho.value_sources` reports such a field as ``"instance"``.

    With subcommands the instance returned is the SELECTED LEAF command's, not
    an instance of `spec`'s own class: it is typed as `spec`'s class, but a
    chosen subcommand's class need not derive from it.
    """
    parser_kwargs = parser_kwargs or {}
    config_source = None
    if config is None:
        config, config_source = _class_config_location(
            spec if isinstance(spec, type) else type(spec), argv
        )
    if isinstance(spec, type):
        cls = spec
        parser = cls._parser_(
            **parser_kwargs, _inherited_config_hint_=config is not None
        )
        _apply_layers(parser, cls, config=config, config_source=config_source)
        return parser.parse_args(argv)

    cls = type(spec)
    parser = cls._parser_(**parser_kwargs, _inherited_config_hint_=config is not None)
    _apply_layers(
        parser, cls, config=config, instance=spec, config_source=config_source
    )
    return parser.parse_args(argv)

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

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

Builds cls's root parser (cls._parser_(**parser_kwargs)), applies the same env/config/class-default layers duho.main/duho.parse do (config overrides cls._config_; precedence CLI > env > config > class default), and parses argv with help/version/print- completion suppressed and subcommand descent skipped entirely, so a consumer can resolve config-file-driven command search paths (or any other global, including one backed by Meta(env=...) or only made non-required by a layer) 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 subparser-detach/terminal-action patching it 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/_entry.py
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
def parse_globals(
    cls: type[_A],
    argv: _ty.Optional[_ty.Sequence[str]] = None,
    *,
    config: _ty.Optional[_ty.Union[str, _pathlib.Path]] = None,
    **parser_kwargs: object,
) -> _A:
    """Parse ONLY a root command's global args, ignoring/relaxing subcommands.

    Builds ``cls``'s root parser (``cls._parser_(**parser_kwargs)``), applies
    the same env/config/class-default layers ``duho.main``/``duho.parse`` do
    (``config`` overrides ``cls._config_``; precedence CLI > env > config >
    class default), and parses ``argv`` with help/version/print-
    completion suppressed and subcommand descent skipped entirely, so a
    consumer can resolve config-file-driven command search paths (or any
    other global, including one backed by ``Meta(env=...)`` or only made
    non-required by a layer) 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
    subparser-detach/terminal-action patching it 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

    config_source = None
    if config is None:
        config, config_source = _class_config_location(cls, argv)
    parser = cls._parser_(**parser_kwargs, _inherited_config_hint_=config is not None)
    # Commands `app` registers later are not in this parser, so their tables
    # would read as unknown keys: the full parse judges the file.
    _apply_layers(
        parser, cls, config=config, validate=False, config_source=config_source
    )
    # A static tree's real parse hands everything after the subcommand name to
    # the subcommand, so a root option written there is not a global.
    argv = _argv_before_subcommand(
        parser, list(_sys.argv[1:] if argv is None else argv)
    )
    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/_actions.py
184
185
186
187
188
189
190
191
192
193
194
195
def print_agent_help(cls, file: _ty.Optional[_ty.TextIO] = 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, *, prog=None)

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

shell is one of "bash", "zsh", "fish", or "powershell" -- an unrecognized name raises a clear ValueError listing the valid ones, instead of an AttributeError about duho.completion's internals -- or, worse, silently calling one of that module's PRIVATE helpers as if it were an emitter. 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>.

prog overrides the command name the emitted script binds to; by default it is the root parser's prog, the application's name (see :func:_app_name), however the program was launched.

Source code in src/duho/args/_actions.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def print_completion(
    cls,
    shell: str,
    file: _ty.Optional[_ty.TextIO] = None,
    *,
    prog: _ty.Optional[str] = None,
) -> None:
    """Print a shell completion script for `cls` to `file` (default sys.stdout).

    ``shell`` is one of ``"bash"``, ``"zsh"``, ``"fish"``, or ``"powershell"``
    -- an unrecognized name raises a clear ``ValueError`` listing the
    valid ones, instead of an ``AttributeError`` about ``duho.completion``'s
    internals -- or, worse, silently calling one of that module's PRIVATE
    helpers as if it were an emitter. 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>`.

    ``prog`` overrides the command name the emitted script binds to; by
    default it is the root parser's ``prog``, the application's name (see
    :func:`_app_name`), however the program was launched.
    """
    from .. import completion as _completion

    if shell not in _COMPLETION_SHELLS:
        raise ValueError(
            f"unknown shell {shell!r}; choose from " f"{', '.join(_COMPLETION_SHELLS)}"
        )
    parser = cls._parser_()
    emitter = getattr(_completion, shell)
    if prog is None:
        prog = parser.prog
    _compat.write_machine(emitter(parser, prog=prog), file)

subcommand(parent)

Decorator factory: register the decorated class as a subcommand of parent.

parent is any Cmd (or Cli) class acting as a group::

@duho.subcommand(Tools)
class Build(duho.Cmd):
    ...

The decorated class is returned unchanged. Equivalent to parent._register_subcmd_(Build); registering twice is a no-op.

Source code in src/duho/args/_cmd.py
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def subcommand(parent: type[Cmd]) -> _ty.Callable[[_C], _C]:
    """Decorator factory: register the decorated class as a subcommand of ``parent``.

    ``parent`` is any ``Cmd`` (or ``Cli``) class acting as a group::

        @duho.subcommand(Tools)
        class Build(duho.Cmd):
            ...

    The decorated class is returned unchanged. Equivalent to
    ``parent._register_subcmd_(Build)``; registering twice is a no-op.
    """

    def register(child: _C) -> _C:
        return parent._register_subcmd_(child)

    return register

value_sources(parsed)

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

Looks up the owning parser via the per-INSTANCE _duho_instance_last_parser_ linkage stashed during dispatch (see _initparser_) first -- falling back to the per-class _duho_last_parser_ one, read through type(parsed).__dict__ rather than getattr -- getattr follows the MRO, so a never-parsed SUBCLASS of an already-parsed base (or of bare Args, parsed every time duho.app runs without a root) would otherwise inherit its base's stale parser and report bogus sources instead of the documented {}. The per-instance lookup matters because the per-class one is shared by EVERY instance of that class: without it, parsing the same class a second time (a different config file, say) would silently change what an OLDER, already-returned instance reports here too. Returns {} when unavailable (e.g. the instance wasn't produced via a parser built by this framework, or no parse has happened yet for its class).

For a SUBCOMMAND instance, root/global fields are included too: _merge_layers_upward folds each actually-selected parser's own provenance/builders up into its parent's as dispatch unwinds, so by the time the root's own _duho_last_parser_ linkage is read here it already holds the whole selected chain, not just the deepest class's own fields.

A field is "cli" if its parsed value differs from the effective default that was in effect for that parse -- the merged env/config/instance 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"/"instance"), or "default" if no layer touched it (value == the untouched class default).

Source code in src/duho/_layers.py
 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
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
def value_sources(parsed: object) -> dict[str, str]:
    """Report the origin layer ("cli", "env", "config", "instance", or
    "default") of each field on a parsed instance produced by
    `duho.parse`/`duho.main`.

    Looks up the owning parser via the per-INSTANCE
    `_duho_instance_last_parser_` linkage stashed during dispatch (see
    `_initparser_`) first -- falling back to the per-class
    `_duho_last_parser_` one, read through ``type(parsed).__dict__`` rather
    than ``getattr`` -- ``getattr`` follows the MRO, so a never-parsed
    SUBCLASS of an already-parsed base (or of bare ``Args``, parsed every
    time `duho.app` runs without a root) would otherwise inherit its base's
    stale parser and report bogus sources instead of the documented ``{}``.
    The per-instance lookup matters because the per-class one is shared by
    EVERY instance of that class: without it, parsing the same class a
    second time (a different config file, say) would silently change what an
    OLDER, already-returned instance reports here too. Returns `{}` when
    unavailable (e.g. the instance wasn't produced via a parser built by this
    framework, or no parse has happened yet for its class).

    For a SUBCOMMAND instance, root/global fields are included too:
    `_merge_layers_upward` folds each actually-selected parser's own
    provenance/builders up into its parent's as dispatch unwinds, so by the
    time the root's own `_duho_last_parser_` linkage is read here it already
    holds the whole selected chain, not just the deepest class's own fields.

    A field is "cli" if its parsed value differs from the effective default
    that was in effect for that parse -- the merged env/config/instance 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"/"instance"), or "default" if no layer touched it (value
    == the untouched class default).
    """
    from .args._argsclass import _duho_instance_last_parser_  # lazy: avoids a circular

    # import (duho.args re-exports this module's own public names).
    parser = _duho_instance_last_parser_.get(id(parsed))
    if parser is None:
        parser = type(parsed).__dict__.get("_duho_last_parser_")
    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 {}
    builders: dict[str, object] = dict(getattr(parser, "_duho_builders_", None) or {})
    builders.update({b.name: b for b in type(parsed)._getargs_()})

    result: dict[str, str] = {}
    for name, builder in builders.items():
        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".
            effective_default = builder._effective_default_()
            layer = "default"
        result[name] = layer if value == effective_default else "cli"
    return result

duho.parser(cls, *args, **kwargs)

Build an ArgumentParser for an Args class.

Public module-level entry point (delegates to cls._parser_, matching its own _Parser[Self] return typing so duho.parser(MyApp) keeps MyApp's type instead of widening to Any). With subcommands, the instance parse_args returns is the SELECTED LEAF command's, not a MyApp: it is typed as MyApp, but a chosen subcommand's class need not derive from it.

Source code in src/duho/__init__.py
87
88
89
90
91
92
93
94
95
96
97
def parser(cls: type[_A], *args: object, **kwargs: object) -> _Parser[_A]:
    """Build an ArgumentParser for an Args class.

    Public module-level entry point (delegates to ``cls._parser_``, matching
    its own ``_Parser[Self]`` return typing so ``duho.parser(MyApp)`` keeps
    ``MyApp``'s type instead of widening to ``Any``). With subcommands, the
    instance ``parse_args`` returns is the SELECTED LEAF command's, not a
    ``MyApp``: it is typed as ``MyApp``, but a chosen subcommand's class need
    not derive from it.
    """
    return cls._parser_(*args, **kwargs)