diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e6280313..95a67932 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,14 @@ Added removal of ``pyyaml`` as a required dependency in v5.0.0 (`#652 `__). +Changed +^^^^^^^ + +- ``jsonargparse.CLI`` renamed to ``jsonargparse.auto_cli`` to follow `PEP 8 + `__ functions + naming convention (`#640 + `__). + Fixed ^^^^^ - Help for ``Protocol`` types not working correctly (`#645 diff --git a/DOCUMENTATION.rst b/DOCUMENTATION.rst index d8d16362..ec6fc6fc 100644 --- a/DOCUMENTATION.rst +++ b/DOCUMENTATION.rst @@ -6,13 +6,13 @@ Basic usage There are multiple ways of using jsonargparse. One is to construct low level parsers (see :ref:`parsers`) being almost a drop in replacement of argparse. However, argparse is too verbose and leads to unnecessary duplication. The -simplest and recommended way of using jsonargparse is by using the :func:`.CLI` -function, which has the benefit of minimizing boilerplate code. A simple example -is: +simplest and recommended way of using jsonargparse is by using the +:func:`.auto_cli` function, which has the benefit of minimizing boilerplate +code. A simple example is: .. testcode:: - from jsonargparse import CLI + from jsonargparse import auto_cli def command(name: str, prize: int = 100): @@ -26,7 +26,7 @@ is: if __name__ == "__main__": - CLI(command) + auto_cli(command) Note that the ``name`` and ``prize`` parameters have type hints and are described in the docstring. These are shown in the help of the command line @@ -49,7 +49,7 @@ tool. In a shell you could see the help and run a command as follows: shown, jsonargparse needs to be installed with the ``signatures`` extras require as explained in section :ref:`installation`. -When :func:`.CLI` receives a single class, the first arguments are for +When :func:`.auto_cli` receives a single class, the first arguments are for parameters to instantiate the class, then a method name is expected (i.e. methods become :ref:`sub-commands`) and the remaining arguments are for parameters of this method. An example would be: @@ -57,7 +57,7 @@ parameters of this method. An example would be: .. testcode:: from random import randint - from jsonargparse import CLI + from jsonargparse import auto_cli class Main: @@ -77,7 +77,7 @@ parameters of this method. An example would be: if __name__ == "__main__": - print(CLI(Main)) + print(auto_cli(Main)) Then in a shell you could run: @@ -88,16 +88,16 @@ Then in a shell you could run: .. doctest:: :hide: - >>> CLI(Main, args=["--max_prize=1000", "person", "Lucky"]) # doctest: +ELLIPSIS + >>> auto_cli(Main, args=["--max_prize=1000", "person", "Lucky"]) # doctest: +ELLIPSIS 'Lucky won ...€!' If the class given does not have any methods, there will be no sub-commands and -:func:`.CLI` will return an instance of the class. For example: +:func:`.auto_cli` will return an instance of the class. For example: .. testcode:: from dataclasses import dataclass - from jsonargparse import CLI + from jsonargparse import auto_cli @dataclass @@ -107,7 +107,7 @@ If the class given does not have any methods, there will be no sub-commands and if __name__ == "__main__": - print(CLI(Settings, as_positional=False)) + print(auto_cli(Settings, as_positional=False)) Then in a shell you could run: @@ -118,17 +118,17 @@ Then in a shell you could run: .. doctest:: :hide: - >>> CLI(Settings, as_positional=False, args=["--name=Lucky"]) # doctest: +ELLIPSIS + >>> auto_cli(Settings, as_positional=False, args=["--name=Lucky"]) # doctest: +ELLIPSIS Settings(name='Lucky', prize=100) Note the use of ``as_positional=False`` to make required arguments as non-positional. -If more than one function is given to :func:`.CLI`, then any of them can be run -via :ref:`sub-commands` similar to the single class example above, i.e. +If more than one function is given to :func:`.auto_cli`, then any of them can be +run via :ref:`sub-commands` similar to the single class example above, i.e. ``example.py function [arguments]`` where ``function`` is the name of the function to execute. If multiple classes or a mixture of functions and classes -is given to :func:`.CLI`, to execute a method of a class, two levels of +is given to :func:`.auto_cli`, to execute a method of a class, two levels of :ref:`sub-commands` are required. The first sub-command would be the name of the class and the second the name of the method, i.e. ``example.py class [init_arguments] method [arguments]``. @@ -159,7 +159,7 @@ Arbitrary levels of sub-commands with custom names can be defined by providing a } if __name__ == "__main__": - print(CLI(components)) + print(auto_cli(components)) Then in a shell: @@ -170,7 +170,7 @@ Then in a shell: .. doctest:: :hide: - >>> CLI(components, args=["weekend", "tier1", "Lucky"]) + >>> auto_cli(components, args=["weekend", "tier1", "Lucky"]) 'Lucky won 300€!' .. note:: @@ -186,7 +186,7 @@ Then in a shell: Writing configuration files --------------------------- -All tools implemented with the :func:`.CLI` function have the ``--config`` +All tools implemented with the :func:`.auto_cli` function have the ``--config`` option to provide settings in a config file (more details in :ref:`configuration-files`). This becomes very useful when the number of configurable parameters is large. To ease the writing of config files, there is @@ -206,14 +206,14 @@ can be advised to follow the following steps: Comparison to Fire ------------------ -The :func:`.CLI` feature is similar to and inspired by `Fire +The :func:`.auto_cli` feature is similar to and inspired by `Fire `__. However, there are fundamental differences. First, the purpose is not allowing to call any python object from the command line. It is only intended for running functions and classes specifically written for this purpose. Second, the arguments are expected to have type hints, and the given values will be validated according to these. Third, the return values of -the functions are not automatically printed. :func:`.CLI` returns the value and -it is up to the developer to decide what to do with it. +the functions are not automatically printed. :func:`.auto_cli` returns the value +and it is up to the developer to decide what to do with it. .. _tutorials: @@ -316,8 +316,7 @@ without doing any parsing. For instance `sphinx-argparse `__ can be used to include the help of CLIs in automatically generated documentation of a package. To use sphinx-argparse it is necessary to have a function that returns the parser. -Having a CLI function this could be easily implemented with -:func:`.capture_parser` as follows: +This can be easily implemented with :func:`.capture_parser` as follows: .. testcode:: @@ -330,7 +329,7 @@ Having a CLI function this could be easily implemented with .. note:: The official way to obtain the parser for command line tools based on - :func:`.CLI` is by using :func:`.capture_parser`. + :func:`.auto_cli` is by using :func:`.capture_parser`. Functions as type diff --git a/jsonargparse/_cli.py b/jsonargparse/_cli.py index 18053476..8606137f 100644 --- a/jsonargparse/_cli.py +++ b/jsonargparse/_cli.py @@ -10,7 +10,7 @@ from ._optionals import get_doc_short_description from ._util import default_config_option_help -__all__ = ["CLI"] +__all__ = ["CLI", "auto_cli"] ComponentType = Union[Callable, Type] @@ -18,7 +18,12 @@ ComponentsType = Optional[Union[ComponentType, List[ComponentType], DictComponentsType]] -def CLI( +def CLI(*args, **kwargs): + """Alias of :func:`auto_cli`.""" + return auto_cli(*args, _stacklevel=3, **kwargs) + + +def auto_cli( components: ComponentsType = None, args: Optional[List[str]] = None, config_help: str = default_config_option_help, @@ -30,6 +35,8 @@ def CLI( ): """Simple creation of command line interfaces. + Previously CLI, renamed to follow the standard of functions in lowercase. + Creates an argument parser from one or more functions/classes, parses arguments and runs one of the functions or class methods depending on what was parsed. If the 'components' parameter is not given, then the components @@ -50,6 +57,7 @@ def CLI( The value returned by the executed function or class method. """ return_parser = kwargs.pop("return_parser", False) + stacklevel = kwargs.pop("_stacklevel", 2) caller = inspect.stack()[1][0] if components is None: @@ -89,7 +97,7 @@ def CLI( if set_defaults is not None: parser.set_defaults(set_defaults) if return_parser: - deprecation_warning_cli_return_parser() + deprecation_warning_cli_return_parser(stacklevel) return parser cfg = parser.parse_args(args) init = parser.instantiate_classes(cfg) @@ -103,7 +111,7 @@ def CLI( if set_defaults is not None: parser.set_defaults(set_defaults) if return_parser: - deprecation_warning_cli_return_parser() + deprecation_warning_cli_return_parser(stacklevel) return parser cfg = parser.parse_args(args) init = parser.instantiate_classes(cfg) diff --git a/jsonargparse/_deprecated.py b/jsonargparse/_deprecated.py index ab05a391..c7df1f87 100644 --- a/jsonargparse/_deprecated.py +++ b/jsonargparse/_deprecated.py @@ -369,8 +369,8 @@ def set_url_support(enabled: bool): """ -def deprecation_warning_cli_return_parser(): - deprecation_warning("CLI.__init__.return_parser", cli_return_parser_message, stacklevel=2) +def deprecation_warning_cli_return_parser(stacklevel): + deprecation_warning("CLI.__init__.return_parser", cli_return_parser_message, stacklevel=stacklevel) logger_property_none_message = """ diff --git a/jsonargparse_tests/test_cli.py b/jsonargparse_tests/test_cli.py index 1a2f46ac..6fb3cb9b 100644 --- a/jsonargparse_tests/test_cli.py +++ b/jsonargparse_tests/test_cli.py @@ -13,7 +13,7 @@ import pytest -from jsonargparse import CLI, capture_parser, lazy_instance +from jsonargparse import CLI, auto_cli, capture_parser, lazy_instance from jsonargparse._optionals import docstring_parser_support, ruyaml_support from jsonargparse.typing import final from jsonargparse_tests.conftest import json_or_yaml_dump, json_or_yaml_load, skip_if_docstring_parser_unavailable @@ -22,7 +22,7 @@ def get_cli_stdout(*args, **kwargs) -> str: out = StringIO() with redirect_stdout(out), suppress(SystemExit), patch.dict(os.environ, {"COLUMNS": "150"}): - CLI(*args, **kwargs) + auto_cli(*args, **kwargs) return out.getvalue() @@ -32,7 +32,7 @@ def get_cli_stdout(*args, **kwargs) -> str: @pytest.mark.parametrize("components", [0, [], {"x": 0}]) def test_unexpected_components(components): with pytest.raises(ValueError): - CLI(components) + auto_cli(components) class ConflictingSubcommandKey: @@ -42,7 +42,7 @@ def subcommand(self, x: int = 0): def test_conflicting_subcommand_key(): with pytest.raises(ValueError) as ctx: - CLI(ConflictingSubcommandKey, args=["subcommand", "--x=1"]) + auto_cli(ConflictingSubcommandKey, args=["subcommand", "--x=1"]) assert ctx.match("subcommand name can't be the same") @@ -54,13 +54,14 @@ def single_function(a1: float): return a1 -def test_single_function_return(): - assert 1.2 == CLI(single_function, args=["1.2"]) +@pytest.mark.parametrize("cli_fn", [CLI, auto_cli]) +def test_single_function_return(cli_fn): + assert 1.2 == cli_fn(single_function, args=["1.2"]) def test_single_function_set_defaults(): def run_cli(): - CLI(single_function, set_defaults={"a1": 3.4}) + auto_cli(single_function, set_defaults={"a1": 3.4}) parser = capture_parser(run_cli) assert 3.4 == parser.get_defaults().a1 @@ -89,7 +90,7 @@ def __call__(self, x: int): def test_callable_instance(): - assert 3 == CLI(callable_instance, as_positional=False, args=["--x=3"]) + assert 3 == auto_cli(callable_instance, as_positional=False, args=["--x=3"]) # multiple functions tests @@ -105,13 +106,13 @@ def cmd2(a2: str = "X"): def test_multiple_functions_return(): - assert 5 == CLI([cmd1, cmd2], args=["cmd1", "5"]) - assert "Y" == CLI([cmd1, cmd2], args=["cmd2", "--a2=Y"]) + assert 5 == auto_cli([cmd1, cmd2], args=["cmd1", "5"]) + assert "Y" == auto_cli([cmd1, cmd2], args=["cmd2", "--a2=Y"]) def test_multiple_functions_set_defaults(): def run_cli(): - CLI([cmd1, cmd2], set_defaults={"cmd2.a2": "Z"}) + auto_cli([cmd1, cmd2], set_defaults={"cmd2.a2": "Z"}) parser = capture_parser(run_cli) assert "Z" == parser.parse_args(["cmd2"]).cmd2.a2 @@ -166,22 +167,22 @@ def method1(self, m1: int): def test_single_class_return(): - assert ("0", 2) == CLI(Class1, args=["0", "method1", "2"]) - assert ("3", 4) == CLI(Class1, args=['--config={"i1": "3", "method1": {"m1": 4}}']) - assert ("5", 6) == CLI(Class1, args=["5", "method1", '--config={"m1": 6}']) + assert ("0", 2) == auto_cli(Class1, args=["0", "method1", "2"]) + assert ("3", 4) == auto_cli(Class1, args=['--config={"i1": "3", "method1": {"m1": 4}}']) + assert ("5", 6) == auto_cli(Class1, args=["5", "method1", '--config={"m1": 6}']) def test_single_class_missing_required_init(): err = StringIO() with redirect_stderr(err), pytest.raises(SystemExit): - CLI(Class1, args=['--config={"method1": {"m1": 2}}']) + auto_cli(Class1, args=['--config={"method1": {"m1": 2}}']) assert '"i1" is required' in err.getvalue() def test_single_class_invalid_method_parameter(): err = StringIO() with redirect_stderr(err), pytest.raises(SystemExit): - CLI(Class1, args=['--config={"i1": "0", "method1": {"m1": "A"}}']) + auto_cli(Class1, args=['--config={"i1": "0", "method1": {"m1": "A"}}']) assert 'key "m1"' in err.getvalue() @@ -271,7 +272,7 @@ def cmd3(): ], ) def test_function_and_class_return(expected, args): - assert expected == CLI([cmd1, Cmd2, cmd3], args=args) + assert expected == auto_cli([cmd1, Cmd2, cmd3], args=args) def test_function_and_class_main_help(): @@ -341,7 +342,7 @@ def test_function_and_class_function_without_parameters(): def test_automatic_components_empty_context(): def empty_context(): - CLI() + auto_cli() with patch("inspect.getmodule") as mock_getmodule: mock_getmodule.return_value = sys.modules["jsonargparse._core"] @@ -353,7 +354,7 @@ def non_empty_context_function(): def function(a1: float): return a1 - return CLI(args=["6.7"]) + return auto_cli(args=["6.7"]) with patch("inspect.getmodule") as mock_getmodule: mock_getmodule.return_value = sys.modules["jsonargparse._core"] @@ -369,7 +370,7 @@ def __init__(self, i1: str): def method(self, m1: int): return self.i1, m1 - return CLI(args=["a", "method", "2"]) + return auto_cli(args=["a", "method", "2"]) with patch("inspect.getmodule") as mock_getmodule: mock_getmodule.return_value = sys.modules["jsonargparse._core"] @@ -386,13 +387,13 @@ class SettingsClass: def test_dataclass_without_methods_response(): - settings = CLI(SettingsClass, args=["--p1=x", "--p2=0"], as_positional=False) + settings = auto_cli(SettingsClass, args=["--p1=x", "--p2=0"], as_positional=False) assert isinstance(settings, SettingsClass) assert asdict(settings) == {"p1": "x", "p2": 0} def test_dataclass_without_methods_parser_groups(): - parser = capture_parser(lambda: CLI(SettingsClass, args=[], as_positional=False)) + parser = capture_parser(lambda: auto_cli(SettingsClass, args=[], as_positional=False)) assert parser.groups == {} @@ -401,8 +402,8 @@ def test_dataclass_without_methods_parser_groups(): def test_named_components_shallow(): components = {"cmd1": single_function, "cmd2": callable_instance} - assert 3.4 == CLI(components, args=["cmd1", "3.4"]) - assert 5 == CLI(components, as_positional=False, args=["cmd2", "--x=5"]) + assert 3.4 == auto_cli(components, args=["cmd1", "3.4"]) + assert 5 == auto_cli(components, as_positional=False, args=["cmd2", "--x=5"]) out = get_cli_stdout(components, args=["--help"]) if docstring_parser_support: assert "Description of single_function" in out @@ -467,9 +468,9 @@ def test_named_components_deep(): if docstring_parser_support: assert "Description of method1" in out - assert 5.6 == CLI(components, args=["lv1_a", "lv2_x", "--a1=5.6"], **kw) - assert 7 == CLI(components, args=["lv1_a", "lv2_y", "lv3_p", "--x=7"], **kw) - assert ("w", 9) == CLI(components, args=["lv1_b", "lv2_z", "lv3_q", "--i1=w", "method1", "--m1=9"], **kw) + assert 5.6 == auto_cli(components, args=["lv1_a", "lv2_x", "--a1=5.6"], **kw) + assert 7 == auto_cli(components, args=["lv1_a", "lv2_y", "lv3_p", "--x=7"], **kw) + assert ("w", 9) == auto_cli(components, args=["lv1_b", "lv2_z", "lv3_q", "--i1=w", "method1", "--m1=9"], **kw) # config file tests @@ -533,7 +534,7 @@ def test_final_and_subclass_type_config_file(tmp_cwd): Path("b.yaml").write_text(json_or_yaml_dump({"a": "a.yaml"})) Path("a.yaml").write_text(json_or_yaml_dump(a_conf)) - out = CLI(run_bf, args=["--config=config.yaml"]) + out = auto_cli(run_bf, args=["--config=config.yaml"]) assert "a yaml" == out @@ -546,7 +547,7 @@ async def run_async(time: float = 0.1): def test_async_function(): - assert "done" == CLI(run_async, args=["--time=0.0"]) + assert "done" == auto_cli(run_async, args=["--time=0.0"]) class AsyncMethod: @@ -561,7 +562,7 @@ async def run(self): def test_async_method(): - assert "done" == CLI(AsyncMethod, args=["--time=0.0", "run"]) + assert "done" == auto_cli(AsyncMethod, args=["--time=0.0", "run"]) async def run_async_instance(cls: Callable[[], AsyncMethod]): @@ -575,4 +576,4 @@ def test_async_instance(): "init_args": {"time": 0.0, "require_async": True}, } } - assert "done" == CLI(run_async_instance, args=[f"--config={json.dumps(config)}"]) + assert "done" == auto_cli(run_async_instance, args=[f"--config={json.dumps(config)}"])