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', 'Const', 'Count', 'Extend', 'Factory', 'main', 'NS', 'NOT_DEFINED', 'parse', 'print_completion', 'UpdateAction', 'value_sources'] module-attribute

Args

Bases: Namespace

Argument

Bases: Protocol

from_type(factory, **kwargs) classmethod

Source code in src/duho/args.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
@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

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
557
558
559
560
561
562
563
564
565
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(),
    )

ArgumentMeta

Bases: _ProtocolMeta

__instancecheck__(instance)

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

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
779
780
781
782
783
784
785
def __call__(  # type:ignore
    self, parser, namespace, values: dict, option_string=None
):
    items: dict = getattr(namespace, self.dest, {})
    items = _copy.deepcopy(items)
    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
757
758
759
760
761
762
763
764
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
772
773
774
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
767
768
769
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
752
753
754
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.

Source code in src/duho/args.py
736
737
738
739
740
741
742
743
744
745
746
747
748
749
def Extend(split: "str | _ty.Callable[[str], _ty.Iterable]", **kwargs):
    """Create an extend-action argument with optional string splitting."""
    kwargs.setdefault("default", [])
    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)

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

Build a parser for cls, parse argv, and dispatch to instance.call().

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 call instance() (i.e. instance.call()) and map a None return to 0.

Source code in src/duho/args.py
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
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 to instance.__call__().

    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 call
    instance() (i.e. instance.__call__()) and map a None return to 0.
    """
    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__} does not implement __call__"
        )

    result = 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
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
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)

print_completion(cls, shell, file=None)

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

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
788
789
790
791
792
793
794
795
796
797
798
799
800
801
def print_completion(cls, shell: str, file=None) -> None:
    """Print a shell completion script for `cls` to `file` (default sys.stdout).

    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
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
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:
            effective_default = builder.default
            layer = "default"
        result[name] = layer if value == effective_default else "cli"
    return result