From 238ee070af40576d23418e41fa96d1c41e53f6b1 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 19 Aug 2023 12:13:14 -0700 Subject: [PATCH] remove mentions of deprecated base classes --- docs/advanced.rst | 59 ++++--- docs/commands.rst | 285 ++++++++++++++------------------ docs/complex.rst | 24 ++- docs/exceptions.rst | 2 +- docs/quickstart.rst | 2 +- docs/upgrading.rst | 8 +- examples/complex/complex/cli.py | 2 +- examples/imagepipe/README | 2 +- src/click/core.py | 25 ++- tests/test_chain.py | 4 +- 10 files changed, 195 insertions(+), 218 deletions(-) diff --git a/docs/advanced.rst b/docs/advanced.rst index 56495617d..de18ed487 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -7,44 +7,46 @@ In addition to common functionality that is implemented in the library itself, there are countless patterns that can be implemented by extending Click. This page should give some insight into what can be accomplished. + .. _aliases: Command Aliases --------------- -Many tools support aliases for commands (see `Command alias example -`_). -For instance, you can configure ``git`` to accept ``git ci`` as alias for -``git commit``. Other tools also support auto-discovery for aliases by -automatically shortening them. - -Click does not support this out of the box, but it's very easy to customize -the :class:`Group` or any other :class:`MultiCommand` to provide this -functionality. +Many tools support aliases for commands. For example, you can configure +``git`` to accept ``git ci`` as alias for ``git commit``. Other tools also +support auto-discovery for aliases by automatically shortening them. -As explained in :ref:`custom-multi-commands`, a multi command can provide -two methods: :meth:`~MultiCommand.list_commands` and -:meth:`~MultiCommand.get_command`. In this particular case, you only need -to override the latter as you generally don't want to enumerate the -aliases on the help page in order to avoid confusion. +It's possible to customize :class:`Group` to provide this functionality. As +explained in :ref:`custom-groups`, a group provides two methods: +:meth:`~Group.list_commands` and :meth:`~Group.get_command`. In this particular +case, you only need to override the latter as you generally don't want to +enumerate the aliases on the help page in order to avoid confusion. -This following example implements a subclass of :class:`Group` that -accepts a prefix for a command. If there were a command called ``push``, -it would accept ``pus`` as an alias (so long as it was unique): +The following example implements a subclass of :class:`Group` that accepts a +prefix for a command. If there was a command called ``push``, it would accept +``pus`` as an alias (so long as it was unique): .. click:example:: class AliasedGroup(click.Group): def get_command(self, ctx, cmd_name): - rv = click.Group.get_command(self, ctx, cmd_name) + rv = super().get_command(ctx, cmd_name) + if rv is not None: return rv - matches = [x for x in self.list_commands(ctx) - if x.startswith(cmd_name)] + + matches = [ + x for x in self.list_commands(ctx) + if x.startswith(cmd_name) + ] + if not matches: return None - elif len(matches) == 1: + + if len(matches) == 1: return click.Group.get_command(self, ctx, matches[0]) + ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") def resolve_command(self, ctx, args): @@ -52,22 +54,27 @@ it would accept ``pus`` as an alias (so long as it was unique): _, cmd, args = super().resolve_command(ctx, args) return cmd.name, cmd, args -And it can then be used like this: +It can be used like this: .. click:example:: - @click.command(cls=AliasedGroup) + @click.group(cls=AliasedGroup) def cli(): pass - @cli.command() + @cli.command def push(): pass - @cli.command() + @cli.command def pop(): pass +See the `alias example`_ in Click's repository for another example. + +.. _alias example: https://github.com/pallets/click/tree/main/examples/aliases + + Parameter Modifications ----------------------- @@ -266,7 +273,7 @@ triggering a parsing error. This can generally be activated in two different ways: 1. It can be enabled on custom :class:`Command` subclasses by changing - the :attr:`~BaseCommand.ignore_unknown_options` attribute. + the :attr:`~Command.ignore_unknown_options` attribute. 2. It can be enabled by changing the attribute of the same name on the context class (:attr:`Context.ignore_unknown_options`). This is best changed through the ``context_settings`` dictionary on the command. diff --git a/docs/commands.rst b/docs/commands.rst index 62981a51b..0cdc169f9 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -3,9 +3,10 @@ Commands and Groups .. currentmodule:: click -The most important feature of Click is the concept of arbitrarily nesting -command line utilities. This is implemented through the :class:`Command` -and :class:`Group` (actually :class:`MultiCommand`). +The structure of a Click application is defined with :class:`Command`, which +defines an individual named command, and :class:`Group`, which defines a nested +collection of commands (or more groups) under a name. + Callback Invocation ------------------- @@ -15,10 +16,9 @@ If the script is the only command, it will always fire (unless a parameter callback prevents it. This for instance happens if someone passes ``--help`` to the script). -For groups and multi commands, the situation looks different. In this case, -the callback fires whenever a subcommand fires (unless this behavior is -changed). What this means in practice is that an outer command runs -when an inner command runs: +For groups, the situation looks different. In this case, the callback fires +whenever a subcommand fires. What this means in practice is that an outer +command runs when an inner command runs: .. click:example:: @@ -148,15 +148,12 @@ nested applications; see :ref:`complex-guide` for more information. Group Invocation Without Command -------------------------------- -By default, a group or multi command is not invoked unless a subcommand is -passed. In fact, not providing a command automatically passes ``--help`` -by default. This behavior can be changed by passing -``invoke_without_command=True`` to a group. In that case, the callback is -always invoked instead of showing the help page. The context object also -includes information about whether or not the invocation would go to a -subcommand. - -Example: +By default, a group is not invoked unless a subcommand is passed. In fact, not +providing a command automatically passes ``--help`` by default. This behavior +can be changed by passing ``invoke_without_command=True`` to a group. In that +case, the callback is always invoked instead of showing the help page. The +context object also includes information about whether or not the invocation +would go to a subcommand. .. click:example:: @@ -172,218 +169,190 @@ Example: def sync(): click.echo('The subcommand') -And how it works in practice: - .. click:run:: invoke(cli, prog_name='tool', args=[]) invoke(cli, prog_name='tool', args=['sync']) -.. _custom-multi-commands: -Custom Multi Commands ---------------------- +.. _custom-groups: -In addition to using :func:`click.group`, you can also build your own -custom multi commands. This is useful when you want to support commands -being loaded lazily from plugins. +Custom Groups +------------- -A custom multi command just needs to implement a list and load method: +You can customize the behavior of a group beyond the arguments it accepts by +subclassing :class:`click.Group`. -.. click:example:: +The most common methods to override are :meth:`~click.Group.get_command` and +:meth:`~click.Group.list_commands`. - import click - import os +The following example implements a basic plugin system that loads commands from +Python files in a folder. The command is lazily loaded to avoid slow startup. - plugin_folder = os.path.join(os.path.dirname(__file__), 'commands') +.. code-block:: python + + import importlib.util + import os + import click - class MyCLI(click.MultiCommand): + class PluginGroup(click.Group): + def __init__(self, name=None, plugin_folder="commands", **kwargs): + super().__init__(name=name, **kwargs) + self.plugin_folder = plugin_folder def list_commands(self, ctx): rv = [] - for filename in os.listdir(plugin_folder): - if filename.endswith('.py') and filename != '__init__.py': + + for filename in os.listdir(self.plugin_folder): + if filename.endswith(".py"): rv.append(filename[:-3]) + rv.sort() return rv def get_command(self, ctx, name): - ns = {} - fn = os.path.join(plugin_folder, name + '.py') - with open(fn) as f: - code = compile(f.read(), fn, 'exec') - eval(code, ns, ns) - return ns['cli'] - - cli = MyCLI(help='This tool\'s subcommands are loaded from a ' - 'plugin folder dynamically.') + path = os.path.join(self.plugin_folder, f"{name}.py") + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.cli + + cli = PluginGroup( + plugin_folder=os.path.join(os.path.dirname(__file__), "commands") + ) - if __name__ == '__main__': + if __name__ == "__main__": cli() -These custom classes can also be used with decorators: +Custom classes can also be used with decorators: -.. click:example:: +.. code-block:: python - @click.command(cls=MyCLI) + @click.group( + cls=PluginGroup, + plugin_folder=os.path.join(os.path.dirname(__file__), "commands") + ) def cli(): pass -Merging Multi Commands ----------------------- -In addition to implementing custom multi commands, it can also be -interesting to merge multiple together into one script. While this is -generally not as recommended as it nests one below the other, the merging -approach can be useful in some circumstances for a nicer shell experience. +.. _command-chaining: -The default implementation for such a merging system is the -:class:`CommandCollection` class. It accepts a list of other multi -commands and makes the commands available on the same level. +Command Chaining +---------------- -Example usage: +It is useful to invoke more than one subcommand in one call. For example, +``my-app validate build upload`` would invoke ``validate``, then ``build``, then +``upload``. To implement this, pass ``chain=True`` when creating a group. .. click:example:: - import click - - @click.group() - def cli1(): - pass - - @cli1.command() - def cmd1(): - """Command on cli1""" - - @click.group() - def cli2(): + @click.group(chain=True) + def cli(): pass - @cli2.command() - def cmd2(): - """Command on cli2""" + @cli.command('validate') + def validate(): + click.echo('validate') - cli = click.CommandCollection(sources=[cli1, cli2]) + @cli.command('build') + def build(): + click.echo('build') - if __name__ == '__main__': - cli() - -And what it looks like: +You can invoke it like this: .. click:run:: - invoke(cli, prog_name='cli', args=['--help']) + invoke(cli, prog_name='my-app', args=['validate', 'build']) -In case a command exists in more than one source, the first source wins. +When using chaining, there are a few restrictions: +- Only the last command may use ``nargs=-1`` on an argument, otherwise the + parser will not be able to find further commands. +- It is not possible to nest groups below a chain group. +- On the command line, options must be specified before arguments for each + command in the chain. +- The :attr:`Context.invoked_subcommand` attribute will be ``'*'`` because the + parser doesn't know the full list of commands that will run yet. -.. _multi-command-chaining: -Multi Command Chaining ----------------------- +Command Pipelines +----------------- -.. versionadded:: 3.0 +When using chaining, a common pattern is to have each command process the +result of the previous command. -Sometimes it is useful to be allowed to invoke more than one subcommand in -one go. For example, ``my-app validate build upload`` would invoke ``validate``, -then ``build``, then ``upload``. To implement this in Click, pass ``chain=True`` -when creating a group. +A straightforward way to do this is to use :func:`make_pass_decorator` to pass +a context object to each command, and store and read the data on that object. .. click:example:: - @click.group(chain=True) - def cli(): - pass - - - @cli.command('sdist') - def sdist(): - click.echo('sdist called') + pass_ns = click.make_pass_decorator(dict, ensure=True) + @click.group(chain=True) + @click.argument("name") + @pass_ns + def cli(ns, name): + ns["name"] = name - @cli.command('bdist_wheel') - def bdist_wheel(): - click.echo('bdist_wheel called') + @cli.command + @pass_ns + def lower(ns): + ns["name"] = ns["name"].lower() -Now you can invoke it like this: + @cli.command + @pass_ns + def show(ns): + click.echo(ns["name"]) .. click:run:: - invoke(cli, prog_name='setup.py', args=['sdist', 'bdist_wheel']) - -When using multi command chaining you can only have one command (the last) -use ``nargs=-1`` on an argument. It is also not possible to nest multi -commands below chained multicommands. Other than that there are no -restrictions on how they work. They can accept options and arguments as -normal. The order between options and arguments is limited for chained -commands. Currently only ``--options argument`` order is allowed. - -Another note: the :attr:`Context.invoked_subcommand` attribute is a bit -useless for multi commands as it will give ``'*'`` as value if more than -one command is invoked. This is necessary because the handling of -subcommands happens one after another so the exact subcommands that will -be handled are not yet available when the callback fires. + invoke(cli, prog_name="process", args=["Click", "show", "lower", "show"]) -.. note:: +Another way to do this is to collect data returned by each command, then process +it at the end of the chain. Use the group's :meth:`~Group.result_callback` +decorator to register a function that is called after the chain is finished. It +is passed the list of return values as well as any parameters registered on the +group. - It is currently not possible for chain commands to be nested. This - will be fixed in future versions of Click. +A command can return anything, including a function. Here's an example of that, +where each subcommand creates a function that processes the input, then the +result callback calls each function. The command takes a file, processes each +line, then outputs it. If no subcommands are given, it outputs the contents +of the file unchanged. - -Multi Command Pipelines ------------------------ - -.. versionadded:: 3.0 - -A very common usecase of multi command chaining is to have one command -process the result of the previous command. There are various ways in -which this can be facilitated. The most obvious way is to store a value -on the context object and process it from function to function. This -works by decorating a function with :func:`pass_context` after which the -context object is provided and a subcommand can store its data there. - -Another way to accomplish this is to setup pipelines by returning -processing functions. Think of it like this: when a subcommand gets -invoked it processes all of its parameters and comes up with a plan of -how to do its processing. At that point it then returns a processing -function and returns. - -Where do the returned functions go? The chained multicommand can register -a callback with :meth:`MultiCommand.result_callback` that goes over all -these functions and then invoke them. - -To make this a bit more concrete consider this example: - -.. click:example:: +.. code-block:: python @click.group(chain=True, invoke_without_command=True) - @click.option('-i', '--input', type=click.File('r')) - def cli(input): + @click.argument("fin", type=click.File("r")) + def cli(fin): pass @cli.result_callback() - def process_pipeline(processors, input): - iterator = (x.rstrip('\r\n') for x in input) + def process_pipeline(processors, fin): + iterator = (x.rstrip("\r\n") for x in input) + for processor in processors: iterator = processor(iterator) + for item in iterator: click.echo(item) - @cli.command('uppercase') + @cli.command("upper") def make_uppercase(): def processor(iterator): for line in iterator: yield line.upper() return processor - @cli.command('lowercase') + @cli.command("lower") def make_lowercase(): def processor(iterator): for line in iterator: yield line.lower() return processor - @cli.command('strip') + @cli.command("strip") def make_strip(): def processor(iterator): for line in iterator: @@ -418,11 +387,11 @@ make resource handling much more complicated. For such it's recommended to not use the file type and manually open the file through :func:`open_file`. -For a more complex example that also improves upon handling of the -pipelines have a look at the `imagepipe multi command chaining demo -`__ in -the Click repository. It implements a pipeline based image editing tool -that has a nice internal structure for the pipelines. +For a more complex example that also improves upon handling of the pipelines, +see the `imagepipe example`_ in the Click repository. It implements a +pipeline based image editing tool that has a nice internal structure. + +.. _imagepipe example: https://github.com/pallets/click/tree/main/examples/imagepipe Overriding Defaults @@ -533,15 +502,15 @@ that were previously hard to implement. In essence any command callback can now return a value. This return value is bubbled to certain receivers. One usecase for this has already been -show in the example of :ref:`multi-command-chaining` where it has been -demonstrated that chained multi commands can have callbacks that process +show in the example of :ref:`command-chaining` where it has been +demonstrated that chained groups can have callbacks that process all return values. When working with command return values in Click, this is what you need to know: - The return value of a command callback is generally returned from the - :meth:`BaseCommand.invoke` method. The exception to this rule has to + :meth:`Command.invoke` method. The exception to this rule has to do with :class:`Group`\s: * In a group the return value is generally the return value of the @@ -551,7 +520,7 @@ know: * If a group is set up for chaining then the return value is a list of all subcommands' results. * Return values of groups can be processed through a - :attr:`MultiCommand.result_callback`. This is invoked with the + :attr:`Group.result_callback`. This is invoked with the list of all return values in chain mode, or the single return value in case of non chained commands. @@ -561,9 +530,9 @@ know: - Click does not have any hard requirements for the return values and does not use them itself. This allows return values to be used for - custom decorators or workflows (like in the multi command chaining + custom decorators or workflows (like in the command chaining example). - When a Click script is invoked as command line application (through - :meth:`BaseCommand.main`) the return value is ignored unless the + :meth:`Command.main`) the return value is ignored unless the `standalone_mode` is disabled in which case it's bubbled through. diff --git a/docs/complex.rst b/docs/complex.rst index f24b0fe84..3db0159b8 100644 --- a/docs/complex.rst +++ b/docs/complex.rst @@ -224,9 +224,9 @@ Lazily Loading Subcommands Large CLIs and CLIs with slow imports may benefit from deferring the loading of subcommands. The interfaces which support this mode of use are -:meth:`MultiCommand.list_commands` and :meth:`MultiCommand.get_command`. A custom -:class:`MultiCommand` subclass can implement a lazy loader by storing extra data such -that :meth:`MultiCommand.get_command` is responsible for running imports. +:meth:`Group.list_commands` and :meth:`Group.get_command`. A custom +:class:`Group` subclass can implement a lazy loader by storing extra data such +that :meth:`Group.get_command` is responsible for running imports. Since the primary case for this is a :class:`Group` which loads its subcommands lazily, the following example shows a lazy-group implementation. @@ -279,7 +279,7 @@ stores a mapping from subcommand names to the information for importing them. # get the Command object from that module cmd_object = getattr(mod, cmd_object_name) # check the result to make debugging easier - if not isinstance(cmd_object, click.BaseCommand): + if not isinstance(cmd_object, click.Command): raise ValueError( f"Lazy loading of {import_path} failed by returning " "a non-command object" @@ -306,6 +306,8 @@ subcommands like so: def cli(): pass +.. code-block:: python + # in foo.py import click @@ -313,6 +315,8 @@ subcommands like so: def cli(): pass +.. code-block:: python + # in bar.py import click from lazy_group import LazyGroup @@ -325,6 +329,8 @@ subcommands like so: def cli(): pass +.. code-block:: python + # in baz.py import click @@ -337,7 +343,7 @@ What triggers Lazy Loading? ``````````````````````````` There are several events which may trigger lazy loading by running the -:meth:`MultiCommand.get_command` function. +:meth:`Group.get_command` function. Some are intuititve, and some are less so. All cases are described with respect to the above example, assuming the main program @@ -358,9 +364,9 @@ Further Deferring Imports It is possible to make the process even lazier, but it is generally more difficult the more you want to defer work. -For example, subcommands could be represented as a custom :class:`BaseCommand` subclass +For example, subcommands could be represented as a custom :class:`Command` subclass which defers importing the command until it is invoked, but which provides -:meth:`BaseCommand.get_short_help_str` in order to support completions and helptext. +:meth:`Command.get_short_help_str` in order to support completions and helptext. More simply, commands can be constructed whose callback functions defer any actual work until after an import. @@ -377,7 +383,7 @@ the "real" callback function is deferred until invocation time: foo_concrete(n, w) -Because ``click`` builds helptext and usage info from options, arguments, and command +Because Click builds helptext and usage info from options, arguments, and command attributes, it has no awareness that the underlying function is in any way handling a -deferred import. Therefore, all ``click``-provided utilities and functionality will work +deferred import. Therefore, all Click-provided utilities and functionality will work as normal on such a command. diff --git a/docs/exceptions.rst b/docs/exceptions.rst index 06ede94bc..dec89da8c 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -10,7 +10,7 @@ like incorrect usage. Where are Errors Handled? ------------------------- -Click's main error handling is happening in :meth:`BaseCommand.main`. In +Click's main error handling is happening in :meth:`Command.main`. In there it handles all subclasses of :exc:`ClickException` as well as the standard :exc:`EOFError` and :exc:`KeyboardInterrupt` exceptions. The latter are internally translated into an :exc:`Abort`. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 61afa0bac..fbf9bd0cb 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -93,7 +93,7 @@ as in the GitHub repository together with readme files: `_ * ``termui``: `Terminal UI functions demo `_ -* ``imagepipe``: `Multi command chaining demo +* ``imagepipe``: `Command chaining demo `_ Basic Concepts - Creating a Command diff --git a/docs/upgrading.rst b/docs/upgrading.rst index c6fa5545f..4c39053e9 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -42,7 +42,7 @@ to address this: Upgrading to 3.2 ---------------- -Click 3.2 had to perform two changes to multi commands which were +Click 3.2 had to perform two changes to groups which were triggered by a change between Click 2 and Click 3 that had bigger consequences than anticipated. @@ -72,10 +72,10 @@ The correct invocation for the above command is the following:: This also allowed us to fix the issue that defaults were not handled properly by this function. -Multicommand Chaining API -````````````````````````` +Command Chaining API +```````````````````` -Click 3 introduced multicommand chaining. This required a change in how +Click 3 introduced command chaining. This required a change in how Click internally dispatches. Unfortunately this change was not correctly implemented and it appeared that it was possible to provide an API that can inform the super command about all the subcommands that will be diff --git a/examples/complex/complex/cli.py b/examples/complex/complex/cli.py index 5d00dba50..81af075c4 100644 --- a/examples/complex/complex/cli.py +++ b/examples/complex/complex/cli.py @@ -28,7 +28,7 @@ def vlog(self, msg, *args): cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "commands")) -class ComplexCLI(click.MultiCommand): +class ComplexCLI(click.Group): def list_commands(self, ctx): rv = [] for filename in os.listdir(cmd_folder): diff --git a/examples/imagepipe/README b/examples/imagepipe/README index 91ec0cd26..5f1046c3e 100644 --- a/examples/imagepipe/README +++ b/examples/imagepipe/README @@ -1,7 +1,7 @@ $ imagepipe_ imagepipe is an example application that implements some - multi commands that chain image processing instructions + commands that chain image processing instructions together. This requires pillow. diff --git a/src/click/core.py b/src/click/core.py index e9e7eab2d..827e99b17 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -69,23 +69,19 @@ def _check_nested_chain( ) -> None: if not base_command.chain or not isinstance(cmd, Group): return + if register: - hint = ( - "It is not possible to add multi commands as children to" - " another multi command that is in chain mode." + message = ( + f"It is not possible to add the group {cmd_name!r} to another" + f" group {base_command.name!r} that is in chain mode." ) else: - hint = ( - "Found a multi command as subcommand to a multi command" - " that is in chain mode. This is not supported." + message = ( + f"Found the group {cmd_name!r} as subcommand to another group " + f" {base_command.name!r} that is in chain mode. This is not supported." ) - raise RuntimeError( - f"{hint}. Command {base_command.name!r} is set to chain and" - f" {cmd_name!r} was added as a subcommand but it in itself is a" - f" multi command. ({cmd_name!r} is a {type(cmd).__name__}" - f" within a chained {type(base_command).__name__} named" - f" {base_command.name!r})." - ) + + raise RuntimeError(message) def batch(iterable: t.Iterable[V], batch_size: int) -> t.List[t.Tuple[V, ...]]: @@ -1506,8 +1502,7 @@ def __init__( for param in self.params: if isinstance(param, Argument) and not param.required: raise RuntimeError( - "Multi commands in chain mode cannot have" - " optional arguments." + "A group in chain mode cannot have optional arguments." ) def to_info_dict(self, ctx: Context) -> t.Dict[str, t.Any]: diff --git a/tests/test_chain.py b/tests/test_chain.py index 6b2eae305..702eaaa3e 100644 --- a/tests/test_chain.py +++ b/tests/test_chain.py @@ -189,7 +189,7 @@ def c(): assert result.output.splitlines() == ["cli=", "a=", "b=", "c="] -def test_multicommand_arg_behavior(runner): +def test_group_arg_behavior(runner): with pytest.raises(RuntimeError): @click.group(chain=True) @@ -219,7 +219,7 @@ def a(): @pytest.mark.xfail -def test_multicommand_chaining(runner): +def test_group_chaining(runner): @click.group(chain=True) def cli(): debug()