From 1cb8a2cc7b86fdbf789f4b912bf91cbe25c1ca3f Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 17 Apr 2019 19:16:43 +0200 Subject: [PATCH] leader/follower support --- examples/Hangman/example.py | 4 +- test/test_leadership.py | 319 ++++++++++++++++++++++++++++ test/test_readme.py | 207 ++++++++++-------- tiramisu_cmdline_parser/api.py | 372 +++++++++++++++++++++++---------- 4 files changed, 708 insertions(+), 194 deletions(-) create mode 100644 test/test_leadership.py diff --git a/examples/Hangman/example.py b/examples/Hangman/example.py index b667691..ca9f92c 100644 --- a/examples/Hangman/example.py +++ b/examples/Hangman/example.py @@ -118,7 +118,7 @@ def main(): proposal = ProposalOption('guess{}'.format(idx), 'Guess {}'.format(idx), requires=requires, - properties=('positional',)) + properties=('positional', 'mandatory')) #FIXME maximum recursion ... #if proposals: # proposal.impl_add_consistency('not_equal', proposals[0]) @@ -150,7 +150,7 @@ def main(): config.property.read_write() try: parser = TiramisuCmdlineParser(config) - parser.parse_args() + parser.parse_args(valid_mandatory=False) except ValueError: # if no more suggestion pass diff --git a/test/test_leadership.py b/test/test_leadership.py new file mode 100644 index 0000000..e2cc119 --- /dev/null +++ b/test/test_leadership.py @@ -0,0 +1,319 @@ +from io import StringIO +from contextlib import redirect_stdout, redirect_stderr +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 + + +def get_config(json, with_mandatory=False): + leader = StrOption('leader', "Leader var", ['192.168.0.1'], multi=True) + follower = StrOption('follower', "Follower", multi=True) + if with_mandatory: + properties = ('mandatory',) + else: + properties = None + follower_submulti = StrOption('follower_submulti', "Follower submulti", multi=submulti, properties=properties) + follower_integer = IntOption('follower_integer', "Follower integer", multi=True) + follower_boolean = BoolOption('follower_boolean', "Follower boolean", multi=True) + follower_choice = ChoiceOption('follower_choice', "Follower choice", ('opt1', 'opt2'), multi=True) + 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) + config = Config(OptionDescription('root', 'root', [leadership])) + if json == 'tiramisu': + return config + jconfig = JsonConfig(config.option.dict()) + return jconfig + + +@pytest.fixture(params=['tiramisu', 'tiramisu-api']) +def json(request): + return request.param + + +def test_leadership_help(json): + output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--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}]] + --leader.follower_mandatory INDEX FOLLOWER_MANDATORY + +optional arguments: + -h, --help show this help message and exit + +leader: + --leader.leader [LEADER [LEADER ...]] + Leader var + --leader.follower INDEX [FOLLOWER] + Follower + --leader.follower_submulti INDEX [FOLLOWER_SUBMULTI ...] + Follower submulti + --leader.follower_integer INDEX [FOLLOWER_INTEGER] + Follower integer + --leader.follower_boolean INDEX + Follower boolean + --leader.no-follower_boolean INDEX + --leader.follower_choice INDEX [{opt1,opt2}] + Follower choice + --leader.follower_mandatory INDEX FOLLOWER_MANDATORY + Follower mandatory +""" + parser = TiramisuCmdlineParser(get_config(json, with_mandatory=True), 'prog.py') + f = StringIO() + with redirect_stdout(f): + parser.print_help() + assert f.getvalue() == output + + +def test_leadership_modif_leader(json): + output = {'leader.leader': ['192.168.1.1'], + 'leader.follower': [None], + 'leader.follower_boolean': [None], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [[]]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.leader', '192.168.1.1']) + assert config.value.dict() == output + + +def test_leadership_modif_follower(json): + output = {'leader.leader': ['192.168.0.1'], + 'leader.follower': ['255.255.255.0'], + 'leader.follower_boolean': [None], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [[]]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.follower', '0', '255.255.255.0']) + assert config.value.dict() == output + + +def test_leadership_modif_follower_not_submulti(json): + output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--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: unrecognized arguments: 255.255.255.0 +""" + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--leader.follower', '0', '255.255.255.0', '255.255.255.0']) + except SystemExit as err: + assert str(err) == "2" + else: + raise Exception('must raises') + assert f.getvalue() == output + + +def test_leadership_modif_follower_submulti(json): + output = {'leader.leader': ['192.168.0.1'], + 'leader.follower': [None], + 'leader.follower_boolean': [None], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [['255.255.255.0']]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.follower_submulti', '0', '255.255.255.0']) + assert config.value.dict() == output + + +def test_leadership_modif_follower_submulti_multi(json): + output = {'leader.leader': ['192.168.0.1'], + 'leader.follower': [None], + 'leader.follower_boolean': [None], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [['255.255.255.0', '255.255.255.128']]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.follower_submulti', '0', '255.255.255.0', '255.255.255.128']) + assert config.value.dict() == output + + +def test_leadership_modif_follower_bool_true(json): + output = {'leader.leader': ['192.168.0.1'], + 'leader.follower': [None], + 'leader.follower_boolean': [True], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [[]]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.follower_boolean', '0']) + assert config.value.dict() == output + + +def test_leadership_modif_follower_bool_false(json): + output = {'leader.leader': ['192.168.0.1'], + 'leader.follower': [None], + 'leader.follower_boolean': [False], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [[]]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.no-follower_boolean', '0']) + assert config.value.dict() == output + + +def test_leadership_modif_follower_choice(json): + output = {'leader.leader': ['192.168.0.1'], + 'leader.follower': [None], + 'leader.follower_boolean': [None], + 'leader.follower_choice': ['opt1'], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [[]]} + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.follower_choice', '0', 'opt1']) + assert config.value.dict() == output + + +def test_leadership_modif_follower_choice_unknown(json): + output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--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: invalid choice: 'opt_unknown' (choose from 'opt1', 'opt2') +""" + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--leader.follower_choice', '0', 'opt_unknown']) + except SystemExit as err: + assert str(err) == "2" + else: + raise Exception('must raises') + assert f.getvalue() == output + + +def test_leadership_modif_follower_not_number(json): + output = """usage: prog.py [-h] [--leader.leader [LEADER [LEADER ...]]] + [--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: index must be a number, not a +""" + + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--leader.follower', 'a', '255.255.255.0']) + except SystemExit as err: + assert str(err) == "2" + else: + raise Exception('must raises') + assert f.getvalue() == output + + +def test_leadership_modif_multi(json): + output = {'leader.leader': ['192.168.1.1', '10.253.10.1', '192.168.253.1'], + 'leader.follower': ['255.255.255.128', None, '255.255.255.0'], + 'leader.follower_boolean': [None, None, None], + 'leader.follower_choice': [None, None, None], + 'leader.follower_integer': [None, 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']) + assert config.value.dict() == output + + +def test_leadership_modif_mandatory(json): + output = {'leader.leader': ['192.168.1.1'], + 'leader.follower': [None], + 'leader.follower_mandatory': ['255.255.255.128'], + 'leader.follower_boolean': [None], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [['255.255.255.128']]} + output2 = """usage: prog.py --leader.leader ['192.168.1.1'] [-h] + [--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}]] + --leader.follower_mandatory + INDEX FOLLOWER_MANDATORY +prog.py: error: the following arguments are required: --leader.follower_submulti""" + + config = get_config(json, with_mandatory=True) + parser = TiramisuCmdlineParser(config, 'prog.py') + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--leader.leader', '192.168.1.1']) + except SystemExit as err: + assert str(err) == "2" + else: + raise Exception('must raises') + assert f.getvalue() == output2 + ', --leader.follower_mandatory\n' + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--leader.leader', '192.168.1.1', + '--leader.follower_mandatory', '0', '255.255.255.128']) + except SystemExit as err: + assert str(err) == "2" + else: + raise Exception('must raises') + assert f.getvalue() == output2 + '\n' + parser.parse_args(['--leader.leader', '192.168.1.1', + '--leader.follower_submulti', '0', '255.255.255.128', + '--leader.follower_mandatory', '0', '255.255.255.128']) + assert config.value.dict() == output + + +def test_leadership_modif_mandatory_unvalidate(json): + output = {'leader.leader': ['192.168.1.1'], + 'leader.follower': [None], + 'leader.follower_mandatory': [None], + 'leader.follower_boolean': [None], + 'leader.follower_choice': [None], + 'leader.follower_integer': [None], + 'leader.follower_submulti': [[]]} + config = get_config(json, with_mandatory=True) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['--leader.leader', '192.168.1.1'], valid_mandatory=False) + assert config.value.dict() == output diff --git a/test/test_readme.py b/test/test_readme.py index 083d127..aa8dc15 100644 --- a/test/test_readme.py +++ b/test/test_readme.py @@ -1,13 +1,15 @@ from io import StringIO from contextlib import redirect_stdout, redirect_stderr +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 -def get_config(has_tree=False, default_verbosity=False): +def get_config(json, has_tree=False, default_verbosity=False, add_long=False, add_store_false=False): choiceoption = ChoiceOption('cmd', 'choice the sub argument', ('str', 'list', 'int', 'none'), @@ -55,10 +57,22 @@ def get_config(has_tree=False, default_verbosity=False): [root]) config = Config(root) config.property.read_write() - return config + if add_store_false: + config.option('verbosity').property.add('storefalse') + if add_long: + config.option('verbosity').property.add('longargument') + if json == 'tiramisu': + return config + jconfig = JsonConfig(config.option.dict()) + return jconfig -def test_readme_help(): +@pytest.fixture(params=['tiramisu', 'tiramisu-json']) +def json(request): + return request.param + + +def test_readme_help(json): output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} positional arguments: @@ -69,14 +83,14 @@ optional arguments: -v, --verbosity increase output verbosity -nv, --no-verbosity """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stdout(f): parser.print_help() assert f.getvalue() == output -def test_readme_help_tree(): +def test_readme_help_tree(json): output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} optional arguments: @@ -87,14 +101,14 @@ root: -v, --root.verbosity increase output verbosity -nv, --root.no-verbosity """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py') f = StringIO() with redirect_stdout(f): parser.print_help() assert f.getvalue() == output -def test_readme_help_tree_flatten(): +def test_readme_help_tree_flatten(json): output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} optional arguments: @@ -105,14 +119,14 @@ root: -v, --verbosity increase output verbosity -nv, --no-verbosity """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py', fullpath=False) + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False) f = StringIO() with redirect_stdout(f): parser.print_help() assert f.getvalue() == output -def test_readme_help_modif_positional(): +def test_readme_help_modif_positional(json): output = """usage: prog.py str [-h] [-v] [-nv] --str STR optional arguments: @@ -121,7 +135,7 @@ optional arguments: -nv, --no-verbosity --str STR string option """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stdout(f): try: @@ -133,7 +147,7 @@ optional arguments: assert f.getvalue() == output -def test_readme_help_modif(): +def test_readme_help_modif(json): output = """usage: prog.py str --str toto [-h] [-v] [-nv] optional arguments: @@ -141,7 +155,7 @@ optional arguments: -v, --verbosity increase output verbosity -nv, --no-verbosity """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stdout(f): try: @@ -153,14 +167,14 @@ optional arguments: assert f.getvalue() == output -def test_readme_help_modif_short(): +def test_readme_help_modif_short1(json): output = """usage: prog.py str --verbosity [-h] --str STR optional arguments: -h, --help show this help message and exit --str STR string option """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stdout(f): try: @@ -172,14 +186,14 @@ optional arguments: assert f.getvalue() == output -def test_readme_help_modif_short_no(): +def test_readme_help_modif_short_no(json): output = """usage: prog.py str --verbosity [-h] --str STR optional arguments: -h, --help show this help message and exit --str STR string option """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stdout(f): try: @@ -191,11 +205,11 @@ optional arguments: assert f.getvalue() == output -def test_readme_positional_mandatory(): +def test_readme_positional_mandatory(json): output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} prog.py: error: the following arguments are required: cmd """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stderr(f): try: @@ -207,11 +221,11 @@ prog.py: error: the following arguments are required: cmd assert f.getvalue() == output -def test_readme_positional_mandatory_tree(): +def test_readme_positional_mandatory_tree(json): output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} prog.py: error: the following arguments are required: root.cmd """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py') f = StringIO() with redirect_stderr(f): try: @@ -223,11 +237,11 @@ prog.py: error: the following arguments are required: root.cmd assert f.getvalue() == output -def test_readme_positional_mandatory_tree_flatten(): +def test_readme_positional_mandatory_tree_flatten(json): output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} prog.py: error: the following arguments are required: cmd """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py', fullpath=False) + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False) f = StringIO() with redirect_stderr(f): try: @@ -239,11 +253,11 @@ prog.py: error: the following arguments are required: cmd assert f.getvalue() == output -def test_readme_mandatory(): +def test_readme_mandatory(json): output = """usage: prog.py str [-h] [-v] [-nv] --str STR prog.py: error: the following arguments are required: --str """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stderr(f): try: @@ -255,11 +269,11 @@ prog.py: error: the following arguments are required: --str assert f.getvalue() == output -def test_readme_mandatory_tree(): +def test_readme_mandatory_tree(json): output = """usage: prog.py str [-h] [-v] [-nv] --root.str STR prog.py: error: the following arguments are required: --root.str """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py') f = StringIO() with redirect_stderr(f): try: @@ -271,11 +285,11 @@ prog.py: error: the following arguments are required: --root.str assert f.getvalue() == output -def test_readme_mandatory_tree_flatten(): +def test_readme_mandatory_tree_flatten(json): output = """usage: prog.py str [-h] [-v] [-nv] --str STR prog.py: error: the following arguments are required: --str """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py', fullpath=False) + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False) f = StringIO() with redirect_stderr(f): try: @@ -287,11 +301,11 @@ prog.py: error: the following arguments are required: --str assert f.getvalue() == output -def test_readme_cross(): +def test_readme_cross(json): output = """usage: prog.py none [-h] [-v] [-nv] prog.py: error: unrecognized arguments: --int """ - parser = TiramisuCmdlineParser(get_config(), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json), 'prog.py') f = StringIO() with redirect_stderr(f): try: @@ -303,11 +317,11 @@ prog.py: error: unrecognized arguments: --int assert f.getvalue() == output -def test_readme_cross_tree(): +def test_readme_cross_tree(json): output = """usage: prog.py none [-h] [-v] [-nv] prog.py: error: unrecognized arguments: --int """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py') + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py') f = StringIO() with redirect_stderr(f): try: @@ -319,11 +333,11 @@ prog.py: error: unrecognized arguments: --int assert f.getvalue() == output -def test_readme_cross_tree_flatten(): +def test_readme_cross_tree_flatten(json): output = """usage: prog.py none [-h] [-v] [-nv] prog.py: error: unrecognized arguments: --int """ - parser = TiramisuCmdlineParser(get_config(True), 'prog.py', fullpath=False) + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False) f = StringIO() with redirect_stderr(f): try: @@ -335,90 +349,105 @@ prog.py: error: unrecognized arguments: --int assert f.getvalue() == output -def test_readme_int(): +def test_readme_unknown(json): + output = """usage: prog.py [-h] [-v] [-nv] {str,list,int,none} +prog.py: error: argument root.cmd: invalid choice: 'unknown' (choose from 'str', 'list', 'int', 'none') +""" + parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False) + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['unknown']) + except SystemExit as err: + assert str(err) == "2" + else: + raise Exception('must raises') + assert f.getvalue() == output + + +def test_readme_int(json): output = {'cmd': 'int', 'int': 3, 'verbosity': False, 'v': False} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--int', '3']) assert config.value.dict() == output -def test_readme_int_tree(): +def test_readme_int_tree(json): output = {'root.cmd': 'int', 'root.int': 3, 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--root.int', '3']) assert config.value.dict() == output -def test_readme_int_tree_flatten(): +def test_readme_int_tree_flatten(json): output = {'root.cmd': 'int', 'root.int': 3, 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['int', '--int', '3']) assert config.value.dict() == output -def test_readme_int_verbosity(): +def test_readme_int_verbosity(json): output = {'cmd': 'int', 'int': 3, 'verbosity': True, 'v': True} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--int', '3', '--verbosity']) assert config.value.dict() == output -def test_readme_int_verbosity_tree(): +def test_readme_int_verbosity_tree(json): output = {'root.cmd': 'int', 'root.int': 3, 'root.verbosity': True, 'root.v': True} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--root.int', '3', '--root.verbosity']) assert config.value.dict() == output -def test_readme_int_verbosity_tree_flatten(): +def test_readme_int_verbosity_tree_flatten(json): output = {'root.cmd': 'int', 'root.int': 3, 'root.verbosity': True, 'root.v': True} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['int', '--int', '3', '--verbosity']) assert config.value.dict() == output -def test_readme_int_verbosity_short(): +def test_readme_int_verbosity_short(json): output = {'cmd': 'int', 'int': 3, 'verbosity': True, 'v': True} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--int', '3', '-v']) assert config.value.dict() == output -def test_readme_int_verbosity_short_store_false(): +def test_readme_int_verbosity_short_store_false(json): output = {'cmd': 'int', 'int': 3, 'verbosity': None, 'v': True} - config = get_config(default_verbosity=None) - config.option('verbosity').property.add('storefalse') + config = get_config(json, default_verbosity=None, add_store_false=True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--int', '3', '-v']) output = {'cmd': 'int', @@ -434,178 +463,188 @@ def test_readme_int_verbosity_short_store_false(): assert config.value.dict() == output -def test_readme_int_verbosity_short_no(): +def test_readme_int_verbosity_short_no(json): output = {'cmd': 'int', 'int': 3, 'verbosity': False, 'v': False} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--int', '3', '-nv']) assert config.value.dict() == output -def test_readme_int_verbosity_short_tree(): +def test_readme_int_verbosity_short_tree(json): output = {'root.cmd': 'int', 'root.int': 3, 'root.verbosity': True, 'root.v': True} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['int', '--root.int', '3', '-v']) assert config.value.dict() == output -def test_readme_int_verbosity_short_tree_flatten(): +def test_readme_int_verbosity_short_tree_flatten(json): output = {'root.cmd': 'int', 'root.int': 3, 'root.verbosity': True, 'root.v': True} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['int', '--int', '3', '-v']) assert config.value.dict() == output -def test_readme_str(): +def test_readme_int_verbosity_short_and_not(json): + output = {'cmd': 'int', + 'int': 3, + 'verbosity': False, + 'v': False} + config = get_config(json) + parser = TiramisuCmdlineParser(config, 'prog.py') + parser.parse_args(['int', '--int', '3', '-v', '-nv']) + assert config.value.dict() == output + + +def test_readme_str(json): output = {'cmd': 'str', 'str': 'value', 'verbosity': False, 'v': False} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['str', '--str', 'value']) assert config.value.dict() == output -def test_readme_str_tree(): +def test_readme_str_tree(json): output = {'root.cmd': 'str', 'root.str': 'value', 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['str', '--root.str', 'value']) assert config.value.dict() == output -def test_readme_str_tree_flatten(): +def test_readme_str_tree_flatten(json): output = {'root.cmd': 'str', 'root.str': 'value', 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['str', '--str', 'value']) assert config.value.dict() == output -def test_readme_str_int(): +def test_readme_str_int(json): output = {'cmd': 'str', 'str': '3', 'verbosity': False, 'v': False} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['str', '--str', '3']) assert config.value.dict() == output -def test_readme_str_int_tree(): +def test_readme_str_int_tree(json): output = {'root.cmd': 'str', 'root.str': '3', 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['str', '--root.str', '3']) assert config.value.dict() == output -def test_readme_str_int_tree_flatten(): +def test_readme_str_int_tree_flatten(json): output = {'root.cmd': 'str', 'root.str': '3', 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['str', '--str', '3']) assert config.value.dict() == output -def test_readme_list(): +def test_readme_list(json): output = {'cmd': 'list', 'list': ['a', 'b', 'c'], 'verbosity': False, 'v': False} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['list', '--list', 'a', 'b', 'c']) assert config.value.dict() == output -def test_readme_list_tree(): +def test_readme_list_tree(json): output = {'root.cmd': 'list', 'root.list': ['a', 'b', 'c'], 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['list', '--root.list', 'a', 'b', 'c']) assert config.value.dict() == output -def test_readme_list_tree_flatten(): +def test_readme_list_tree_flatten(json): output = {'root.cmd': 'list', 'root.list': ['a', 'b', 'c'], 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['list', '--list', 'a', 'b', 'c']) assert config.value.dict() == output -def test_readme_list_uniq(): +def test_readme_list_uniq(json): output = {'cmd': 'list', 'list': ['a'], 'verbosity': False, 'v': False} - config = get_config() + config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['list', '--list', 'a']) assert config.value.dict() == output -def test_readme_list_uniq_tree(): +def test_readme_list_uniq_tree(json): output = {'root.cmd': 'list', 'root.list': ['a'], 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['list', '--root.list', 'a']) assert config.value.dict() == output -def test_readme_list_uniq_tree_flatten(): +def test_readme_list_uniq_tree_flatten(json): output = {'root.cmd': 'list', 'root.list': ['a'], 'root.verbosity': False, 'root.v': False} - config = get_config(True) + config = get_config(json, True) parser = TiramisuCmdlineParser(config, 'prog.py', fullpath=False) parser.parse_args(['list', '--list', 'a']) assert config.value.dict() == output -def test_readme_longargument(): +def test_readme_longargument(json): output = {'cmd': 'list', 'list': ['a'], 'verbosity': True, 'v': True} - config = get_config() - config.option('verbosity').property.add('longargument') + config = get_config(json, add_long=True) parser = TiramisuCmdlineParser(config, 'prog.py') parser.parse_args(['list', '--list', 'a', '--v']) assert config.value.dict() == output diff --git a/tiramisu_cmdline_parser/api.py b/tiramisu_cmdline_parser/api.py index 571a4b7..9515bd0 100644 --- a/tiramisu_cmdline_parser/api.py +++ b/tiramisu_cmdline_parser/api.py @@ -12,7 +12,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -from typing import Union, List, Optional +from typing import Union, List, Dict, Tuple, Optional, Any from argparse import ArgumentParser, Namespace, SUPPRESS, _HelpAction, HelpFormatter from copy import copy from gettext import gettext as _ @@ -20,10 +20,11 @@ from gettext import gettext as _ try: from tiramisu import Config - from tiramisu.error import PropertiesOptionError + from tiramisu.error import PropertiesOptionError, RequirementError except ModuleNotFoundError: Config = None from tiramisu_json_api.error import PropertiesOptionError + RequirementError = PropertiesOptionError try: from tiramisu_json_api import Config as ConfigJson if Config is None: @@ -33,34 +34,71 @@ except ModuleNotFoundError: class TiramisuNamespace(Namespace): - def _populate(self): - #self._config.property.read_only() + def __init__(self, + config: Config) -> None: + super().__setattr__('_config', config) + super().__setattr__('list_force_no', {}) + self._populate() + super().__init__() + + def _populate(self) -> None: for tiramisu_key, tiramisu_value in self._config.value.dict().items(): option = self._config.option(tiramisu_key) if not option.option.issymlinkoption(): - if tiramisu_value == [] and option.option.ismulti() and option.owner.isdefault(): + if tiramisu_value == [] and \ + option.option.ismulti() and \ + option.owner.isdefault(): tiramisu_value = None super().__setattr__(tiramisu_key, tiramisu_value) - #self._config.property.read_write() - def __init__(self, config): - self._config = config - super().__init__() + def __setattr__(self, + key: str, + value: Any) -> None: + if key in self.list_force_no: + true_key = self.list_force_no[key] + else: + true_key = key + option = self._config.option(true_key) + if option.option.isfollower(): + _setattr = self._setattr_follower + true_value = ','.join(value[1:]) + else: + _setattr = self._setattr + true_value = value + try: + _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()]))) + else: + raise err - def __setattr__(self, key, value): - if key == '_config': - super().__setattr__(key, value) - return - # self._config.property.read_write() - option = self._config.option(key) - if option.option.ismulti() and value is not None and not isinstance(value, list): + def _setattr(self, + option: 'Option', + true_key: str, + key: str, + value: Any) -> None: + if option.option.ismulti() and \ + value is not None and \ + not isinstance(value, list): value = [value] option.value.set(value) - def __getattribute__(self, key): - if key == '__dict__' and hasattr(self, '_config'): - self._populate() - return super().__getattribute__(key) + def _setattr_follower(self, + option: 'Option', + true_key: str, + key: str, + value: Any) -> None: + if not value[0].isdecimal(): + raise ValueError('index must be a number, not {}'.format(value[0])) + index = int(value[0]) + if option.option.type() == 'boolean': + value = key not in self.list_force_no + elif option.option.issubmulti(): + value = value[1:] + else: + value = value[1] + self._config.option(true_key, index).value.set(value) class TiramisuHelpFormatter(HelpFormatter): @@ -81,6 +119,62 @@ class _TiramisuHelpAction(_HelpAction): _HelpAction.__call__(self, parser, None, None) +class _BuildKwargs: + def __init__(self, + name: str, + option: 'Option', + cmdlineparser: 'TiramisuCmdlineParser', + properties: List[str], + force_no: bool) -> None: + self.kwargs = {} + self.cmdlineparser = cmdlineparser + self.properties = properties + self.force_no = force_no + if not self.force_no: + 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() + else: + ga_name = name + self.kwargs['dest'] = self.gen_argument_name(option.path(), False) + self.args = [self.cmdlineparser._gen_argument(ga_name, is_short_name)] + else: + self.args = [option.path()] + + def __setitem__(self, + key: str, + value: Any) -> None: + self.kwargs[key] = value + + def add_argument(self, + option: 'Option'): + 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) + else: + name = option.name() + self.args.insert(0, self.cmdlineparser._gen_argument(name, is_short_name)) + + def gen_argument_name(self, name, is_short_name): + if self.force_no: + if is_short_name: + prefix = 'n' + else: + prefix = 'no-' + if '.' in name: + sname = name.rsplit('.', 1) + name = sname[0] + '.' + prefix + sname[1] + else: + name = prefix + name + return name + + def get(self) -> Tuple[Dict]: + return self.args, self.kwargs + + class TiramisuCmdlineParser(ArgumentParser): def __init__(self, config: Union[Config, ConfigJson], @@ -91,6 +185,7 @@ class TiramisuCmdlineParser(ArgumentParser): self.fullpath = fullpath self.config = config kwargs['formatter_class'] = TiramisuHelpFormatter + self.namespace = TiramisuNamespace(self.config) super().__init__(*args, **kwargs) self.register('action', 'help', _TiramisuHelpAction) self._config_to_argparser(_forhelp, @@ -118,9 +213,19 @@ class TiramisuCmdlineParser(ArgumentParser): actions.pop(0) return super()._match_arguments_partial(actions, arg_string_pattern) - def _parse_known_args(self, args=None, namespace=None): + def _is_short_name(self, name, longargument): + return len(name) == 1 and not longargument - namespace_, args_ = super()._parse_known_args(args, namespace) + def _gen_argument(self, name, is_short_name): + if is_short_name: + return self.prefix_chars + name + return self.prefix_chars * 2 + name + + def _parse_known_args(self, args=None, namespace=None): + try: + namespace_, args_ = super()._parse_known_args(args, namespace) + except ValueError as err: + self.error(err) if args != args_ and args_ and args_[0].startswith(self.prefix_chars): # option that was disabled are no more disable # so create a new parser @@ -148,21 +253,39 @@ class TiramisuCmdlineParser(ArgumentParser): def add_subparsers(self, *args, **kwargs): raise NotImplementedError(_('do not use add_subparsers')) - def _gen_argument(self, name, longargument, no_prefix=False): - shortarg = len(name) == 1 and not longargument - if no_prefix: - if shortarg: - prefix = 'n' + def _option_is_not_default(self, + properties, + type, + name, + value): + if 'positional' not in properties: + is_short_name = self._is_short_name(name, 'longargument' in properties) + self.prog += ' {}'.format(self._gen_argument(name, is_short_name)) + if type != 'boolean': + self.prog += f' {value}' + + def _config_list(self, + config: Config, + prefix: Optional[str], + _forhelp: bool, + group): + for obj in config.list(type='all'): + # do not display frozen option + if 'frozen' in obj.option.properties(): + continue + if obj.option.isoptiondescription(): + if _forhelp: + group = self.add_argument_group(obj.option.doc()) + if prefix: + prefix_ = prefix + '.' + obj.option.name() + else: + prefix_ = obj.option.path() + self._config_to_argparser(_forhelp, obj, prefix_, group) + elif obj.option.type() == 'boolean' and not obj.option.issymlinkoption(): + yield obj, False + yield obj, True else: - prefix = 'no-' - if '.' in name: - sname = name.rsplit('.', 1) - name = sname[0] + '.' + prefix + sname[1] - else: - name = prefix + name - if shortarg: - return self.prefix_chars + name - return self.prefix_chars * 2 + name + yield obj, None def _config_to_argparser(self, _forhelp: bool, @@ -172,107 +295,128 @@ class TiramisuCmdlineParser(ArgumentParser): if group is None: group = super() actions = {} - for obj in config.list(type='all'): + leadership_len = None + for obj, force_no in self._config_list(config, prefix, _forhelp, group): option = obj.option - if option.isoptiondescription(): - if _forhelp: - group = self.add_argument_group(option.doc()) - if prefix: - prefix_ = prefix + '.' + option.name() - else: - prefix_ = option.path() - self._config_to_argparser(_forhelp, obj, prefix_, group) - continue - if 'frozen' in option.properties(): - continue + if option.isleader(): + value = obj.value.get() + leadership_len = len(value) + elif option.isfollower(): + value = [] + for index in range(leadership_len): + value.append(self.config.option(obj.option.path(), index).value.get()) + else: + value = obj.value.get() name = option.name() if name.startswith(self.prefix_chars): raise ValueError(_('name cannot startswith "{}"').format(self.prefix_chars)) if self.fullpath and prefix: name = prefix + '.' + name - properties = obj.property.get() - kwargs = {'help': option.doc().replace('%', '%%')} + if option.isfollower(): + properties = obj.option.properties() + else: + properties = obj.property.get() if option.issymlinkoption(): symlink_name = option.name(follow_symlink=True) if symlink_name in actions: - actions[symlink_name][0][0].insert(0, self._gen_argument(option.name(), 'longargument' in properties)) - if len(actions[symlink_name]) == 2: - actions[symlink_name][1][0].insert(0, self._gen_argument(option.name(), False, True)) + for action in actions[symlink_name]: + action.add_argument(option) continue - if _forhelp and not obj.owner.isdefault() and obj.value.get() is not None: - if 'positional' not in properties: - self.prog += ' {}'.format(self._gen_argument(name, 'longargument' in properties)) - if option.type() != 'boolean': - self.prog += ' {}'.format(obj.value.get()) + kwargs = _BuildKwargs(name, option, self, properties, force_no) + 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, + option.type(), + name, + value) else: if 'positional' in properties: if option.type() == 'boolean': raise ValueError(_('boolean option must not be positional')) - # if not 'mandatory' in properties: - # raise ValueError('"positional" argument must be "mandatory" too') - args = [option.path()] + if not 'mandatory' in properties: + raise ValueError('"positional" argument must be "mandatory" too') if _forhelp: kwargs['default'] = obj.value.default() else: - kwargs['default'] = obj.value.get() + kwargs['default'] = value kwargs['nargs'] = '?' else: - kwargs['dest'] = option.path() kwargs['default'] = SUPPRESS if _forhelp and 'mandatory' in properties: kwargs['required'] = True - if option.type() == 'boolean': + if option.type() == 'boolean' and not option.isfollower(): if 'storefalse' in properties: + if force_no: + action = 'store_true' + else: + action = 'store_false' + elif force_no: action = 'store_false' - no_action = 'store_true' else: action = 'store_true' - no_action = 'store_false' kwargs['action'] = action - args = [self._gen_argument(name, 'longargument' in properties)] - # - nkwargs = copy(kwargs) - nkwargs['action'] = no_action - del nkwargs['help'] - nargs = [self._gen_argument(name, 'longargument' in properties, True)] - actions[option.name()] = [(args, kwargs), (nargs, nkwargs)] - continue - args = [self._gen_argument(name, 'longargument' in properties)] - if _forhelp: - value = obj.value.default() - else: - value = obj.value.get() - if value not in [None, []]: - #kwargs['default'] = kwargs['const'] = option.default() - #kwargs['action'] = 'store_const' - kwargs['nargs'] = '?' - if option.ismulti(): - if _forhelp and 'mandatory' in properties: - kwargs['nargs'] = '+' else: - kwargs['nargs'] = '*' - if option.type() == 'string': - pass - elif option.type() == 'integer': - kwargs['type'] = int - elif option.type() == 'choice': - kwargs['choices'] = obj.value.list() - else: - pass - #raise NotImplementedError('not supported yet') - actions[option.name()] = [(args, kwargs)] + 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 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(): + kwargs['nargs'] = '+' + else: + kwargs['nargs'] = 2 + if _forhelp and 'mandatory' not in properties: + metavar = f'[{metavar}]' + if option.type() == 'choice': + choice_list = obj.value.list() + if choice_list[0] == '': + del choice_list[0] + choices = '{{{}}}'.format(','.join(choice_list)) + if 'mandatory' not in properties: + choices = f'[{choices}]' + kwargs['metavar'] = ('INDEX', choices) + else: + kwargs['metavar'] = ('INDEX', metavar) + if option.type() == 'string': + pass + elif option.type() == 'integer' or option.type() == 'boolean': + # when boolean we are here only if follower + kwargs['type'] = int + if _forhelp and option.type() == 'boolean': + kwargs['metavar'] = 'INDEX' + kwargs['nargs'] = 1 + elif option.type() == 'choice' and not option.isfollower(): + kwargs['choices'] = obj.value.list() + else: + pass + #raise NotImplementedError('not supported yet') + actions.setdefault(option.name(), []).append(kwargs) for values in actions.values(): - for args, kwargs in values: + for value in values: + args, kwargs = value.get() group.add_argument(*args, **kwargs) + def _valid_mandatory(self): + pass def parse_args(self, *args, valid_mandatory=True, **kwargs): - kwargs['namespace'] = TiramisuNamespace(self.config) + kwargs['namespace'] = self.namespace try: namespaces = super().parse_args(*args, **kwargs) - del namespaces.__dict__['_config'] except PropertiesOptionError as err: name = err._option_bag.option.impl_getname() properties = self.config.option(name).property.get() @@ -286,19 +430,31 @@ class TiramisuCmdlineParser(ArgumentParser): else: self.error('unrecognized arguments: {}'.format(name)) if valid_mandatory: + errors = [] for key in self.config.value.mandatory(): - properties = self.config.option(key).property.get() - if 'positional' not in properties: - if self.fullpath or '.' not in key: - name = key + properties = self.config.option(key).option.properties() + if not self.config.option(key).option.isfollower(): + if 'positional' not in properties: + if self.fullpath or '.' not in key: + name = key + else: + name = key.rsplit('.', 1)[1] + is_short_name = self._is_short_name(name, 'longargument' in self.config.option(key).property.get()) + args = self._gen_argument(name, is_short_name) else: - name = key.rsplit('.', 1)[1] - args = self._gen_argument(name, 'longargument' in self.config.option(key).property.get()) + args = key else: - args = key + if 'positional' not in properties: + args = self._gen_argument(key, False) + else: + args = key if not self.fullpath and '.' in args: args = args.rsplit('.', 1)[1] - self.error('the following arguments are required: {}'.format(args)) + if 'positional' not in properties: + args = self._gen_argument(args, False) + errors.append(args) + if errors: + self.error('the following arguments are required: {}'.format(', '.join(errors))) return namespaces def format_usage(self,