diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8cacc..d652b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### 0.22.5 - fix: Refactor parser combinators into dedicated module, and document the behavior more thoroughly. +- refactor: Pull handling of `--no-*` bool arguments out of the parser +- fix: Only apply `--no-*` handling when there is both a positive and negative variant ### 0.22.4 diff --git a/pyproject.toml b/pyproject.toml index 6e17220..d5f228b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cappa" -version = "0.22.4" +version = "0.22.5" description = "Declarative CLI argument parser." urls = {repository = "https://github.com/dancardin/cappa"} diff --git a/src/cappa/arg.py b/src/cappa/arg.py index 4c58f4f..8be2dac 100644 --- a/src/cappa/arg.py +++ b/src/cappa/arg.py @@ -619,19 +619,20 @@ def explode_negated_bool_args(args: typing.Sequence[Arg]) -> typing.Iterable[Arg for arg in args: yielded = False if isinstance(arg.action, ArgAction) and arg.action.is_bool_action and arg.long: - long = typing.cast(list[str], arg.long) + long = typing.cast(typing.List[str], arg.long) negatives = [item for item in long if "--no-" in item] positives = [item for item in long if "--no-" not in item] - positive_arg = dataclasses.replace( - arg, long=positives, action=ArgAction.store_true - ) - negative_arg = dataclasses.replace( - arg, long=negatives, action=ArgAction.store_false - ) - yield positive_arg - yield negative_arg - yielded = True + if negatives and positives: + positive_arg = dataclasses.replace( + arg, long=positives, action=ArgAction.store_true + ) + negative_arg = dataclasses.replace( + arg, long=negatives, action=ArgAction.store_false + ) + yield positive_arg + yield negative_arg + yielded = True if not yielded: yield arg diff --git a/src/cappa/argparse.py b/src/cappa/argparse.py index c6b2eb0..1adb9b2 100644 --- a/src/cappa/argparse.py +++ b/src/cappa/argparse.py @@ -93,22 +93,6 @@ def print_help(self, file=None): raise HelpExit(self.command.help_formatter(self.command, self.prog)) -class BooleanOptionalAction(argparse.Action): - """Simplified backport of same-named class from 3.9 onward. - - We know more about the called context here, and thus need much less of the - logic. Also, we support 3.8, which does not have the original class, so we - couldn't use it anyway. - """ - - def __init__(self, **kwargs): - super().__init__(nargs=0, **kwargs) - - def __call__(self, parser, namespace, values, option_string=None): - assert isinstance(option_string, str) - setattr(namespace, self.dest, not option_string.startswith("--no-")) - - def custom_action(arg: Arg, action: Callable): class CustomAction(argparse.Action): def __call__( # type: ignore diff --git a/src/cappa/parser.py b/src/cappa/parser.py index 325d0af..b94fdcd 100644 --- a/src/cappa/parser.py +++ b/src/cappa/parser.py @@ -624,11 +624,12 @@ class Value(typing.Generic[T]): value: T -def store_bool(val: bool): - def store(): - return val +def store_true(): + return True - return store + +def store_false(): + return False def store_count(context: ParseContext, arg: Arg): @@ -645,13 +646,13 @@ def store_append(context: ParseContext, arg: Arg, value: Value[typing.Any]): return result -process_options = { +process_options: dict[ArgAction, typing.Callable] = { ArgAction.help: HelpAction.from_context, ArgAction.version: VersionAction.from_arg, ArgAction.completion: CompletionAction.from_value, ArgAction.set: store_set, - ArgAction.store_true: store_bool(True), - ArgAction.store_false: store_bool(False), + ArgAction.store_true: store_true, + ArgAction.store_false: store_false, ArgAction.count: store_count, ArgAction.append: store_append, } diff --git a/tests/arg/test_bool.py b/tests/arg/test_bool.py index 7f401de..1c83f31 100644 --- a/tests/arg/test_bool.py +++ b/tests/arg/test_bool.py @@ -168,3 +168,29 @@ class ArgTest: with patch("os.environ", new={"ENV_DEFAULT": "1"}): test = parse(ArgTest, "--env-default", backend=backend) assert test.env_default is True + + +@backends +def test_sole_no_arg(backend): + @dataclass + class ArgTest: + no_dry_run: bool = False + + test = parse(ArgTest, backend=backend) + assert test.no_dry_run is False + + test = parse(ArgTest, "--no-dry-run", backend=backend) + assert test.no_dry_run is True + + +@backends +def test_sole_no_arg_inverted(backend): + @dataclass + class ArgTest: + no_dry_run: bool = True + + test = parse(ArgTest, backend=backend) + assert test.no_dry_run is True + + test = parse(ArgTest, "--no-dry-run", backend=backend) + assert test.no_dry_run is False