From 4e2c37822ca1ecf485030480697824f820fc78fb Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sat, 27 Jul 2019 18:51:16 +0200 Subject: [PATCH] add --pop-leader to remove a value in a leader option --- test/test_help.py | 2 +- test/test_leadership.py | 27 ++++++- test/test_optiondescription.py | 6 +- test/test_readme.py | 2 +- test/test_shortarg.py | 2 +- tiramisu_cmdline_parser/api.py | 127 +++++++++++++++++++++------------ 6 files changed, 109 insertions(+), 57 deletions(-) diff --git a/test/test_help.py b/test/test_help.py index 42052e4..5e70123 100644 --- a/test/test_help.py +++ b/test/test_help.py @@ -7,7 +7,7 @@ from argparse import RawDescriptionHelpFormatter from tiramisu_cmdline_parser import TiramisuCmdlineParser from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \ SymLinkOption, OptionDescription, Config -from tiramisu_json_api import Config as JsonConfig +from tiramisu_api import Config as JsonConfig diff --git a/test/test_leadership.py b/test/test_leadership.py index 5c394ec..3c76c56 100644 --- a/test/test_leadership.py +++ b/test/test_leadership.py @@ -6,7 +6,7 @@ import pytest from tiramisu_cmdline_parser import TiramisuCmdlineParser from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \ SymLinkOption, OptionDescription, Leadership, Config, submulti -from tiramisu_json_api import Config as JsonConfig +from tiramisu_api import Config as JsonConfig def get_config(json, with_mandatory=False): @@ -23,7 +23,7 @@ def get_config(json, with_mandatory=False): opt_list = [leader, follower, follower_submulti, follower_integer, follower_boolean, follower_choice] if with_mandatory: opt_list.append(StrOption('follower_mandatory', "Follower mandatory", multi=True, properties=('mandatory',))) - leadership = Leadership('leader', '', opt_list) + leadership = Leadership('leader', 'leader', opt_list) config = Config(OptionDescription('root', 'root', [leadership])) if json == 'tiramisu': return config @@ -38,6 +38,7 @@ def json(request): def test_leadership_help(json): output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--leader.pop-leader INDEX] [--leader.follower INDEX [FOLLOWER]] --leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...] [--leader.follower_integer INDEX [FOLLOWER_INTEGER]] @@ -54,6 +55,7 @@ leader: --leader.leader [LEADER [LEADER ...]] Leader var + --leader.pop-leader INDEX --leader.follower INDEX [FOLLOWER] Follower --leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...] @@ -105,6 +107,7 @@ def test_leadership_modif_follower(json): def test_leadership_modif_follower_not_submulti(json): output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--leader.pop-leader INDEX] [--leader.follower INDEX [FOLLOWER]] [--leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...]] [--leader.follower_integer INDEX [FOLLOWER_INTEGER]] @@ -199,6 +202,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 ...]]] + [--leader.pop-leader INDEX] [--leader.follower INDEX [FOLLOWER]] [--leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...]] [--leader.follower_integer INDEX [FOLLOWER_INTEGER]] @@ -222,6 +226,7 @@ prog.py: error: invalid choice: 'opt_unknown' (choose from 'opt1', 'opt2') def test_leadership_modif_follower_not_number(json): output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--leader.pop-leader INDEX] [--leader.follower INDEX [FOLLOWER]] [--leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...]] [--leader.follower_integer INDEX [FOLLOWER_INTEGER]] @@ -260,6 +265,23 @@ def test_leadership_modif_multi(json): assert config.value.dict() == output +def test_leadership_modif_multi_reduce(json): + output = {'leader.leader': ['192.168.1.1', '192.168.253.1'], + 'leader.follower': ['255.255.255.128', '255.255.255.0'], + 'leader.follower_boolean': [None, None], + 'leader.follower_choice': [None, None], + 'leader.follower_integer': [None, None], + 'leader.follower_submulti': [[], []]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.leader', '192.168.1.1', '10.253.10.1', '192.168.253.1', + '--leader.follower', '0', '255.255.255.128', + '--leader.follower', '2', '255.255.255.0', + '--leader.pop-leader', '1']) + assert config.value.dict() == output + + def test_leadership_modif_mandatory(json): output = {'leader.leader': ['192.168.1.1'], 'leader.follower': [None], @@ -269,6 +291,7 @@ def test_leadership_modif_mandatory(json): 'leader.follower_integer': [None], 'leader.follower_submulti': [['255.255.255.128']]} output2 = """usage: prog.py --leader.leader ['192.168.1.1'] [-h] + [--leader.pop-leader INDEX] [--leader.follower INDEX [FOLLOWER]] --leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...] diff --git a/test/test_optiondescription.py b/test/test_optiondescription.py index 02c6e4c..ba42e86 100644 --- a/test/test_optiondescription.py +++ b/test/test_optiondescription.py @@ -6,7 +6,7 @@ import pytest from tiramisu_cmdline_parser import TiramisuCmdlineParser from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \ SymLinkOption, OptionDescription, Config -from tiramisu_json_api import Config as JsonConfig +from tiramisu_api import Config as JsonConfig def get_config(json, has_tree=False, default_verbosity=False, add_long=False, add_store_false=False): @@ -80,8 +80,6 @@ od1.od0: -nv, --od1.od0.no-verbosity od2: - od2 - --od2.before BEFORE Before --od2.after AFTER After @@ -115,8 +113,6 @@ od1.od0: -nv, --od1.od0.no-verbosity od2: - od2 - --od2.before BEFORE Before --od2.after AFTER After diff --git a/test/test_readme.py b/test/test_readme.py index 4d21f76..74ee790 100644 --- a/test/test_readme.py +++ b/test/test_readme.py @@ -6,7 +6,7 @@ import pytest from tiramisu_cmdline_parser import TiramisuCmdlineParser from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \ SymLinkOption, OptionDescription, Config -from tiramisu_json_api import Config as JsonConfig +from tiramisu_api import Config as JsonConfig def get_config(json, has_tree=False, default_verbosity=False, add_long=False, add_store_false=False): diff --git a/test/test_shortarg.py b/test/test_shortarg.py index e68278c..9481623 100644 --- a/test/test_shortarg.py +++ b/test/test_shortarg.py @@ -6,7 +6,7 @@ from contextlib import redirect_stderr from tiramisu_cmdline_parser import TiramisuCmdlineParser from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \ SymLinkOption, OptionDescription, Config -from tiramisu_json_api import Config as JsonConfig +from tiramisu_api import Config as JsonConfig @pytest.fixture(params=['tiramisu', 'tiramisu-json']) diff --git a/tiramisu_cmdline_parser/api.py b/tiramisu_cmdline_parser/api.py index 4ae5ff9..2832986 100644 --- a/tiramisu_cmdline_parser/api.py +++ b/tiramisu_cmdline_parser/api.py @@ -39,6 +39,7 @@ class TiramisuNamespace(Namespace): super().__setattr__('_config', config) super().__setattr__('_root', root) super().__setattr__('list_force_no', {}) + super().__setattr__('list_force_del', {}) self._populate() super().__init__() @@ -61,6 +62,8 @@ class TiramisuNamespace(Namespace): value: Any) -> None: if key in self.list_force_no: true_key = self.list_force_no[key] + elif key in self.list_force_del: + true_key = self.list_force_del[key] else: true_key = key option = self._config.option(true_key) @@ -71,7 +74,10 @@ class TiramisuNamespace(Namespace): _setattr = self._setattr true_value = value try: - _setattr(option, true_key, key, value) + if key in self.list_force_del: + option.value.pop(value) + else: + _setattr(option, true_key, key, value) except ValueError as err: if option.option.type() == 'choice': raise ValueError("invalid choice: '{}' (choose from {})".format(true_value, ', '.join([f"'{val}'" for val in option.value.list()]))) @@ -122,17 +128,12 @@ class TiramisuHelpFormatter: self.items[0][0].__name__ == '_format_text': return '' # Remove OD if name == description - if self.items and \ - self.formatter.remove_empty_description_od and \ - self.items[0][0].__name__ == '_format_text': - name = self.items[0][1][0] - path = self.heading - if '.' in path: - compare = path.rsplit('.', 1)[1] - else: - compare = path - if name == compare: - return '' + if self.formatter.remove_empty_description_od and \ + self.items is not None and \ + self.heading is not None and \ + len(self.items) > 1 and \ + self.items[0][0].__name__ != '_format_text': + return '' return super().format_help() @@ -151,18 +152,23 @@ class _BuildKwargs: option: 'Option', cmdlineparser: 'TiramisuCmdlineParser', properties: List[str], - force_no: bool) -> None: + force_no: bool, + force_del: bool) -> None: self.kwargs = {} self.cmdlineparser = cmdlineparser self.properties = properties self.force_no = force_no - if not self.force_no: + self.force_del = force_del + if not self.force_no and not self.force_del: self.kwargs['help'] = option.doc().replace('%', '%%') if 'positional' not in self.properties: is_short_name = self.cmdlineparser._is_short_name(name, 'longargument' in self.properties) if self.force_no: ga_name = self.gen_argument_name(name, is_short_name) self.cmdlineparser.namespace.list_force_no[ga_name] = option.path() + elif self.force_del: + ga_name = self.gen_argument_name(name, is_short_name) + self.cmdlineparser.namespace.list_force_del[ga_name] = option.path() else: ga_name = name self.kwargs['dest'] = self.gen_argument_name(option.path(), False) @@ -180,6 +186,8 @@ class _BuildKwargs: is_short_name = self.cmdlineparser._is_short_name(option.name(), 'longargument' in self.properties) if self.force_no: name = self.gen_argument_name(option.name(), is_short_name) + elif self.force_del: + name = self.gen_argument_name(option.name(), is_short_name) else: name = option.name() self.args.insert(0, self.cmdlineparser._gen_argument(name, is_short_name)) @@ -195,6 +203,16 @@ class _BuildKwargs: name = sname[0] + '.' + prefix + sname[1] else: name = prefix + name + if self.force_del: + if is_short_name: + prefix = 'p' + else: + prefix = 'pop-' + if '.' in name: + sname = name.rsplit('.', 1) + name = sname[0] + '.' + prefix + sname[1] + else: + name = prefix + name return name def get(self) -> Tuple[Dict]: @@ -315,7 +333,7 @@ class TiramisuCmdlineParser(ArgumentParser): config: Config, prefix: Optional[str], _forhelp: bool, - group): + group, level): for obj in config.list(type='all'): # do not display frozen option if 'frozen' in obj.option.properties(): @@ -329,23 +347,32 @@ class TiramisuCmdlineParser(ArgumentParser): prefix_ = prefix + '.' + obj.option.name() else: prefix_ = obj.option.path() - self._config_to_argparser(_forhelp, obj, prefix_, newgroup) + self._config_to_argparser(_forhelp, obj, prefix_, newgroup, level + 1) elif obj.option.type() == 'boolean' and not obj.option.issymlinkoption(): - yield obj, False - yield obj, True + if not obj.option.isleader(): + yield obj, False, None + yield obj, True, None + else: + yield obj, False, False + yield obj, False, True + yield obj, True, None + elif obj.option.isleader(): + yield obj, None, False + yield obj, None, True else: - yield obj, None + yield obj, None, None def _config_to_argparser(self, _forhelp: bool, config, prefix: Optional[str], - group=None) -> None: + group=None, + level=0) -> None: if group is None: group = super() actions = {} leadership_len = None - for obj, force_no in self._config_list(config, prefix, _forhelp, group): + for obj, force_no, force_del in self._config_list(config, prefix, _forhelp, group, level): option = obj.option name = option.name() if name.startswith(self.prefix_chars): @@ -356,7 +383,9 @@ class TiramisuCmdlineParser(ArgumentParser): for action in actions[symlink_name]: action.add_argument(option) continue - if option.isleader(): + if force_del: + value = None + elif option.isleader(): value = obj.value.get() leadership_len = len(value) elif option.isfollower(): @@ -371,7 +400,7 @@ class TiramisuCmdlineParser(ArgumentParser): properties = obj.option.properties() else: properties = obj.property.get() - kwargs = _BuildKwargs(name, option, self, properties, force_no) + kwargs = _BuildKwargs(name, option, self, properties, force_no, force_del) if not option.isfollower() and _forhelp and not obj.owner.isdefault() and value is not None: if not force_no: self._option_is_not_default(properties, @@ -393,33 +422,34 @@ class TiramisuCmdlineParser(ArgumentParser): kwargs['default'] = SUPPRESS if _forhelp and 'mandatory' in properties: kwargs['required'] = True - if option.type() == 'boolean' and not option.isfollower(): - if 'storefalse' in properties: - if force_no: - action = 'store_true' - else: + if not force_del and option.type() == 'boolean': + if not option.isfollower(): + if 'storefalse' in properties: + if force_no: + action = 'store_true' + else: + action = 'store_false' + elif force_no: action = 'store_false' - elif force_no: - action = 'store_false' + else: + action = 'store_true' + kwargs['action'] = action else: - action = 'store_true' - kwargs['action'] = action - else: - if option.type() == 'boolean': kwargs['metavar'] = 'INDEX' - if option.type() != 'boolean': - if _forhelp: - value = obj.value.default() - if value not in [None, []]: - #kwargs['default'] = kwargs['const'] = option.default() - #kwargs['action'] = 'store_const' - kwargs['nargs'] = '?' + if option.type() != 'boolean' or force_del: + if not force_del: + if _forhelp: + value = obj.value.default() + if value not in [None, []]: + #kwargs['default'] = kwargs['const'] = option.default() + #kwargs['action'] = 'store_const' + kwargs['nargs'] = '?' - if not option.isfollower() and option.ismulti(): - if _forhelp and 'mandatory' in properties: - kwargs['nargs'] = '+' - else: - kwargs['nargs'] = '*' + if not option.isfollower() and option.ismulti(): + if _forhelp and 'mandatory' in properties: + kwargs['nargs'] = '+' + else: + kwargs['nargs'] = '*' if option.isfollower() and not option.type() == 'boolean': metavar = option.name().upper() if option.issubmulti(): @@ -438,7 +468,10 @@ class TiramisuCmdlineParser(ArgumentParser): kwargs['metavar'] = ('INDEX', choices) else: kwargs['metavar'] = ('INDEX', metavar) - if option.type() == 'string': + if force_del: + kwargs['metavar'] = 'INDEX' + kwargs['type'] = int + elif option.type() == 'string': pass elif option.type() == 'integer' or option.type() == 'boolean': # when boolean we are here only if follower