File: advanced.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 (413 lines) | stat: -rw-r--r-- 14,287 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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
Advanced Patterns
=================

.. currentmodule:: click

In addition to common functionality, Click offers some advanced features.

.. contents::
    :depth: 1
    :local:

Callbacks and Eager Options
---------------------------

Sometimes, you want a parameter to completely change the execution flow.
For instance, this is the case when you want to have a ``--version``
parameter that prints out the version and then exits the application.

Note: an actual implementation of a ``--version`` parameter that is
reusable is available in Click as :func:`click.version_option`.  The code
here is merely an example of how to implement such a flag.

In such cases, you need two concepts: eager parameters and a callback.  An
eager parameter is a parameter that is handled before others, and a
callback is what executes after the parameter is handled.  The eagerness
is necessary so that an earlier required parameter does not produce an
error message.  For instance, if ``--version`` was not eager and a
parameter ``--foo`` was required and defined before, you would need to
specify it for ``--version`` to work.  For more information, see
:ref:`callback-evaluation-order`.

A callback is a function that is invoked with three parameters: the
current :class:`Context`, the current :class:`Parameter`, and the value.
The context provides some useful features such as quitting the
application and gives access to other already processed parameters.

Here's an example for a ``--version`` flag:

.. click:example::

    async def print_version(ctx, param, value):
        if not value or ctx.resilient_parsing:
            return
        click.echo('Version 1.0')
        ctx.aexit()

    @click.command()
    @click.option('--version', is_flag=True, callback=print_version,
                  expose_value=False, is_eager=True)
    def hello():
        click.echo('Hello World!')

(Note that ``ctx.exit`` is asynchronous in asyncclick, and has thus been renamed.
Alternately you can use ``sys.exit``.)

The `expose_value` parameter prevents the pretty pointless ``version``
parameter from being passed to the callback.  If that was not specified, a
boolean would be passed to the `hello` script.  The `resilient_parsing`
flag is applied to the context if Click wants to parse the command line
without any destructive behavior that would change the execution flow.  In
this case, because we would exit the program, we instead do nothing.

What it looks like:

.. click:run::

    invoke(hello)
    invoke(hello, args=['--version'])

Callbacks for Validation
------------------------

.. versionchanged:: 2.0

If you want to apply custom validation logic, you can do this in the
parameter callbacks. These callbacks can both modify values as well as
raise errors if the validation does not work. The callback runs after
type conversion. It is called for all sources, including prompts.

In Click 1.0, you can only raise the :exc:`UsageError` but starting with
Click 2.0, you can also raise the :exc:`BadParameter` error, which has the
added advantage that it will automatically format the error message to
also contain the parameter name.

.. click:example::

    def validate_rolls(ctx, param, value):
        if isinstance(value, tuple):
            return value

        try:
            rolls, _, dice = value.partition("d")
            return int(dice), int(rolls)
        except ValueError:
            raise click.BadParameter("format must be 'NdM'")

    @click.command()
    @click.option(
        "--rolls", type=click.UNPROCESSED, callback=validate_rolls,
        default="1d6", prompt=True,
    )
    def roll(rolls):
        sides, times = rolls
        click.echo(f"Rolling a {sides}-sided dice {times} time(s)")

.. click:run::

    invoke(roll, args=["--rolls=42"])
    println()
    invoke(roll, args=["--rolls=2d12"])
    println()
    invoke(roll, input=["42", "2d12"])

Parameter Modifications
-----------------------

Parameters (options and arguments) are forwarded to the command callbacks
as you have seen.  One common way to prevent a parameter from being passed
to the callback is the `expose_value` argument to a parameter which hides
the parameter entirely.  The way this works is that the :class:`Context`
object has a :attr:`~Context.params` attribute which is a dictionary of
all parameters.  Whatever is in that dictionary is being passed to the
callbacks.

This can be used to make up additional parameters.  Generally this pattern
is not recommended but in some cases it can be useful.  At the very least
it's good to know that the system works this way.

.. click:example::

    import urllib

    def open_url(ctx, param, value):
        if value is not None:
            ctx.params['fp'] = urllib.urlopen(value)
            return value

    @click.command()
    @click.option('--url', callback=open_url)
    def cli(url, fp=None):
        if fp is not None:
            click.echo(f"{url}: {fp.code}")

In this case the callback returns the URL unchanged but also passes a
second ``fp`` value to the callback.  What's more recommended is to pass
the information in a wrapper however:

.. click:example::

    import urllib

    class URL(object):

        def __init__(self, url, fp):
            self.url = url
            self.fp = fp

    def open_url(ctx, param, value):
        if value is not None:
            return URL(value, urllib.urlopen(value))

    @click.command()
    @click.option('--url', callback=open_url)
    def cli(url):
        if url is not None:
            click.echo(f"{url.url}: {url.fp.code}")


Token Normalization
-------------------

.. versionadded:: 2.0

Starting with Click 2.0, it's possible to provide a function that is used
for normalizing tokens.  Tokens are option names, choice values, or command
values.  This can be used to implement case insensitive options, for
instance.

In order to use this feature, the context needs to be passed a function that
performs the normalization of the token.  For instance, you could have a
function that converts the token to lowercase:

.. click:example::

    CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower())

    @click.command(context_settings=CONTEXT_SETTINGS)
    @click.option('--name', default='Pete')
    def cli(name):
        click.echo(f"Name: {name}")

And how it works on the command line:

.. click:run::

    invoke(cli, prog_name='cli', args=['--NAME=Pete'])

Invoking Other Commands
-----------------------

Sometimes, it might be interesting to invoke one command from another
command.  This is a pattern that is generally discouraged with Click, but
possible nonetheless.  For this, you can use the :func:`Context.invoke`
or :func:`Context.forward` methods.

They work similarly, but the difference is that :func:`Context.invoke` merely
invokes another command with the arguments you provide as a caller,
whereas :func:`Context.forward` fills in the arguments from the current
command.  Both accept the command as the first argument and everything else
is passed onwards as you would expect.

These methods are asynchrous.

Example:

.. click:example::

    cli = click.Group()

    @cli.command()
    @click.option('--count', default=1)
    def test(count):
        click.echo(f'Count: {count}')

    @cli.command()
    @click.option('--count', default=1)
    @click.pass_context
    def dist(ctx, count):
        await ctx.forward(test)
        await ctx.invoke(test, count=42)

And what it looks like:

.. click:run::

    invoke(cli, prog_name='cli', args=['dist'])



.. _forwarding-unknown-options:

Forwarding Unknown Options
--------------------------

In some situations it is interesting to be able to accept all unknown
options for further manual processing.  Click can generally do that as of
Click 4.0, but it has some limitations that lie in the nature of the
problem.  The support for this is provided through a parser flag called
``ignore_unknown_options`` which will instruct the parser to collect all
unknown options and to put them to the leftover argument instead of
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:`~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.

For most situations the easiest solution is the second.  Once the behavior
is changed something needs to pick up those leftover options (which at
this point are considered arguments).  For this again you have two
options:

1.  You can use :func:`pass_context` to get the context passed.  This will
    only work if in addition to :attr:`~Context.ignore_unknown_options`
    you also set :attr:`~Context.allow_extra_args` as otherwise the
    command will abort with an error that there are leftover arguments.
    If you go with this solution, the extra arguments will be collected in
    :attr:`Context.args`.
2.  You can attach an :func:`argument` with ``nargs`` set to `-1` which
    will eat up all leftover arguments.  In this case it's recommended to
    set the `type` to :data:`UNPROCESSED` to avoid any string processing
    on those arguments as otherwise they are forced into unicode strings
    automatically which is often not what you want.

In the end you end up with something like this:

.. click:example::

    import sys
    from subprocess import call

    @click.command(context_settings=dict(
        ignore_unknown_options=True,
    ))
    @click.option('-v', '--verbose', is_flag=True, help='Enables verbose mode')
    @click.argument('timeit_args', nargs=-1, type=click.UNPROCESSED)
    def cli(verbose, timeit_args):
        """A fake wrapper around Python's timeit."""
        cmdline = ['echo', 'python', '-mtimeit'] + list(timeit_args)
        if verbose:
            click.echo(f"Invoking: {' '.join(cmdline)}")
        call(cmdline)

And what it looks like:

.. click:run::

    invoke(cli, prog_name='cli', args=['--help'])
    println()
    invoke(cli, prog_name='cli', args=['-n', '100', 'a = 1; b = 2; a * b'])
    println()
    invoke(cli, prog_name='cli', args=['-v', 'a = 1; b = 2; a * b'])

As you can see the verbosity flag is handled by Click, everything else
ends up in the `timeit_args` variable for further processing which then
for instance, allows invoking a subprocess.  There are a few things that
are important to know about how this ignoring of unhandled flag happens:

*   Unknown long options are generally ignored and not processed at all.
    So for instance if ``--foo=bar`` or ``--foo bar`` are passed they
    generally end up like that.  Note that because the parser cannot know
    if an option will accept an argument or not, the ``bar`` part might be
    handled as an argument.
*   Unknown short options might be partially handled and reassembled if
    necessary.  For instance in the above example there is an option
    called ``-v`` which enables verbose mode.  If the command would be
    ignored with ``-va`` then the ``-v`` part would be handled by Click
    (as it is known) and ``-a`` would end up in the leftover parameters
    for further processing.
*   Depending on what you plan on doing you might have some success by
    disabling interspersed arguments
    (:attr:`~Context.allow_interspersed_args`) which instructs the parser
    to not allow arguments and options to be mixed.  Depending on your
    situation this might improve your results.

Generally though the combined handling of options and arguments from
your own commands and commands from another application are discouraged
and if you can avoid it, you should.  It's a much better idea to have
everything below a subcommand be forwarded to another application than to
handle some arguments yourself.


Managing Resources
------------------

It can be useful to open a resource in a group, to be made available to
subcommands. Many types of resources need to be closed or otherwise
cleaned up after use. The standard way to do this in Python is by using
a context manager with the ``with`` statement.

For example, the ``Repo`` class from :doc:`complex` might actually be
defined as a context manager:

.. code-block:: python

    class Repo:
        def __init__(self, home=None):
            self.home = os.path.abspath(home or ".")
            self.db = None

        def __enter__(self):
            path = os.path.join(self.home, "repo.db")
            self.db = open_database(path)
            return self

        def __exit__(self, exc_type, exc_value, tb):
            self.db.close()

Ordinarily, it would be used with the ``with`` statement:

.. code-block:: python

    with Repo() as repo:
        repo.db.query(...)

However, a ``with`` block in a group would exit and close the database
before it could be used by a subcommand.

Instead, use the context's :meth:`~click.Context.with_resource` method
to enter the context manager and return the resource. When the group and
any subcommands finish, the context's resources are cleaned up.

.. code-block:: python

    @click.group()
    @click.option("--repo-home", default=".repo")
    @click.pass_context
    def cli(ctx, repo_home):
        ctx.obj = ctx.with_resource(Repo(repo_home))

    @cli.command()
    @click.pass_obj
    def log(obj):
        # obj is the repo opened in the cli group
        for entry in obj.db.query(...):
            click.echo(entry)

If the resource isn't a context manager, usually it can be wrapped in
one using something from :mod:`contextlib`. If that's not possible, use
the context's :meth:`~click.Context.call_on_close` method to register a
cleanup function.

.. code-block:: python

    @click.group()
    @click.option("--name", default="repo.db")
    @click.pass_context
    def cli(ctx, repo_home):
        ctx.obj = db = open_db(repo_home)

        @ctx.call_on_close
        def close_db():
            db.record_use()
            db.save()
            db.close()


.. versionchanged:: 8.2 ``Context.call_on_close`` and context managers registered
    via ``Context.with_resource`` will be closed when the CLI exits. These were
    previously not called on exit.