# Arguments Similar to commands, use of cappa-specific argument annotations are "only" required when the default behavior is not what you want. A completely un-annotated dataclass like ```python @dataclass class Example: name: str ``` is perfectly valid. However, a CLI of any real complexity **will** need to use the `Arg` annotation. By default: - Each class member is inferred to be a positional argument - Its name is the dash-case version of the class member name - Its value is coerced to the annotated type - Its help text is inferred from various natural sources (See [Help Text Inference](./help.md) for details) However, by using `Arg`, with syntax like `name: Annotated[str, cappa.Arg(...)]`, you can customize the behavior of the field. For each field of customization, there is a default inferred value, based on the particulars of the dataclass and/or other configured fields. Below we document when configuring a given field might **imply** some other field by default (e.g. `count=True` **implies** `num_args=0`), however in all cases, an explicitly provided value will take precedence over the default inference. ```{note} See the [annotation docs](./annotation.md) for more details on how annotations can be used and are interpreted to handle different kinds of CLI input. ``` (arg-value-name)= ## `Arg.value_name` This field controls the name of the field in helptext/errors. By default it matches the dataclass' field name. ```python @dataclass class Command: foo: Annotated[int, cappa.Arg(value_name="bar")] happy: Annotated[int, cappa.Arg(value_name="sad", long=True)] = 1 ``` Would yield help text like: ``` Options [--happy SAD] Arguments BAR ``` (arg-short)= ## `Arg.short` By default, unannotated dataclass fields are assumed to be positional arguments. An `Arg.short` argument is a non-positional argument which can be selected by a single `-` followed by the short name. The value can be supplied in a few different ways: * `foo: Annotated[int, Arg(short=True)]`: Rendered as `-f`. If there are more than one field starting with "f" with `short=True`, at least one will need to be disambiguated. * `foo: Annotated[int, Arg(short='-b')]`: Rendered as-is * `foo: Annotated[int, Arg(short='b')]`: Rendered as `-b`. * `foo: Annotated[int, Arg(short=['-b', '-f'])]`: Rendered as **both/either** `-b` and `-f`. * `foo: Annotated[int, Arg(short='-b/-f')]`: Rendered as **both/either** `-b` and `-f`. (arg-long)= ## `Arg.long` By default, unannotated dataclass fields are assumed to be positional arguments. An `Arg.long` argument is a non-positional argument which can be selected by a single `--` followed by the long name. ```{note} `snake_case` field names will be converted to `dash-case` by default. ``` The value can be supplied in a few different ways: * `foo: Annotated[int, Arg(long=True)]`: Rendered as `--foo`. field starting with "f" with `long=True`, at least one will need to be disambiguated. * `foo: Annotated[int, Arg(long='--bar')]`: Rendered as-is * `foo: Annotated[int, Arg(long='bar')]`: Rendered as `--bar`. * `foo: Annotated[int, Arg(long=['--bar', '-foo'])]`: Rendered as **both/either** `--bar` and `--foo`. * `foo: Annotated[int, Arg(long='--bar/--foo')]`: Rendered as **both/either** `--bar` and `--foo`. (arg-count)= ## `Arg.count` Counts instances of the argument in question. This implies `num_args=0` and `action=ArgAction.count` by default. The canonical example of this is counting verbosity flags. ```python @dataclass class Example: verbose: Annotated[int, cappa.Arg(short="-v", count=True), ``` (arg-help)= ## `Arg.help` Controls the per-argument help text. See [Help](./help.md) for more details, but in short help text is inferred by default from a few different sources, including docstrings and "attribute docstrings". (arg-action)= ## `Arg.action` Obliquely referenced through other `Arg` options like `count`, every `Arg` has a corresponding "action". The action is automatically inferred, most of the time, based on other options (i.e. `count`), the annotated type (i.e. `bool` -> `ArgAction.store_true/ArgAction.store_false`, `list` -> `ArgAction.append`), etc. However the inferred action can always be directly set, in order to override the default inferred behavior. ### Custom Actions ```{note} This feature is currently experimental, in particular because the parser state available to either backend's callable is radically different. However, for an action callable which accepts no arguments, the behavior is unlikely to change. ``` In addition to one of the literal `ArgAction` variants, the provided action can be given as an arbitrary callable. The callable will be called as the parser "action" in response to parsing that argument. ```{note} Custom actions may interfere with general inference features, depending on what you're doing (given that you're taking over the parser's duty of determining how the code ought to handle the argument in question). As such, you may need to specify options like `num_args`, where you wouldn't have otherwise needed to. ``` Similarly to the [invoke](./invoke.md) system, you can use the type system to automatically inject objects of supported types from the parse context into the function in question. The return value of the function will be used as the result of parsing that particular argument. The set of available objects to inject include: - [Command](cappa.Command): The command currently being parsed (a relevant piece of context when using subcommands) - [Arg](cappa.Arg): The argument being parsed. - [Value](cappa.parser.Value): The raw input value parsed in the context of the argument. Depending on other settings, this may be a list (when num_args > 1), or a raw value otherwise. - [RawOption](cappa.parser.RawOption): In the event the value in question corresponds to an option value, the representation of that option from the original input. The above set of objects is of potentially limited value. More parser state will likely be exposed through this interface in the future. If you think some specific bit of parser state is missing and could be useful to you, please raise an issue! For example: ```python def example(): return 'foo!' def example(arg: Arg): return 'foo!' def example2(value: Value): return value.value[1:] @dataclass class Example: ex1: Annotated[str, cappa.Arg(action=example) ex2: Annotated[str, cappa.Arg(action=example2) ex3: Annotated[str, cappa.Arg(action=example3) ``` ```{eval-rst} .. autoapiclass:: cappa.ArgAction :noindex: ``` ```{warning} `ArgAction.append` and `num_args=-1` can be potentially confused, in that they can both produce list/sequence types as outputs. They can be used in conjunction or separately, but they are not the same! Given some example option `--foo`: `ArgAction` generally, affects the way the parser maps input CLI values to a given field overall. As such `ArgAction.append` causes the field-level value to be a sequence, and for multiple instances of `--foo` to accumulate into a list. e.x. `--foo 1 --foo 2` -> `[1, 2]`. `num_args` instead, affects the number of values that a single instance of `--foo` will consume before stopping. e.x. `--foo 1 2` -> `[1, 2]`. From an input perspective, the above examples show how they present differently at the CLI interface, requiring different inputs to successfully parse. From an output perspective, the table below shows how their parsed output will end up looking when mapped to real values. | action | num_args | result | | ------ | -------- | ------ | | set | 1 | 1 | | set | -1 | [1] | | append | 1 | [1] | | append | -1 | [[1]] | ``` ## `Arg.num_args` `num_args` controls the number of arguments that the parser will consume in order to fulfill a specific field. `num_args=1` is the default behavior, meaning only one argument/value will be consumed for that field. This yields a scalar-type value. `num_args=3` would therefore mean that exactly 3 values would be required, resulting in a sequence-type output, rather than a scalar one. `num_args=-1` can be used to indicate that 0 or more (i.e. unbounded number of) arguments will be consumed, similarly resulting in a sequence-type output (even in the zero case). ```{note} Generally `num_args` will be automatically inferred by your type annotation. For example, `tuple[int, int, int]` implies `num_args=3`. However, an explicitly provided value is always preferred to the inferred value. See [annotations](./annotation.md) for more details. ``` ## `Arg.default` Controls the default argument value at the CLI level. Generally, you can avoid direct use of cappa's default by simply using the source class' native default mechanism. (i.e. `foo: int = 0`, `foo: int = field(default=0)`, or `foo: list[str] = field(default_factory=list)` for dataclasses). However it **can** be convenient to use cappa's default because it does not affect whether the underlying class definition makes that field required in the class' constructor. ```{note} The `default` value is not **typically** parsed by the given `parse` function. That is to say, if no value is selected at the CLI and the default value is used instead; it will not be coerced into the annotated type automatically. (`Env`, `Prompt`, and `Confirm` are exceptions to this rule, explained in their sections below). The reason for this is twofold: 1. Typechecking should already emit an error when the given default is of the incorrect type. 2. This interacts poorly with types who's constructor does not accept an instance of the given type as an input argument. For example, `foo: Foo = Foo('')` would infer `parse=Foo` and attempt to pass `Foo(Foo(''))` during parsing. ``` Additionally there are a number of natively integrated objects that can be used as default to create more complex behaviors given a **missing** CLI argument. The below objects will not be evaluated unless the user did not supply a value for the argument it's attached to. - [cappa.Default](cappa.Default) - [cappa.Env](cappa.Env) - [cappa.Prompt](cappa.Prompt)/[cappa.Confirm](cappa.Confirm) - [cappa.ValueFrom](cappa.ValueFrom) ### `Default` All other types of default are ultimately shorthand forms of `Default`. The `Default` construct allows for specifying an ordered chain of default fallback options, with an optional static fallback item at the end. For example: - `foo: int = 4` is the same as `default=Default(default=4)`. This unconditionally defaults to 4. - `foo: Annotated[int, Arg(default=Env("FOO"))] = 4` is the same as `Arg(default=Default(Env("FOO"), default=4)`. This attempts to read the environment variable `FOO`, and falls back to 4 if the env var is unset. - `foo: Annotated[int, Arg(default=Env("FOO") | Prompt("Gimme"))]` is the same as `Arg(default=Default(Env("FOO"), Prompt("Gimme"))`. This attempts to read the environment variable `FOO`, followed by a user prompt if the env var is unset. As shown above, any combination of defaultable values can be used as fallbacks of one another by using the `|` operator to chain them together. As noted above, a value produced by `Default.default` **does not** invoke the `Arg.parse` parser. This is for similar reasons as to native dataclass defaults. The programmer is supplying the default value which should not **need** to be parsed. ```{note} `Default`'s fallback behavior will fallback to lower priority handlers in the "default sequence" when that item's default returns `cappa.Empty`. All the native (`Env`, `Prompt`, `Confirm`) handlers will do this natively, when they fail to produce a value. A `ValueFrom` (described below) accepts a user-provided function, so it **may** need to be aware of this and return `cappa.Empty`, depending on the desired behavior. ``` ### `Env` [cappa.Env](cappa.Env) performs environment variable lookups in an attempt to provide a value to the class field. `Arg(..., default=Env("FOO", default='default value'))`, cappa will attempt to look up the environment variable `FOO` for the default value, if there was no supplied value at the CLI level. As noted above, a value produced by `Env` **does** invoke the `Arg.parse` parser. This is because `Env` values will always be returned as a string, very similarly to a normal pre-parse CLI value. ### `Prompt`/`Confirm` [cappa.Prompt](cappa.Prompt) and [cappa.Confirm](cappa.Confirm) can be used to ask for user input to fulfill the value. ```{note} `rich.prompt.Prompt` and `rich.prompt.Confirm` can also be used transparently for the same purpose. ``` ```python import cappa class Example: value: Annotated[int, cappa.Arg(default=cappa.Prompt("A number value"))] is_ok: Annotated[bool, cappa.Arg(default=cappa.Confirm("You sure?"))] ``` As noted above, a value produced by `Prompt`/`Confirm` **does** invoke the `Arg.parse` parser. This is because these values will always be returned as a string, very similarly to a normal pre-parse CLI value. ### `ValueFrom` [cappa.ValueFrom](cappa.ValueFrom) is a means for calling an arbitrary function at mapping time, to allow for dynamic default values. ```{note} A dataclass `field(default_factory=list)` is internally the same thing as `default=ValueFrom(list)`! ``` ```python from pathlib import Path import cappa def load_default(key): config = json.loads(Path("config.json").read_text()) return config[key] class Example: value: Annotated[int, cappa.Arg(default=cappa.ValueFrom(load_default, key='value'))] ``` This construct is able to be automatically supplied with [cappa.State](State), in the event shared parse state is required to evaluate a field's default. As noted above, a value produced by `ValueFrom` **does not** invoke the `Arg.parse` parser. This is because the called function is programmer-supplied and can/should just return the correct end value. ```{note} If there are scenarios where `ValueFrom` **should** fail to provide a value and fall back to the class-level, or `Default`-level default value, it should return `cappa.Empty` in order to indicate to cappa that it shouldn't accept the returned value as the **actual** value to used when constructing the resulting class instance. ``` ## `Arg.show_default` Defaults to `True` (e.g. `DefaultFormatter(format='{default}', show=True)`). This field controls **both**: * Whether to render the argument's default for helptext * The formatting for the default value itself. `show_default` can be provided as any of: * `bool`: Implies the `DefaultFormatter.show` value (e.g. `DefaultFormatter(show=)`). * `str`: Implies the `DefaultFormatter.format` value (e.g. `DefaultFormatter(format=, show=True)`). * `DefaultFormatter`: Is taken as-is. `DefaultFormatter.show` enables/disables the display of the given `Arg.default`. While the literal default value is formatted through `DefaultFormatter.format`. ```{note} The `str.format` context is provided as named `default` format arg. That is to say, the literal default value can be templated into the string through the named `{default}` syntax. Normal format specifiers can be used to control formatting of the value, for example `{default:.2f}` to limit the precision of a default float value. ``` ```{note} The `format` value **can** simply be a static string, to avoid taking the literal default value. An reasonable example of this might be `source: Annotated[BinaryIO, Arg(show_default='')] = '-'`. ``` (arg-group)= ## `Arg.group`: Groups (and Mutual Exclusion) `Arg(group=...)` can be used to customize the way arguments/options are grouped together and interpreted. The `group` argument can be any of: - `str`: A string that indicates a custom group. This only affects help text output, by placing any `Arg`s with that group string under a common heading. ```python class Example: arg1: Annotated[str, Arg(group='Special')] arg2: Annotated[str, Arg(group='Special')] ``` - `tuple[int, str]`: This additionally provides an `int` which is considered when determining the `order` of group output. ```python class Example: arg1: Annotated[str, Arg(group=(4, 'Special'))] arg2: Annotated[str, Arg(group=(4, 'Special'))] ``` - [Group](Group): Instances of `Group` are the normalized form of the above two shorthand options (which are ultimately coerced into `Group`s). [Group](Group) additionally has an `exclusive: bool` option, which can be used to indicate that options within a group are mutually exclusive to one other. ```{note} Both `order` and `exclusive` options on `Group` are expected (and validated) to be identical across all options sharing that group string. ``` ```python class Example: arg1: Annotated[str, Arg(group=Group('Special', exclusive=True))] arg2: Annotated[str, Arg(group=Group('Special', exclusive=True))] ``` In this case, `example --arg1 foo --arg2` would generate an error like: ``` Argument 'arg1' is not allowed with argument 'arg2' ``` (exclusive-group-syntax)= ### Dedicated Mutual Exclusion Syntax A potentially common use of mutually exclusive arguments would be two distinct CLI-level arguments, which ultimately are different ways of configuring a specific code-level field. In such cases, two (or more) `cappa.Arg`s can be annotated on a single class field. This **implies** a mutually exclusive group automatically, defaulting to the name of the field ("Verbose" in this case) as the group name. For example: ```python @dataclass class Example: verbose: Annotated[ int, cappa.Arg(short="-v", action=cappa.ArgAction.count), cappa.Arg(long="--verbosity"), ] = 0 ``` - `example -vvv` would yield: `verbose=3` - `example --verbosity 4` would yield: `verbose=4` - `example -vvv --verbosity 4` would yield the error: `Argument '--verbosity' is not allowed with argument '-v'` ```{note} An explicit `group=` can still be used in concert with the above syntax to control the `order` and name of the resultant group. ``` (arg-parse)= ## `Arg.parse` `Arg.parse` can be used to provide **specific** instruction to cappa as to how to handle the raw value given from the CLI parser backend. In _general_, this argument shouldn't need to be specified because the annotated type will generally _imply_ how that particular value ought to be parsed, especially for built in python types. However, there will inevitably be cases where the type itself is not enough to infer the specific parsing required. Take for example: ```python from datetime import date @dataclass class Example: iso_date: date american_date: Annotated[date, cappa.Arg(parse=lambda date_str: date.strptime('%d/%m/%y'))] ``` Cappa's default date parsing assumes an input isoformat string. However you might instead want a specific alternate parsing behavior; and `parse=` is how that is achieved. Further, this is likely more useful for parsing any custom classes which dont have simple, single-string-input constructor arguments. ```{note} Note cappa itself contains a number of component `parse_*` functions inside the `parse` module, which can be used in combination with your own custom `parse` functions. ``` ### `default_parse`/`Arg.parse_inference A function [cappa.default_parse](cappa.default_parse) is exposed and **can** be directly referenced in parse function sequences. However by default the `default_parse` function is applied automatically at the end of the user-provided parse function/sequence. With that said, if the default behavior is undesirable, one can set `Arg(parse_inference=False)` to **only** execute the user provided parser function(s). (parsing-json)= ### Parsing JSON Another example of a potentially useful parsing concept could be to parse json string input. For example: ```python import json from dataclasses import dataclass from typing import Annotated import cappa @dataclass class Example: todo: Annotated[dict[str, int], cappa.Arg(parse=json.loads)] todo = cappa.parse(Todo) print(todo) ``` Natively (at present), cappa doesn't have any specific `dict` type inference because it's ambiguous what CLI input shape that ought to map to. However, by combining that with a dedicated `parse=json.loads` annotation, `example.py '{"foo": "bar"}'` now yields `Example({'foo': 'bar'})`. (composing-parsers)= ### Composing Multiple Parsers `Arg.parse` accepts **either** a single parser function, like above, or a sequence of parsers which will be called...in sequence. The return value of earlier parsers in the chain will be routed into the input of later parsers. ```python import json from dataclasses import dataclass from typing import Annotated import cappa def get_key(value: dict[str, str]) -> str: return value.get("todo", "") @dataclass class Example: todo: Annotated[str, cappa.Arg(parse=[json.loads, get_key])] todo = cappa.parse(Todo) print(todo) ``` (unpack-arguments)= ### `unpack_arguments`: Invoking constructors with `*` and `**` operators In some cases, you might want to construct objects that have required keyword arguments (pydantic models!) or otherwise who's constructors do not accept the single argument that would typically be provided to single CLI fields' parser. This is the use case for [cappa.unpack_arguments](cappa.unpack_arguments). Cappa will unpack the arguments based on the incoming type of data (e.g. sequence vs mapping). ```{note} (Arg.destructure)[#argument-destructuring] is a **similar** feature, in that it allows one to compose complex types into the CLI structure; but it in essentially opposite uses. `unpack_arguments` produces a complex type from a single CLI argument, whereas a "destructured" argument composes together multiple CLI arguments into one object without requiring a separate command. ``` #### *args Unpacking Sequence unpacking (`*`) can be used to provide values to constructors that accept multiple arguments positionally. For example: ```python from dataclasses import dataclass from typing import Annotated from cappa import unpack_arguments, Arg, parse @dataclass class Point: x: int y: int @dataclass class CLI: point: Annotated[Point, Arg(num_args=2, parse=unpack_arguments)] print(parse(CLI)) ``` ```bash $ cli.py 1, 2 CLI(Point(1, 2)) ``` **Without** `unpack_arguments`, the `Point(...)` constructor call would have been handed `[1, 2]`. Essentially the above is roughly equivalent to ```python class Point: ... @classmethod def from_list(cls, value: list[int]): return cls(*value) ... class CLI: point: Annotated[Point, Arg(num_args=2, parse=Point.from_list)] ``` #### **kwargs Unpacking Mapping unpacking (`**`) can be used for constructors which accept multiple arguments by keyword. For example, **Pydantic** classes' constructors **require** keyword-only arguments. This poses a challenge to a single CLI argument representing that constructor value. One way around this could be accepting a JSON string, parsing the string as JSON, and then unpacking the result into the constructor, effectively combining [Parsing JSON](#parsing-json) and [Composing Multiple Parsers](#composing-parsers). ```python from pydantic import BaseModel from typing import Annotated from cappa import unpack_arguments, Arg, parse class Point(BaseModel: x: int y: int @dataclass class CLI: point: Annotated[Point, Arg(parse=[json.loads, unpack_arguments])] print(parse(CLI)) ``` ```bash $ cli.py '{"x": 1, "y": 2}' CLI(Point(1, 2)) ``` Similarly, you could imagine swapping `json.loads` for some alternate parser which accepted/parsed `x=1,y=2` or other input formats. ### Parse Dependencies Parse functions can are provided the same dependency injection system given by `invoke` and [actions](#arg-action). Thus, by accepting an additional argument to the parser annotated with a supported type annotation, they'll be provided that runtime value automatically. This enables user-defined parsers that have the same amount of information that built-in parsers have when making decisions, namely the type information. Currently supported injectable values include: * [TypeView](https://github.com/litestar-org/type-lens/blob/main/type_lens/type_view.py) ```python from cappa.type_view import TypeView def parse(value, type_view: TypeView): value = json.loads(value) if type_view.is_mapping: return value raise ValueError("Requires a JSON mapping, string input!") class CLI: arg: Annotated[int, Arg(parse=parse)] ``` ```{note} More injectable dependencies **could** be supported. In particular the `Arg` instance comes to mind, it's just not immediately obvious what that would be. File an issue if you have a usecase! ``` ## `Arg.choices` This can be used to limit the set of valid inputs to one of a few literal values, for example, `Arg(choices=['a', 'b', 'c']`. Both `Literal`s (and literal unions) and `Enum`s automatically infer `choices`. ```{note} It **may** be preferable to use typing inference to **imply** choices as opposed to providing explicit choices depending on what you're doing. In scenarios with simple types/mappings, it wont make any difference (other than yielding more or less precise type information to a type checker). For example `foo: Literal[1, 2]` or `foo: list[Literal[1, 2]]` can be instead provided as `choices=` and it will yield exactly the same behavior. However, the type inference is sufficiently comprehensive that it is **possible** to yield more complex types like `list[tuple[str, Literal["1m", "1s"]]]`, where a simple `choices=` option **cannot** precisely target the required values. In such cases providing `choices=` will likely yield a runtime error, whereas using the inferred type validation will correctly parse the inputs. ``` ## `Arg.completion` This is an optional function which, if provided will be called during CLI (typically TAB) completion events in the shell. You can supply a function which accepts the partial input and returns an optional list of completions for that input. ```{note} `Arg.choices` has a default completion implementation. ``` ```python def complete_file(raw: str) -> list[cappa.Completion]: return [filename for filename in os.listdir() if filename.startswith(raw)] @dataclass class Foo: file: Annotated[str, cappa.Arg(completion=complete_file) ``` ```{note} Filename completion is actually natively supported by the shells, so this example is somewhat unnecessary to **actually** implement yourself. ``` ## `Arg.has_value` `Arg(has_value=True/False)` can be used to explicitly control whether the argument in question corresponds to a destination-type attribute as a value. For example the `Arg` that produces the `--help` option has a dedicated action which produces help text, and as such has a `has_value=False`. While there may not be much point in manually setting this attribute to `True` (because it will default to `True` in most cases), you **could** conceivably manually combine `has_value=False` and a custom [action](#action), to avoid cappa trying to map your `Arg` back to a specific attribute. (argument-destructuring)= ## `Arg.destructured` **Generally** a single class/type corresponds to a command, and that type's attributes correspond to the arguments/options for that command. The [exclusive group syntax](#exclusive-group-syntax) is one counter example, where a single class attribute maps to more than one CLI argument. "Destructured" arguments are essentially the inverse, in that they allow multiple attributes (and thus CLI arguments) to be mapped back to a single command's class attribute. ```python from __future__ import annotations from dataclasses import dataclass from typing import Annotated from cappa import Arg, Destructured @dataclass class Args: sub_object: Destructured[SubObject] @dataclass class SubObject: arg1: Annotated[str, Arg(long=True)] arg2: Annotated[int, Arg(long=True)] ``` This essentially fans out the `--arg1=foo --arg2=2` CLI arguments up into the parent `Args` command, while ultimately mapping the resultant values back into the expected output structure of: `Args(sub_object=SubObject(arg1='foo', arg2=2))`. This concept has a couple of practical uses: - Code/argument reuse: In the above example `SubObject` can now be shared between multiple subcommands to provide the same set of arguments in different places **without** requiring subclassing. - Logical grouping/organization: This allows grouping of logically related fields in the **python** code without affecting how those arguments are represented in the CLI shape. ```{note} Currently `Arg.destructure()` only works with **singular concrete** type annotations. That is, in the above example `Annotated[SubObject, Arg.destructured()]`; whereas it will raise a `ValueError` if given `SubObject | None` or other more exotic annotations. Principally, `Annotated[SubObject | None, Arg.destructured()]` **could** make sense to imply that all child options are therefore optional, or that if any child attributes are missing, then that implies `sub_object=None` at the top level. However both of these are mechanically much more complex than the feature, as it exists today. Additionally, this feature only works with the native backend. This **probably** has a workable solution for argparse, so file an issue if this affects you! ``` ```{note} (unpack_arguments)[#unpack-arguments] is a **similar** feature, in that it allows one to compose complex types into the CLI structure; but it in essentially opposite uses. `unpack_arguments` produces a complex type from a single CLI argument, whereas a "destructured" argument composes together multiple CLI arguments into one object without requiring a separate command. ``` ## `Arg.propagate` Argument propagation is a way of making higher-level arguments available during the parsing of child subcommands. When an argument is marked as `Arg(propagate=True)`, access to that argument will be made available in all **child** subcommands, while still recording the value itself to the object on which it was defined. ```python from __future__ import annotations from dataclasses import dataclass from typing import Annotated import cappa @dataclass class Main: file: Annotated[str, cappa.Arg(long=True, propagate=True)] subcommand: cappa.Subcommands[One | Two | None] = None @dataclass class One: ... @dataclass class Two: subcommand: cappa.Subcommands[Nested | None] = None @dataclass class Nested: ... print(cappa.parse(Main)) ``` Given the above example, all of the following would be valid, and produce `Main(file='config.txt', ...)`: - `main.py --file config.txt` - `main.py one --file config.txt` - `main.py two --file config.txt` - `main.py two nested --file config.txt` If defined on a top-level command object (like above), that argument will effectively be available globally within the CLI, again while actually propagating the value back to the place at which it was defined. However if the propagated argument is **not** defined at the top-level, it will not propagate "upwards" to parent commands; only downward to child subcommands. ```{note} `Arg.propagate` is not currently enabled/allowed for positional arguments (file an issue if this is a problem for you!) largely because it's not clear that the feature makes any sense except on (particularly optional) options. `Arg.propagate` is not implemented in the `argparse` backend. ``` ### Propagated Arg Help By default propagated arguments are added to child subcommand's help as though the argument was defined like any other argument. If you want propagated arguments categorically separated from normal arguments, you can assign them a distinct [group](#arg-group), which will cause them to be displayed separately. For example: ```python group = cappa.Group(name="Global", section=1) @dataclass class Command: other1: Annotated[int, cappa.Arg(long=True)] = 1 other2: Annotated[int, cappa.Arg(long=True)] = 1 foo: Annotated[int, cappa.Arg(long=True, propagate=True, group=group)] = 1 bar: Annotated[str, cappa.Arg(long=True, propagate=True, group=group)] = 1 ``` Would yield: ``` Options [--other1 OTHER1] (Default: 1) [--other2 OTHER2] (Default: 1) Global [--foo FOO] (Default: 1) [--bar BAR] (Default: 1) ``` Note, this is no different from use of `Arg.group` in any other context, except in that the argument only exists at the declaration point, so any grouping configuration will also propagate down into the way child commands render those arguments as well. ## `Arg.hidden` Controls whether an argument is displayed in help text. ## `Arg.required` Controls whether an argument is required. ## `Arg.deprecated` When truthy, will emit a deprecation message upon use of an argument. There is a default message generated, but supplying a string will yield **that** as the deprecation message. ## API ```{eval-rst} .. autoapiclass:: cappa.Arg :noindex: ```