Skip to content

Commit c489574

Browse files
authored
Merge pull request #194 from DanCardin/dc/optional-positional
fix: Optional positional arguments should not get a default value from their action.
2 parents 7b9cd2e + 4225cc4 commit c489574

File tree

6 files changed

+68
-12
lines changed

6 files changed

+68
-12
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 0.26
44

5+
### 0.26.2
6+
7+
- fix: Optional positional arguments should not get a default value from their action.
8+
59
### 0.26.1
610

711
- fix: Default formatting when value is None/Empty and formatter is supplied.

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.26.1"
3+
version = "0.26.2"
44
description = "Declarative CLI argument parser."
55

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

Diff for: src/cappa/argparse.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def add_argument(
229229

230230
is_positional = not names
231231

232-
num_args = backend_num_args(arg.num_args)
232+
num_args = backend_num_args(arg.num_args, assert_type(arg.required, bool))
233233

234234
kwargs: dict[str, typing.Any] = {
235235
"dest": dest_prefix + assert_type(arg.field_name, str),
@@ -295,12 +295,14 @@ def add_subcommands(
295295
)
296296

297297

298-
def backend_num_args(num_args: int | None) -> int | str | None:
298+
def backend_num_args(num_args: int | None, required: bool) -> int | str | None:
299299
if num_args is None or num_args == 1:
300300
return None
301301

302302
if num_args == -1:
303-
return "+"
303+
if required:
304+
return "+"
305+
return "*"
304306

305307
return num_args
306308

Diff for: src/cappa/parser.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -597,13 +597,16 @@ def consume_arg(
597597
arg=arg,
598598
)
599599
else:
600-
missing_arg_requirement = (
601-
# Positive fixed-arg amount
602-
(orig_num_args > 0 and len(result) != orig_num_args)
603-
# Unbounded but required arg
604-
or (orig_num_args < 0 and arg.required and not result)
605-
)
606-
if missing_arg_requirement:
600+
required_arg = arg.required
601+
fixed_arg_mismatch = orig_num_args > 0 and len(result) != orig_num_args
602+
unbounded_missing = orig_num_args < 0 and not result
603+
if fixed_arg_mismatch or unbounded_missing:
604+
option_requiring_values = option and requires_values
605+
if not (required_arg or option_requiring_values or result):
606+
# Lacking a consumed value for an optional positional arg should avoid
607+
# hitting the argument's action, so as to apply it's default handling.
608+
return
609+
607610
quoted_result = [f"'{r}'" for r in result]
608611
names_str = arg.names_str("/")
609612

Diff for: tests/arg/test_default_value_from.py

+47
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,50 @@ class Command:
5151

5252
result = parse(Command, backend=backend)
5353
assert result == Command(0)
54+
55+
56+
def called():
57+
return ["42"]
58+
59+
60+
@backends
61+
def test_positional_unbound_num_args(backend):
62+
@dataclass
63+
class Command:
64+
positional: Annotated[
65+
list[str],
66+
cappa.Arg(
67+
short=False,
68+
long=False,
69+
default=cappa.ValueFrom(called),
70+
),
71+
]
72+
73+
result = parse(Command, "7", backend=backend)
74+
assert result == Command(["7"])
75+
76+
result = parse(Command, backend=backend)
77+
assert result == Command(["42"])
78+
79+
80+
def two_tuple():
81+
return ("42", "0")
82+
83+
84+
def test_optional_positional_2_tuple():
85+
@dataclass
86+
class Command:
87+
positional: Annotated[
88+
tuple[str, str],
89+
cappa.Arg(
90+
short=False,
91+
long=False,
92+
default=cappa.ValueFrom(two_tuple),
93+
),
94+
]
95+
96+
result = parse(Command, "7", "8")
97+
assert result == Command(("7", "8"))
98+
99+
result = parse(Command)
100+
assert result == Command(("42", "0"))

Diff for: uv.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)