From 9da6c6f421d89948462ac77affbc75d61433c688 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Sun, 28 Sep 2025 20:08:12 +0200 Subject: [PATCH] fix: better support for boolean help --- tests/test_boolean.py | 146 +++++++++++++++++++++++++++++++++ tests/test_choice.py | 10 +-- tests/test_readme.py | 14 ++-- tiramisu_cmdline_parser/api.py | 9 +- 4 files changed, 163 insertions(+), 16 deletions(-) create mode 100644 tests/test_boolean.py diff --git a/tests/test_boolean.py b/tests/test_boolean.py new file mode 100644 index 0000000..34c43bd --- /dev/null +++ b/tests/test_boolean.py @@ -0,0 +1,146 @@ +from io import StringIO +from contextlib import redirect_stdout, redirect_stderr +import pytest + + +from tiramisu_cmdline_parser import TiramisuCmdlineParser +from tiramisu import BoolOption, OptionDescription, Config +from .utils import TestHelpFormatter, to_dict + + +def get_config(has_tree=False, default_verbosity=False): + booloption = BoolOption('disabled', + 'disabled', + properties=('disabled',), + ) + booloption2 = BoolOption('verbosity', + 'increase output verbosity', + default=default_verbosity, + ) + root = OptionDescription('root', + 'root', + [booloption, booloption2], + ) + if has_tree: + root = OptionDescription('root', + 'root', + [root], + ) + config = Config(root) + config.property.read_write() + return config + + +def test_boolean_help_tree(): + output = """usage: prog.py [-h] [--root.verbosity] [--root.no-verbosity] + +options: + -h, --help show this help message and exit + +root: + --root.verbosity increase output verbosity (default: False) + --root.no-verbosity +""" + parser = TiramisuCmdlineParser(get_config(has_tree=True), 'prog.py', formatter_class=TestHelpFormatter) + f = StringIO() + with redirect_stdout(f): + parser.print_help() + assert f.getvalue() == output + + +def test_boolean_help(): + output = """usage: prog.py [-h] [--verbosity] [--no-verbosity] + +options: + -h, --help show this help message and exit + --verbosity increase output verbosity (default: False) + --no-verbosity +""" + parser = TiramisuCmdlineParser(get_config(), 'prog.py', formatter_class=TestHelpFormatter) + f = StringIO() + with redirect_stdout(f): + parser.print_help() + assert f.getvalue() == output + + +def test_boolean_help2(): + output = """usage: prog.py [-h] [--verbosity] [--no-verbosity] + +options: + -h, --help show this help message and exit + --verbosity increase output verbosity (default: True) + --no-verbosity +""" + parser = TiramisuCmdlineParser(get_config(default_verbosity=True), 'prog.py', formatter_class=TestHelpFormatter) + f = StringIO() + with redirect_stdout(f): + parser.print_help() + assert f.getvalue() == output + + +def test_boolean_true(): + config = get_config(default_verbosity=True) + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + assert to_dict(config.value.get()) == {'verbosity': True} + + +def test_boolean_false(): + config = get_config() + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + assert to_dict(config.value.get()) == {'verbosity': False} + + +def test_boolean_true_to_false(): + config = get_config(default_verbosity=True) + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + parser.parse_args(['--no-verbosity']) + assert to_dict(config.value.get()) == {'verbosity': False} + + +def test_boolean_true_to_true(): + config = get_config(default_verbosity=True) + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + parser.parse_args(['--verbosity']) + assert to_dict(config.value.get()) == {'verbosity': True} + + +def test_boolean_false_to_true(): + config = get_config() + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + parser.parse_args(['--verbosity']) + assert to_dict(config.value.get()) == {'verbosity': True} + + +def test_boolean_false_to_false(): + config = get_config() + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + parser.parse_args(['--verbosity']) + assert to_dict(config.value.get()) == {'verbosity': True} + + +def test_boolean_disabled(): + config = get_config(default_verbosity=True) + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--disabled']) + except SystemExit as err: + assert str(err) == "2" + assert f.getvalue() == """usage: prog.py [-h] [--verbosity] [--no-verbosity] +prog.py: error: unrecognized arguments: --disabled (cannot access to option "disabled" because has property "disabled") +""" + + +def test_boolean_no_disabled(): + config = get_config(default_verbosity=True) + parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) + f = StringIO() + with redirect_stderr(f): + try: + parser.parse_args(['--no-disabled']) + except SystemExit as err: + assert str(err) == "2" + assert f.getvalue() == """usage: prog.py [-h] [--verbosity] [--no-verbosity] +prog.py: error: unrecognized arguments: --no-disabled (cannot access to option "disabled" because has property "disabled") +""" diff --git a/tests/test_choice.py b/tests/test_choice.py index 750252d..9d0a241 100644 --- a/tests/test_choice.py +++ b/tests/test_choice.py @@ -52,10 +52,10 @@ def json(request): def test_choice_positional(json): output1 = '''usage: prog.py "str" "1" [-h] [--str {str1,str2,str3}] [--int {1,2,3}] [--int_multi [{1,2,3} ...]] {str,list,int,none} {1,2,3} -prog.py: error: argument positional: invalid choice: 'error' (choose from 'str', 'list', 'int', 'none') +prog.py: error: argument positional: invalid choice: 'error' (choose from str, list, int, none) ''' output2 = '''usage: prog.py "str" "1" [-h] [--str {str1,str2,str3}] [--int {1,2,3}] [--int_multi [{1,2,3} ...]] {str,list,int,none} {1,2,3} -prog.py: error: argument positional_int: invalid choice: '4' (choose from '1', '2', '3') +prog.py: error: argument positional_int: invalid choice: '4' (choose from 1, 2, 3) ''' config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) @@ -88,7 +88,7 @@ prog.py: error: argument positional_int: invalid choice: '4' (choose from '1', ' def test_choice_str(json): output = """usage: prog.py "str" "1" --str "str3" [-h] [--str {str1,str2,str3}] [--int {1,2,3}] [--int_multi [{1,2,3} ...]] {str,list,int,none} {1,2,3} -prog.py: error: argument --str: invalid choice: 'error' (choose from 'str1', 'str2', 'str3') +prog.py: error: argument --str: invalid choice: 'error' (choose from str1, str2, str3) """ config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) @@ -128,7 +128,7 @@ prog.py: error: argument --str: invalid choice: 'error' (choose from 'str1', 'st def test_choice_int(json): output = """usage: prog.py "str" "1" --int "1" [-h] [--str {str1,str2,str3}] [--int {1,2,3}] [--int_multi [{1,2,3} ...]] {str,list,int,none} {1,2,3} -prog.py: error: argument --int: invalid choice: '4' (choose from '1', '2', '3') +prog.py: error: argument --int: invalid choice: '4' (choose from 1, 2, 3) """ config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) @@ -156,7 +156,7 @@ prog.py: error: argument --int: invalid choice: '4' (choose from '1', '2', '3') def test_choice_int_multi(json): output = """usage: prog.py "str" "1" --int_multi "1" "2" [-h] [--str {str1,str2,str3}] [--int {1,2,3}] [--int_multi [{1,2,3} ...]] {str,list,int,none} {1,2,3} -prog.py: error: argument --int_multi: invalid choice: '4' (choose from '1', '2', '3') +prog.py: error: argument --int_multi: invalid choice: '4' (choose from 1, 2, 3) """ config = get_config(json) parser = TiramisuCmdlineParser(config, 'prog.py', formatter_class=TestHelpFormatter) diff --git a/tests/test_readme.py b/tests/test_readme.py index 893976b..ac7baa6 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -466,7 +466,7 @@ prog.py: error: the following arguments are required: --str def test_readme_cross(json): output = """usage: prog.py "none" [-h] [-v] [-nv] {str,list,int,none} -prog.py: error: unrecognized arguments: --int +prog.py: error: unrecognized arguments: --int (cannot access to option "int option" because has property "disabled") """ parser = TiramisuCmdlineParser(get_config(json), 'prog.py', formatter_class=TestHelpFormatter) f = StringIO() @@ -482,7 +482,7 @@ prog.py: error: unrecognized arguments: --int def test_readme_cross_remove(json): output = """usage: prog.py "none" [-h] [-v] [-nv] -prog.py: error: unrecognized arguments: --int +prog.py: error: unrecognized arguments: --int (cannot access to option "int option" because has property "disabled") """ parser = TiramisuCmdlineParser(get_config(json), 'prog.py', display_modified_value=False, formatter_class=TestHelpFormatter) f = StringIO() @@ -498,7 +498,7 @@ prog.py: error: unrecognized arguments: --int def test_readme_cross_tree(json): output = """usage: prog.py "none" [-h] [-v] [-nv] {str,list,int,none} -prog.py: error: unrecognized arguments: --root.int +prog.py: error: unrecognized arguments: --root.int (cannot access to option "int option" because has property "disabled") """ parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', formatter_class=TestHelpFormatter) f = StringIO() @@ -514,7 +514,7 @@ prog.py: error: unrecognized arguments: --root.int def test_readme_cross_tree_remove(json): output = """usage: prog.py "none" [-h] [-v] [-nv] -prog.py: error: unrecognized arguments: --root.int +prog.py: error: unrecognized arguments: --root.int (cannot access to option "int option" because has property "disabled") """ parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', display_modified_value=False, formatter_class=TestHelpFormatter) f = StringIO() @@ -530,7 +530,7 @@ prog.py: error: unrecognized arguments: --root.int def test_readme_cross_tree_flatten(json): output = """usage: prog.py "none" [-h] [-v] [-nv] {str,list,int,none} -prog.py: error: unrecognized arguments: --int +prog.py: error: unrecognized arguments: --int (cannot access to option "int option" because has property "disabled") """ parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False, formatter_class=TestHelpFormatter) f = StringIO() @@ -546,7 +546,7 @@ prog.py: error: unrecognized arguments: --int def test_readme_cross_tree_flatten_remove(json): output = """usage: prog.py "none" [-h] [-v] [-nv] -prog.py: error: unrecognized arguments: --int +prog.py: error: unrecognized arguments: --int (cannot access to option "int option" because has property "disabled") """ parser = TiramisuCmdlineParser(get_config(json, True), 'prog.py', fullpath=False, display_modified_value=False, formatter_class=TestHelpFormatter) f = StringIO() @@ -562,7 +562,7 @@ prog.py: error: unrecognized arguments: --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') +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, formatter_class=TestHelpFormatter) f = StringIO() diff --git a/tiramisu_cmdline_parser/api.py b/tiramisu_cmdline_parser/api.py index 523a060..130b957 100644 --- a/tiramisu_cmdline_parser/api.py +++ b/tiramisu_cmdline_parser/api.py @@ -179,9 +179,9 @@ class TiramisuNamespace(Namespace): value = [value] try: option.value.set(value) - except PropertiesOptionError: + except PropertiesOptionError as err: raise AttributeError( - "unrecognized arguments: {}".format(self.arguments[key]) + "unrecognized arguments: {} ({})".format(self.arguments[key], err) ) def _setattr_follower( @@ -272,11 +272,12 @@ class _BuildKwargs: self.cmdlineparser.namespace.list_force_del[ga_path] = option.path() else: ga_name = name + ga_path = option.path() self.kwargs["dest"] = gen_argument_name( option.path(), False, self.force_no, self.force_del ) argument = self.cmdlineparser._gen_argument(ga_name, is_short_name) - self.cmdlineparser.namespace.arguments[option.path()] = argument + self.cmdlineparser.namespace.arguments[ga_path] = argument self.args = [argument] self.ga_name = ga_name else: @@ -707,7 +708,7 @@ class TiramisuCmdlineParser(ArgumentParser): if err.proptype == ["mandatory"]: self.error("the following arguments are required: {}".format(name)) else: - self.error("unrecognized arguments: {}".format(name)) + self.error("unrecognized arguments: {} ({})".format(name, err)) if valid_mandatory: errors = [] for option in self.config.value.mandatory():