From 44378162b2dc5aee3a9e34d5d731b7f01536f5b1 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Tue, 7 Oct 2025 20:58:45 +0200 Subject: [PATCH] feat: do not exit() with exit_on_error to False --- tests/test_leadership.py | 2 +- tiramisu_cmdline_parser/api.py | 43 +++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/tests/test_leadership.py b/tests/test_leadership.py index c0bfd2a..4195b1b 100644 --- a/tests/test_leadership.py +++ b/tests/test_leadership.py @@ -306,7 +306,7 @@ def test_leadership_modif_follower_choice(json): def test_leadership_modif_follower_choice_unknown(json): output = """usage: prog.py [-h] [--leader.leader [LEADER ...]] [--leader.pop-leader INDEX] [--leader.follower INDEX [FOLLOWER]] [--leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...]] [--leader.follower_integer INDEX [FOLLOWER_INTEGER]] [--leader.follower_boolean INDEX] [--leader.no-follower_boolean INDEX] [--leader.follower_choice INDEX [{opt1,opt2}]] -prog.py: error: argument --leader.follower_choice: invalid choice: 'opt_unknown' (choose from 'opt1', 'opt2') +prog.py: error: argument --leader.follower_choice: invalid choice: 'opt_unknown' (choose from opt1, opt2) """ config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) diff --git a/tiramisu_cmdline_parser/api.py b/tiramisu_cmdline_parser/api.py index 130b957..9fc35f9 100644 --- a/tiramisu_cmdline_parser/api.py +++ b/tiramisu_cmdline_parser/api.py @@ -15,6 +15,7 @@ from typing import Union, List, Dict, Tuple, Optional, Any from argparse import ( ArgumentParser, + ArgumentError, Namespace, SUPPRESS, _HelpAction, @@ -123,11 +124,13 @@ class TiramisuNamespace(Namespace): option = self._config.option(true_key) if option.isfollower(): _setattr = self._setattr_follower - if not value[0].isdecimal(): - raise ValueError("index must be a number, not {}".format(value[0])) - index = int(value[0]) + index = value[0] + if isinstance(index, str): + if not value[0].isdecimal(): + raise ValueError("index must be a number, not {}".format(value[0])) + index = int(index) option = self._config.option(true_key, index) - true_value = ",".join(value[1:]) + true_value = ",".join([str(v) for v in value[1:]]) else: _setattr = self._setattr true_value = value @@ -168,7 +171,7 @@ class TiramisuNamespace(Namespace): "argument {}: invalid choice: '{}' (choose from {})".format( self.arguments[key], display_value, - ", ".join([f"'{val}'" for val in choices]), + ", ".join([f"{val}" for val in choices]), ) ) else: @@ -177,6 +180,12 @@ class TiramisuNamespace(Namespace): def _setattr(self, option: "Option", true_key: str, key: str, value: Any) -> None: if option.ismulti() and value is not None and not isinstance(value, list): value = [value] + if option.isleader(): + # set value for a leader, it began to remove all values! + len_leader = option.value.len() + if len_leader: + for idx in range(len_leader - 1, -1, -1): + option.value.pop(idx) try: option.value.set(value) except PropertiesOptionError as err: @@ -400,6 +409,7 @@ class TiramisuCmdlineParser(ArgumentParser): self.config, self.prog, root=self.root, + exit_on_error=self.exit_on_error, remove_empty_od=self.remove_empty_od, display_modified_value=self.display_modified_value, formatter_class=self.formatter_class, @@ -654,7 +664,7 @@ class TiramisuCmdlineParser(ArgumentParser): kwargs["nargs"] = 2 if _forhelp and "mandatory" not in properties: metavar = "[{}]".format(metavar) - if option.type() == "choice": + if _forhelp and option.type() == "choice": # do not manage choice with argparse there is problem with integer problem kwargs["metavar"] = ( "INDEX", @@ -673,13 +683,15 @@ class TiramisuCmdlineParser(ArgumentParser): if _forhelp and option.type() == "boolean": kwargs["metavar"] = "INDEX" kwargs["nargs"] = 1 - elif option.type() == "choice" and not option.isfollower(): + elif _forhelp and option.type() == "choice" and not option.isfollower(): # do not manage choice with argparse there is problem with integer problem kwargs["choices"] = get_choice_list(option, properties, False) elif option.type() == "float": kwargs["type"] = float else: pass + if not _forhelp and option.type() != "boolean" and "nargs" not in kwargs.kwargs: + kwargs["nargs"] = "?" actions.setdefault(kwargs.ga_name, []).append(kwargs) for option_is_not_default in options_is_not_default.values(): @@ -696,7 +708,9 @@ class TiramisuCmdlineParser(ArgumentParser): def parse_args(self, *args, valid_mandatory=True, **kwargs): kwargs["namespace"] = self.namespace try: - namespaces = super().parse_args(*args, **kwargs) + namespaces, unknown = super().parse_known_args(*args, **kwargs) + if unknown: + msg_unknown = 'unrecognized arguments: %s' % ' '.join(unknown) except PropertiesOptionError as err: name = err._subconfig.path properties = self.config.option(name).property.get() @@ -739,6 +753,13 @@ class TiramisuCmdlineParser(ArgumentParser): self.error( "the following arguments are required: {}".format(", ".join(errors)) ) + if unknown: + if self.exit_on_error: + self.error(msg_unknown) + else: + err = ArgumentError(None, msg_unknown) + err.unknown = unknown + raise err return namespaces def format_usage(self, *args, **kwargs): @@ -779,3 +800,9 @@ class TiramisuCmdlineParser(ArgumentParser): def get_config(self): return self.config + + def error(self, msg): + if self.exit_on_error: + super().error(msg) + else: + raise ArgumentError(None, msg)