diff --git a/CHANGES b/CHANGES index e6169d9..703a60a 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,15 @@ Backwards incompatible changes: instead of returning without error. For most users, this means failed commands will now exit with a failure status instead of a success. (#161) +Deprecated: + +- Renamed arguments in `add_commands()` (#165): + + - `namespace` → `group_name` + - `namespace_kwargs` → `group_kwargs` + + The old names are deprecated and will be removed in v.0.30. + Enhancements: - Can control exit status (see Backwards Incompatible Changes above) when raising diff --git a/docs/source/subparsers.rst b/docs/source/subparsers.rst index d2dc88f..aeab975 100644 --- a/docs/source/subparsers.rst +++ b/docs/source/subparsers.rst @@ -2,7 +2,7 @@ Subparsers ~~~~~~~~~~ The statement ``parser.add_commands([bar, quux])`` builds two subparsers named -`bar` and `quux`. A "subparser" is an argument parser bound to a namespace. In +`bar` and `quux`. A "subparser" is an argument parser bound to a group name. In other words, it works with everything after a certain positional argument. `Argh` implements commands by creating a subparser for every function. @@ -31,7 +31,7 @@ The equivalent code without `Argh` would be:: Now consider this expression:: parser = ArghParser() - parser.add_commands([bar, quux], namespace='foo') + parser.add_commands([bar, quux], group_name='foo') parser.dispatch() It produces a command hierarchy for the command-line expressions ``foo bar`` diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index b82467e..0c34420 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -233,10 +233,9 @@ The commands will be accessible under the related functions' names:: Subcommands ........... -If the application has too many commands, they can be grouped into namespaces:: +If the application has too many commands, they can be grouped:: - argh.add_commands(parser, [serve, ping], namespace='www', - title='Web-related commands') + argh.add_commands(parser, [serve, ping], group_name='www') The resulting CLI is as follows:: diff --git a/src/argh/assembling.py b/src/argh/assembling.py index 3d95134..6ddb150 100644 --- a/src/argh/assembling.py +++ b/src/argh/assembling.py @@ -308,13 +308,15 @@ def set_default_command(parser, function): def add_commands( parser, functions, - namespace=None, - namespace_kwargs=None, + group_name=None, + group_kwargs=None, func_kwargs=None, # deprecated args: title=None, description=None, help=None, + namespace=None, + namespace_kwargs=None, ): """ Adds given functions as commands to given parser. @@ -333,13 +335,13 @@ def add_commands( function name. Note that the underscores in the name are replaced with hyphens, i.e. function name "foo_bar" becomes command name "foo-bar". - :param namespace: + :param group_name: an optional string representing the group of commands. For example, if - a command named "hello" is added without the namespace, it will be - available as "prog.py hello"; if the namespace if specified as "greet", + a command named "hello" is added without the group name, it will be + available as "prog.py hello"; if the group name if specified as "greet", then the command will be accessible as "prog.py greet hello". The - namespace itself is not callable, so "prog.py greet" will fail and only + group itself is not callable, so "prog.py greet" will fail and only display a help message. :param func_kwargs: @@ -349,12 +351,28 @@ def add_commands( dictionary have the highest priority, so a function's docstring is overridden by a `help` in `func_kwargs` (if present). - :param namespace_kwargs: + :param group_kwargs: a `dict` of keyword arguments to be passed to the nested ArgumentParser - instance under given `namespace`. + instance under given `group_name`. + + Deprecated params that should be renamed: + + :param namespace: + + .. deprecated:: 0.29.0 + + This argument will be removed in Argh v.0.30. + Please use `group_name` instead. + + :param namespace_kwargs: - Deprecated params that should be moved into `namespace_kwargs`: + .. deprecated:: 0.29.0 + + This argument will be removed in Argh v.0.30. + Please use `group_kwargs` instead. + + Deprecated params that should be moved into `group_kwargs`: :param title: @@ -392,13 +410,27 @@ def add_commands( results in `AssemblingError`. """ - # FIXME "namespace" is a correct name but it clashes with the "namespace" - # that represents arguments (argparse.Namespace and our ArghNamespace). - # We should rename the argument here. - namespace_kwargs = namespace_kwargs or {} + group_kwargs = group_kwargs or {} - # TODO remove this in 0.30 + # ------------------------------------------------------------------------ + # TODO remove all of these in 0.30 # + if namespace: + warnings.warn( + "Argument `namespace` is deprecated in add_commands(), " + + "it will be removed in Argh 0.30. " + + "Please use `group_name` instead.", + DeprecationWarning, + ) + group_name = namespace + if namespace_kwargs: + warnings.warn( + "Argument `namespace_kwargs` is deprecated in add_commands(), " + + "it will be removed in Argh 0.30. " + + "Please use `group_kwargs` instead.", + DeprecationWarning, + ) + group_kwargs = namespace_kwargs if title: warnings.warn( "Argument `title` is deprecated in add_commands(), " @@ -406,7 +438,7 @@ def add_commands( + "Please use `parser_kwargs` instead.", DeprecationWarning, ) - namespace_kwargs["description"] = title + group_kwargs["description"] = title if help: warnings.warn( "Argument `help` is deprecated in add_commands(), " @@ -414,7 +446,7 @@ def add_commands( + "Please use `parser_kwargs` instead.", DeprecationWarning, ) - namespace_kwargs["help"] = help + group_kwargs["help"] = help if description: warnings.warn( "Argument `description` is deprecated in add_commands(), " @@ -422,28 +454,28 @@ def add_commands( + "Please use `parser_kwargs` instead.", DeprecationWarning, ) - namespace_kwargs["description"] = description + group_kwargs["description"] = description # - # / + # ------------------------------------------------------------------------ subparsers_action = get_subparsers(parser, create=True) - if namespace: + if group_name: # Make a nested parser and init a deeper _SubParsersAction under it. # Create a named group of commands. It will be listed along with # root-level commands in ``app.py --help``; in that context its `title` # can be used as a short description on the right side of its name. # Normally `title` is shown above the list of commands - # in ``app.py my-namespace --help``. + # in ``app.py my-group --help``. subsubparser_kw = { - "help": namespace_kwargs.get("title"), + "help": group_kwargs.get("title"), } - subsubparser = subparsers_action.add_parser(namespace, **subsubparser_kw) - subparsers_action = subsubparser.add_subparsers(**namespace_kwargs) + subsubparser = subparsers_action.add_parser(group_name, **subsubparser_kw) + subparsers_action = subsubparser.add_subparsers(**group_kwargs) else: - if namespace_kwargs: - raise ValueError("`parser_kwargs` only makes sense " "with `namespace`.") + if group_kwargs: + raise ValueError("`group_kwargs` only makes sense with `group_name`.") for func in functions: cmd_name, func_parser_kwargs = _extract_command_meta_from_func(func) @@ -474,23 +506,21 @@ def _extract_command_meta_from_func(func): return cmd_name, func_parser_kwargs -def add_subcommands(parser, namespace, functions, **namespace_kwargs): +def add_subcommands(parser, group_name, functions, **group_kwargs): """ A wrapper for :func:`add_commands`. These examples are equivalent:: - add_commands(parser, [get, put], namespace='db', - namespace_kwargs={ - 'title': 'database commands', - 'help': 'CRUD for our silly database' + add_commands(parser, [get, put], group_name="db", + group_kwargs={ + "title": "database commands", + "help": "CRUD for our silly database" }) - add_subcommands(parser, 'db', [get, put], - title='database commands', - help='CRUD for our silly database') + add_subcommands(parser, "db", [get, put], + title="database commands", + help="CRUD for our database") """ - add_commands( - parser, functions, namespace=namespace, namespace_kwargs=namespace_kwargs - ) + add_commands(parser, functions, group_name=group_name, group_kwargs=group_kwargs) diff --git a/tests/test_assembling.py b/tests/test_assembling.py index 59077e7..fc5d5bf 100644 --- a/tests/test_assembling.py +++ b/tests/test_assembling.py @@ -143,14 +143,14 @@ def three(): assert ns_default.get_function() == three -def test_add_command_with_namespace_kwargs_but_no_namespace_name(): +def test_add_command_with_group_kwargs_but_no_group_name(): def one(): return 1 p = argh.ArghParser() - err_msg = "`parser_kwargs` only makes sense with `namespace`" + err_msg = "`group_kwargs` only makes sense with `group_name`" with pytest.raises(ValueError, match=err_msg): - p.add_commands([one], namespace_kwargs={"help": "foo"}) + p.add_commands([one], group_kwargs={"help": "foo"}) def test_set_default_command_mixed_arg_types(): @@ -287,18 +287,18 @@ def test_set_default_command_deprecation_warnings(): with pytest.warns( DeprecationWarning, match="Argument `title` is deprecated in add_commands()" ): - argh.add_commands(parser, [], namespace="a", title="bar") + argh.add_commands(parser, [], group_name="a", title="bar") with pytest.warns( DeprecationWarning, match="Argument `description` is deprecated in add_commands()", ): - argh.add_commands(parser, [], namespace="b", description="bar") + argh.add_commands(parser, [], group_name="b", description="bar") with pytest.warns( DeprecationWarning, match="Argument `help` is deprecated in add_commands()" ): - argh.add_commands(parser, [], namespace="c", help="bar") + argh.add_commands(parser, [], group_name="c", help="bar") @mock.patch("argh.assembling.add_commands") @@ -319,8 +319,8 @@ def get_items(): mock_add_commands.assert_called_with( mock_parser, [get_items], - namespace="db", - namespace_kwargs={ + group_name="db", + group_kwargs={ "title": "database commands", "help": "CRUD for our silly database", }, diff --git a/tests/test_integration.py b/tests/test_integration.py index 6446342..e7831d9 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -378,7 +378,7 @@ def cmd(args): # nested command p = DebugArghParser() - p.add_commands([cmd], namespace="nest") + p.add_commands([cmd], group_name="nest") assert "invalid choice" in run(p, "nest bar", exit=True) @@ -432,14 +432,40 @@ def parrot(dead=False): assert run(p, "parrot --dead").out == "this parrot is no more\n" -def test_bare_namespace(): +def test_bare_group_name(): + "A command can be resolved to a function, not a group_name." + + def hello(): + return "hello world" + + p = DebugArghParser() + p.add_commands([hello], group_name="greet") + + # without arguments + + # returns a help message and doesn't exit + assert "usage:" in run(p, "greet").out + + # with an argument + + # exits with an informative error + message = "unrecognized arguments: --name=world" + assert run(p, "greet --name=world", exit=True) == message + + +# TODO: remove in v.0.30 +def test_bare_group_name__deprecated_arg(): "A command can be resolved to a function, not a namespace." def hello(): return "hello world" p = DebugArghParser() - p.add_commands([hello], namespace="greet") + with pytest.warns( + DeprecationWarning, + match=r"Argument `namespace` is deprecated .+ it will be removed in Argh 0\.30", + ): + p.add_commands([hello], namespace="greet") # without arguments @@ -453,7 +479,31 @@ def hello(): assert run(p, "greet --name=world", exit=True) == message -def test_namespaced_function(): +def test_function_under_group_name(): + "A subcommand is resolved to a function." + + def hello(name="world"): + return "Hello {0}!".format(name or "world") + + def howdy(buddy): + return "Howdy {0}?".format(buddy) + + p = DebugArghParser() + p.add_commands([hello, howdy], group_name="greet") + + assert run(p, "greet hello").out == "Hello world!\n" + assert run(p, "greet hello --name=John").out == "Hello John!\n" + assert run(p, "greet hello John", exit=True) == "unrecognized arguments: John" + + # exits with an informative error + message = "the following arguments are required: buddy" + + assert message in run(p, "greet howdy --name=John", exit=True) + assert run(p, "greet howdy John").out == "Howdy John?\n" + + +# TODO: remove in v.0.30 +def test_namespaced_function__deprecated_arg(): "A subcommand is resolved to a function." def hello(name="world"): @@ -463,7 +513,11 @@ def howdy(buddy): return "Howdy {0}?".format(buddy) p = DebugArghParser() - p.add_commands([hello, howdy], namespace="greet") + with pytest.warns( + DeprecationWarning, + match=r"Argument `namespace` is deprecated .+ it will be removed in Argh 0\.30", + ): + p.add_commands([hello, howdy], namespace="greet") assert run(p, "greet hello").out == "Hello world!\n" assert run(p, "greet hello --name=John").out == "Hello John!\n" @@ -574,7 +628,7 @@ def whiner_iterable(): ) -def test_custom_namespace(): +def test_custom_argparse_namespace(): @argh.expects_obj def cmd(args): return args.custom_value @@ -588,25 +642,25 @@ def cmd(args): @pytest.mark.parametrize( - "namespace_class", [argparse.Namespace, argh.dispatching.ArghNamespace] + "argparse_namespace_class", [argparse.Namespace, argh.dispatching.ArghNamespace] ) -def test_get_function_from_namespace_obj(namespace_class): - namespace = namespace_class() +def test_get_function_from_namespace_obj(argparse_namespace_class): + argparse_namespace = argparse_namespace_class() def func(): pass - retval = argh.dispatching._get_function_from_namespace_obj(namespace) + retval = argh.dispatching._get_function_from_namespace_obj(argparse_namespace) assert retval is None - setattr(namespace, argh.constants.DEST_FUNCTION, "") + setattr(argparse_namespace, argh.constants.DEST_FUNCTION, "") - retval = argh.dispatching._get_function_from_namespace_obj(namespace) + retval = argh.dispatching._get_function_from_namespace_obj(argparse_namespace) assert retval is None - setattr(namespace, argh.constants.DEST_FUNCTION, func) + setattr(argparse_namespace, argh.constants.DEST_FUNCTION, func) - retval = argh.dispatching._get_function_from_namespace_obj(namespace) + retval = argh.dispatching._get_function_from_namespace_obj(argparse_namespace) assert retval == func @@ -857,29 +911,169 @@ def second_func(): ) -def test_add_commands_namespace_overrides1(capsys: pytest.CaptureFixture[str]): +def test_add_commands_group_overrides1(capsys: pytest.CaptureFixture[str]): """ - When `namespace_kwargs` is passed to `add_commands()`, its members override + When `group_kwargs` is passed to `add_commands()`, its members override whatever was specified on function level. """ def first_func(foo=123): """Owl stretching time""" + return foo + + def second_func(): pass + p = argh.ArghParser(prog="myapp") + p.add_commands( + [first_func, second_func], + group_name="my-group", + group_kwargs={ + "help": "group help override", + "description": "group description override", + }, + ) + + run(p, "--help", exit=True) + captured = capsys.readouterr() + assert ( + captured.out + == unindent( + f""" + usage: myapp [-h] {{my-group}} ... + + positional arguments: + {{my-group}} + my-group + + {HELP_OPTIONS_LABEL}: + -h, --help show this help message and exit + """ + )[1:] + ) + + +def test_add_commands_group_overrides2(capsys: pytest.CaptureFixture[str]): + """ + When `group_kwargs` is passed to `add_commands()`, its members override + whatever was specified on function level. + """ + + def first_func(foo=123): + """Owl stretching time""" + return foo + def second_func(): pass p = argh.ArghParser(prog="myapp") p.add_commands( [first_func, second_func], - namespace="ns", - namespace_kwargs={ - "help": "namespace help override", - "description": "namespace description override", + group_name="my-group", + group_kwargs={ + "help": "group help override", + "description": "group description override", }, ) + run(p, "my-group --help", exit=True) + captured = capsys.readouterr() + assert ( + captured.out + == unindent( + f""" + usage: myapp my-group [-h] {{first-func,second-func}} ... + + {HELP_OPTIONS_LABEL}: + -h, --help show this help message and exit + + subcommands: + group description override + + {{first-func,second-func}} + group help override + first-func Owl stretching time + second-func + """ + )[1:] + ) + + +def test_add_commands_group_overrides3(capsys: pytest.CaptureFixture[str]): + """ + When `group_kwargs` is passed to `add_commands()`, its members override + whatever was specified on function level. + """ + + def first_func(foo=123): + """Owl stretching time""" + return foo + + def second_func(): + pass + + p = argh.ArghParser(prog="myapp") + p.add_commands( + [first_func, second_func], + group_name="my-group", + group_kwargs={ + "help": "group help override", + "description": "group description override", + }, + ) + + run(p, "my-group first-func --help", exit=True) + captured = capsys.readouterr() + assert ( + captured.out + == unindent( + f""" + usage: myapp my-group first-func [-h] [-f FOO] + + Owl stretching time + + {HELP_OPTIONS_LABEL}: + -h, --help show this help message and exit + -f FOO, --foo FOO 123 + """ + )[1:] + ) + + +# TODO: remove in v.0.30 +def test_add_commands_group_overrides1__deprecated(capsys: pytest.CaptureFixture[str]): + """ + When `namespace_kwargs` is passed to `add_commands()`, its members override + whatever was specified on function level. + """ + + def first_func(foo=123): + """Owl stretching time""" + pass + + def second_func(): + pass + + p = argh.ArghParser(prog="myapp") + with pytest.warns(DeprecationWarning) as recorded_warnings: + p.add_commands( + [first_func, second_func], + namespace="ns", + namespace_kwargs={ + "help": "namespace help override", + "description": "namespace description override", + }, + ) + assert len(recorded_warnings) == 2 + assert re.match( + r".*`namespace` is deprecated .+ removed in Argh 0\.30.+ use `group_name` instead", + str(recorded_warnings[0].message), + ) + assert re.match( + r".*`namespace_kwargs` is deprecated .+ removed in Argh 0\.30.+ use `group_kwargs` instead", + str(recorded_warnings[1].message), + ) + run(p, "--help", exit=True) captured = capsys.readouterr() assert ( @@ -899,7 +1093,8 @@ def second_func(): ) -def test_add_commands_namespace_overrides2(capsys: pytest.CaptureFixture[str]): +# TODO: remove in v.0.30 +def test_add_commands_group_overrides2__deprecated(capsys: pytest.CaptureFixture[str]): """ When `namespace_kwargs` is passed to `add_commands()`, its members override whatever was specified on function level. @@ -945,7 +1140,8 @@ def second_func(): ) -def test_add_commands_namespace_overrides3(capsys: pytest.CaptureFixture[str]): +# TODO: remove in v.0.30 +def test_add_commands_group_overrides3__deprecated(capsys: pytest.CaptureFixture[str]): """ When `namespace_kwargs` is passed to `add_commands()`, its members override whatever was specified on function level.