File: extending-click.rst

package info (click to toggle)
python-asyncclick 8.3.0.5%2Basync-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,664 kB
  • sloc: python: 14,154; makefile: 12; sh: 10
file content (138 lines) | stat: -rw-r--r-- 3,953 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
Extending Click
=================

.. currentmodule:: click

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.

.. contents::
    :depth: 2
    :local:

.. _custom-groups:

Custom Groups
-------------

You can customize the behavior of a group beyond the arguments it accepts by
subclassing :class:`click.Group`.

The most common methods to override are :meth:`~click.Group.get_command` and
:meth:`~click.Group.list_commands`.

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.

.. code-block:: python

    import importlib.util
    import os
    import click

    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(self.plugin_folder):
                if filename.endswith(".py"):
                    rv.append(filename[:-3])

            rv.sort()
            return rv

        def get_command(self, ctx, name):
            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__":
        cli()

Custom classes can also be used with decorators:

.. code-block:: python

    @click.group(
        cls=PluginGroup,
        plugin_folder=os.path.join(os.path.dirname(__file__), "commands")
    )
    def cli():
        pass

.. _aliases:

Command Aliases
---------------

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.

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.

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 = 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)
            ]

            if not matches:
                return None

            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):
            # always return the full command name
            _, cmd, args = super().resolve_command(ctx, args)
            return cmd.name, cmd, args

It can be used like this:

.. click:example::

    @click.group(cls=AliasedGroup)
    def cli():
        pass

    @cli.command
    def push():
        pass

    @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