Skip to content

Commit c46749e

Browse files
committed
feat: Expand Arg.default capabilities through Default and
`ValueFrom` objects.
1 parent de0fe5b commit c46749e

29 files changed

+909
-243
lines changed

Diff for: CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 0.26
4+
5+
### 0.26.0
6+
7+
- Add `Default` object with associated fallback semantics for sequences of default handlers.
8+
- Add `ValueFrom` for handling default_factory lazily, as well as arbitrary function dispatch.
9+
- Add `State` as object accessible to invoke, Arg.parse, and ValueFrom.callable for sharing
10+
state amongst different stages of argument parsing.
11+
- fix: Skip non-init fields in dataclasses.
12+
313
## 0.25
414

515
### 0.25.1

Diff for: docs/source/api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
```{eval-rst}
44
.. autoapimodule:: cappa
5-
:members: parse, invoke, invoke_async, collect, command, Command, Subcommand, Dep, Arg, ArgAction, Exit, Env, Completion, Output, FileMode, unpack_arguments, Group
5+
:members: parse, invoke, invoke_async, collect, command, Command, Subcommand, Dep, Arg, ArgAction, Exit, Env, Completion, Output, FileMode, unpack_arguments, Group, Prompt, Confirm, ValueFrom, Default, State
66
```
77

88
```{eval-rst}

Diff for: docs/source/arg.md

+100-14
Original file line numberDiff line numberDiff line change
@@ -177,16 +177,20 @@ See [annotations](./annotation.md) for more details.
177177

178178
Controls the default argument value at the CLI level. Generally, you can avoid
179179
direct use of cappa's default by simply using the source class' native default
180-
mechanism. (i.e. `foo: int = 0` or `foo: int = field(default=0)` for
181-
dataclasses).
180+
mechanism. (i.e. `foo: int = 0`, `foo: int = field(default=0)`, or
181+
`foo: list[str] = field(default_factory=list)` for dataclasses).
182182

183-
However it can be convenient to use cappa's default because it does not affect
184-
the optionality of the field in question in the resultant class constructor.
183+
However it **can** be convenient to use cappa's default because it does not affect
184+
whether the underlying class definition makes that field required in the class'
185+
constructor.
185186

186187
```{note}
187-
The `default` value is not parsed by `parse`. That is to say, if no value is
188-
selected at the CLI and the default value is used instead, it will not be
189-
coerced into the annotated type automatically.
188+
The `default` value is not **typically** parsed by the given `parse` function.
189+
That is to say, if no value is selected at the CLI and the default value is used
190+
instead; it will not be coerced into the annotated type automatically.
191+
192+
(`Env`, `Prompt`, and `Confirm` are exceptions to this rule, explained in their
193+
sections below).
190194

191195
The reason for this is twofold:
192196

@@ -196,21 +200,103 @@ The reason for this is twofold:
196200
would infer `parse=Foo` and attempt to pass `Foo(Foo(''))` during parsing.
197201
```
198202

199-
### Environment Variable Fallback
203+
Additionally there are a number of natively integrated objects that can be used
204+
as default to create more complex behaviors given a **missing** CLI argument.
205+
The below objects will not be evaluated unless the user did not supply a value
206+
for the argument it's attached to.
207+
208+
- [cappa.Default](cappa.Default)
209+
- [cappa.Env](cappa.Env)
210+
- [cappa.Prompt](cappa.Prompt)/[cappa.Confirm](cappa.Confirm)
211+
- [cappa.ValueFrom](cappa.ValueFrom)
212+
213+
### `Default`
214+
215+
All other types of default are ultimately shorthand forms of `Default`. The
216+
`Default` construct allows for specifying an ordered chain of default fallback
217+
options, with an optional static fallback item at the end.
218+
219+
For example:
220+
221+
- `foo: int = 4` is the same as `default=Default(default=4)`. This unconditionally defaults to 4.
222+
223+
- `foo: Annotated[int, Arg(default=Env("FOO"))] = 4` is the same as
224+
`Arg(default=Default(Env("FOO"), default=4)`. This attempts to read the environment variable `FOO`,
225+
and falls back to 4 if the env var is unset.
226+
227+
- `foo: Annotated[int, Arg(default=Env("FOO") | Prompt("Gimme"))]` is the same as
228+
`Arg(default=Default(Env("FOO"), Prompt("Gimme"))`. This attempts to read the environment variable `FOO`,
229+
followed by a user prompt if the env var is unset.
230+
231+
As shown above, any combination of defaultable values can be used as fallbacks of one another by using
232+
the `|` operator to chain them together.
200233
201-
You can also use the default field to supply supported kinds of
202-
default-value-getting behaviors.
234+
As noted above, a value produced by `Default.default` **does not** invoke the `Arg.parse` parser. This is
235+
for similar reasons as to native dataclass defaults. The programmer is supplying the default value
236+
which should not **need** to be parsed.
237+
238+
### `Env`
239+
240+
[cappa.Env](cappa.Env) performs environment variable lookups in an attempt to provide a value to the
241+
class field.
203242
204-
`Env` is one such example, where with
205243
`Arg(..., default=Env("FOO", default='default value'))`, cappa will attempt to
206244
look up the environment variable `FOO` for the default value, if there was no
207245
supplied value at the CLI level.
208246
209-
```{eval-rst}
210-
.. autoapiclass:: cappa.Env
211-
:noindex:
247+
As noted above, a value produced by `Env` **does** invoke the `Arg.parse` parser. This is
248+
because `Env` values will always be returned as a string, very similarly to a normal pre-parse
249+
CLI value.
250+
251+
### `Prompt`/`Confirm`
252+
253+
[cappa.Prompt](cappa.Prompt) and [cappa.Confirm](cappa.Confirm) can be used to ask for user input
254+
to fulfill the value.
255+
256+
```{note}
257+
`rich.prompt.Prompt` and `rich.prompt.Confirm` can also be used transparently for the same purpose.
258+
```
259+
260+
```python
261+
import cappa
262+
263+
class Example:
264+
value: Annotated[int, cappa.Arg(default=cappa.Prompt("A number value"))]
265+
is_ok: Annotated[bool, cappa.Arg(default=cappa.Confirm("You sure?"))]
266+
```
267+
268+
As noted above, a value produced by `Prompt`/`Confirm` **does** invoke the `Arg.parse` parser. This is
269+
because these values will always be returned as a string, very similarly to a normal pre-parse
270+
CLI value.
271+
272+
### `ValueFrom`
273+
274+
[cappa.ValueFrom](cappa.ValueFrom) is a means for calling an arbitrary function at mapping time,
275+
to allow for dynamic default values.
276+
277+
```{info}
278+
A dataclass `field(default_factory=list)` is internally the same thing as `default=ValueFrom(list)`!
212279
```
213280
281+
```python
282+
from pathlib import Path
283+
import cappa
284+
285+
def load_default(key):
286+
config = json.loads(Path("config.json").read_text())
287+
return config[key]
288+
289+
class Example:
290+
value: Annotated[int, cappa.Arg(default=cappa.ValueFrom(load_default, key='value'))]
291+
```
292+
293+
This construct is able to be automatically supplied with [cappa.State](State), in the even shared
294+
parse state is required to evaluate a field's default.
295+
296+
As noted above, a value produced by `ValueFrom` **does not** invoke the `Arg.parse` parser. This is
297+
because the called function is programmer-supplied and can/should just return the correct end
298+
value.
299+
214300
(arg-group)=
215301
## `Arg.group`: Groups (and Mutual Exclusion)
216302

Diff for: docs/source/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Help/Help Inference <help>
4444
Asyncio <asyncio>
4545
Manual Construction <manual_construction>
4646
Sphinx/Docutils Directive <sphinx>
47+
Shared State <state>
4748
```
4849

4950
```{toctree}

Diff for: docs/source/rich.md

+4-9
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ You can define your own rich `Theme` object and supply it into
1717
[invoke](cappa.invoke) or [parse](cappa.parse), which will used when rendering
1818
help-text output (and error messaging).
1919

20-
Cappa's theme defines and uses the follwing style groups, which you would need
20+
Cappa's theme defines and uses the following style groups, which you would need
2121
to also supply:
2222

2323
- `cappa.prog`
@@ -29,8 +29,7 @@ to also supply:
2929

3030
## Prompt/Confirm
3131

32-
Cappa does not come with a native prompt/confirm option. However it does ship
33-
with built-in integration with `rich.prompt.Prompt`.
32+
Cappa integrates directly with `rich.prompt.Prompt`/`rich.prompt.Confirm`.
3433

3534
You can directly make use of confirm/prompt from within your code anywhere, and
3635
it should "just work"
@@ -59,12 +58,8 @@ In the event the value for that argument was omitted at the command-line, the
5958
prompt will be evaluated.
6059

6160
```{note}
62-
Input prompts can be a hazzard for testing. `cappa.rich.TestPrompt` can be used
63-
in any CLI-level testing, which relocates rich's `default` and `stream` arguments
64-
off the `.ask` function.
65-
66-
You can create a `TestPrompt("message", input='text input', default='some default')`
67-
to simulate a user inputting values to stdin inside tests.
61+
Input prompts can be a hazard for testing. As such, you can supply `input=StringIO(...)` to
62+
either of `parse`/`invoke` as a way to write tests that exercise the prompt code.
6863
```
6964

7065
## Pretty Tracebacks

Diff for: docs/source/state.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# State
2+
3+
A [cappa.State](cappa.State) is ultimately a thin wrapper around a simple dictionary, that can
4+
be used to share state among different parts of the overall cappa parsing process.
5+
6+
A `state=` argument can be supplied to [parse](cappa.parse) or [invoke](cappa.invoke), which accepts a
7+
`State` instance. If no upfront `State` instance is supplied, one will be constructed automatically
8+
so it can always be assumed to exist.
9+
10+
```{note}
11+
It is also optionally generic over the dict type. So it can be annotated with `TypedDict` to retain
12+
safety over the dict's fields.
13+
```
14+
15+
```python
16+
import cappa
17+
from typing import Any, Annotated, Any
18+
from dataclasses import dataclass
19+
20+
class CliState:
21+
config: dict[str, Any]
22+
23+
24+
def get_config(key: str, state: State[CliState]):
25+
return state.state["config"][key]
26+
27+
@dataclass
28+
class Example:
29+
token: Annotated[str, cappa.Arg(default=cappa.ValueFrom(get_config, key="token"))]
30+
31+
32+
config = load_config()
33+
state = State({"config": config})
34+
cappa.parse(Example, state=state)
35+
```
36+
37+
The above example shows some pre-cappa data/state being loaded and provided to cappa through `state`.
38+
Then some field accesses the shared `state` by getting it dependency injected into the `ValueFrom`
39+
callable.
40+
41+
```{note}
42+
`Arg.parse` and `invoke` functions can **also** accept `State` annotated inputs in order to
43+
be provided with the `State` instance.
44+
```

Diff for: pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cappa"
3-
version = "0.25.1"
3+
version = "0.26.0"
44
description = "Declarative CLI argument parser."
55

66
urls = {repository = "https://github.com/dancardin/cappa"}

Diff for: src/cappa/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from cappa.base import collect, command, invoke, invoke_async, parse
22
from cappa.command import Command
33
from cappa.completion.types import Completion
4-
from cappa.env import Env
4+
from cappa.default import Confirm, Default, Env, Prompt, ValueFrom
55
from cappa.file_io import FileMode
66
from cappa.help import HelpFormatable, HelpFormatter
77
from cappa.invoke import Dep
88
from cappa.output import Exit, HelpExit, Output
99
from cappa.parse import unpack_arguments
10+
from cappa.state import State
1011
from cappa.subcommand import Subcommand, Subcommands
1112

1213
# isort: split
@@ -21,6 +22,8 @@
2122
"ArgAction",
2223
"Command",
2324
"Completion",
25+
"Confirm",
26+
"Default",
2427
"Dep",
2528
"Env",
2629
"Exit",
@@ -30,8 +33,11 @@
3033
"HelpFormatable",
3134
"HelpFormatter",
3235
"Output",
36+
"Prompt",
37+
"State",
3338
"Subcommand",
3439
"Subcommands",
40+
"ValueFrom",
3541
"argparse",
3642
"backend",
3743
"collect",

0 commit comments

Comments
 (0)