diff --git a/.github/workflows/test-flask.yaml b/.github/workflows/test-flask.yaml new file mode 100644 index 000000000..9fe4785f7 --- /dev/null +++ b/.github/workflows/test-flask.yaml @@ -0,0 +1,25 @@ +name: Test Flask Main +on: + pull_request: + paths-ignore: ['docs/**', 'README.md'] + push: + branches: [main, stable] + paths-ignore: ['docs/**', 'README.md'] +jobs: + flask-tests: + name: flask-tests + runs-on: ubuntu-latest + steps: + - uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3 + with: + enable-cache: true + prune-cache: false + - run: git clone https://github.com/pallets/flask + - run: uv venv --python 3.14 + working-directory: ./flask + - run: source .venv/bin/activate + working-directory: ./flask + - run: uv sync --all-extras + working-directory: ./flask + - run: uv run --with "git+https://github.com/pallets/click.git@main" -- pytest + working-directory: ./flask diff --git a/docs/complex.rst b/docs/complex.md similarity index 51% rename from docs/complex.rst rename to docs/complex.md index c176617a1..3bb77f684 100644 --- a/docs/complex.rst +++ b/docs/complex.md @@ -1,9 +1,9 @@ -.. _complex-guide: +(complex-guide)= -Complex Applications -==================== +# Complex Applications -.. currentmodule:: click +```{currentmodule} click +``` Click is designed to assist with the creation of complex and simple CLI tools alike. However, the power of its design is the ability to arbitrarily nest @@ -16,20 +16,21 @@ In a theoretical world of two separate Click command line utilities, they could solve this problem by nesting one inside the other. For instance, the web framework could also load the commands for the message queue framework. -.. contents:: - :depth: 1 - :local: +```{contents} +--- +depth: 1 +local: true +--- +``` -Basic Concepts --------------- +## Basic Concepts To understand how this works, you need to understand two concepts: contexts and the calling convention. -Contexts -```````` +### Contexts -Whenever a Click command is executed, a :class:`Context` object is created +Whenever a Click command is executed, a {class}`Context` object is created which holds state for this particular invocation. It remembers parsed parameters, what command created it, which resources need to be cleaned up at the end of the function, and so forth. It can also optionally hold an @@ -46,38 +47,36 @@ it if needed. Most of the time, you do not see the context object, but when writing more complex applications it comes in handy. This brings us to the next point. -Calling Convention -`````````````````` +### Calling Convention When a Click command callback is executed, it's passed all the non-hidden parameters as keyword arguments. Notably absent is the context. However, a callback can opt into being passed to the context object by marking itself -with :func:`pass_context`. +with {func}`pass_context`. So how do you invoke a command callback if you don't know if it should receive the context or not? The answer is that the context itself -provides a helper function (:meth:`Context.invoke`) which can do this for +provides a helper function ({meth}`Context.invoke`) which can do this for you. It accepts the callback as first argument and then invokes the function correctly. -Building a Git Clone --------------------- +## Building a Git Clone In this example, we want to build a command line tool that resembles a version control system. Systems like Git usually provide one over-arching command that already accepts some parameters and configuration, and then have extra subcommands that do other things. -The Root Command -```````````````` +### The Root Command At the top level, we need a group that can hold all our commands. In this -case, we use the basic :func:`click.group` which allows us to register +case, we use the basic {func}`click.group` which allows us to register other Click commands below it. For this command, we also want to accept some parameters that configure the state of our tool: +```{eval-rst} .. click:example:: import os @@ -97,59 +96,57 @@ state of our tool: @click.pass_context def cli(ctx, repo_home, debug): ctx.obj = Repo(repo_home, debug) - +``` Let's understand what this does. We create a group command which can have subcommands. When it is invoked, it will create an instance of a -``Repo`` class. This holds the state for our command line tool. In this +`Repo` class. This holds the state for our command line tool. In this case, it just remembers some parameters, but at this point it could also start loading configuration files and so on. -This state object is then remembered by the context as :attr:`~Context.obj`. +This state object is then remembered by the context as {attr}`~Context.obj`. This is a special attribute where commands are supposed to remember what they need to pass on to their children. In order for this to work, we need to mark our function with -:func:`pass_context`, because otherwise, the context object would be +{func}`pass_context`, because otherwise, the context object would be entirely hidden from us. -The First Child Command -``````````````````````` +### The First Child Command Let's add our first child command to it, the clone command: -.. click:example:: - - @cli.command() - @click.argument('src') - @click.argument('dest', required=False) - def clone(src, dest): - pass +```python +@cli.command() +@click.argument('src') +@click.argument('dest', required=False) +def clone(src, dest): + pass +``` So now we have a clone command, but how do we get access to the repo? As -you can imagine, one way is to use the :func:`pass_context` function which +you can imagine, one way is to use the {func}`pass_context` function which again will make our callback also get the context passed on which we memorized the repo. However, there is a second version of this decorator -called :func:`pass_obj` which will just pass the stored object, (in our case +called {func}`pass_obj` which will just pass the stored object, (in our case the repo): -.. click:example:: - - @cli.command() - @click.argument('src') - @click.argument('dest', required=False) - @click.pass_obj - def clone(repo, src, dest): - pass +```python +@cli.command() +@click.argument('src') +@click.argument('dest', required=False) +@click.pass_obj +def clone(repo, src, dest): + pass +``` -Interleaved Commands -```````````````````` +### Interleaved Commands While not relevant for the particular program we want to build, there is also quite good support for interleaving systems. Imagine for instance that there was a super cool plugin for our version control system that needed a lot of configuration and wanted to store its own configuration as -:attr:`~Context.obj`. If we would then attach another command below that, +{attr}`~Context.obj`. If we would then attach another command below that, we would all of a sudden get the plugin configuration instead of our repo object. @@ -162,45 +159,44 @@ linked nature of contexts. We know that the plugin context is linked to the context that created our repo. Because of that, we can start a search for the last level where the object stored by the context was a repo. -Built-in support for this is provided by the :func:`make_pass_decorator` +Built-in support for this is provided by the {func}`make_pass_decorator` factory, which will create decorators for us that find objects (it -internally calls into :meth:`Context.find_object`). In our case, we -know that we want to find the closest ``Repo`` object, so let's make a +internally calls into {meth}`Context.find_object`). In our case, we +know that we want to find the closest `Repo` object, so let's make a decorator for this: -.. click:example:: - - pass_repo = click.make_pass_decorator(Repo) +```python +pass_repo = click.make_pass_decorator(Repo) +``` -If we now use ``pass_repo`` instead of ``pass_obj``, we will always get a +If we now use `pass_repo` instead of `pass_obj`, we will always get a repo instead of something else: -.. click:example:: - - @cli.command() - @click.argument('src') - @click.argument('dest', required=False) - @pass_repo - def clone(repo, src, dest): - pass +```python +@cli.command() +@click.argument('src') +@click.argument('dest', required=False) +@pass_repo +def clone(repo, src, dest): + pass +``` -Ensuring Object Creation -```````````````````````` +### Ensuring Object Creation The above example only works if there was an outer command that created a -``Repo`` object and stored it in the context. For some more advanced use +`Repo` object and stored it in the context. For some more advanced use cases, this might become a problem. The default behavior of -:func:`make_pass_decorator` is to call :meth:`Context.find_object` +{func}`make_pass_decorator` is to call {meth}`Context.find_object` which will find the object. If it can't find the object, -:meth:`make_pass_decorator` will raise an error. -The alternative behavior is to use :meth:`Context.ensure_object` +{meth}`make_pass_decorator` will raise an error. +The alternative behavior is to use {meth}`Context.ensure_object` which will find the object, and if it cannot find it, will create one and store it in the innermost context. This behavior can also be enabled for -:func:`make_pass_decorator` by passing ``ensure=True``: +{func}`make_pass_decorator` by passing `ensure=True`: -.. click:example:: - - pass_repo = click.make_pass_decorator(Repo, ensure=True) +```python +pass_repo = click.make_pass_decorator(Repo, ensure=True) +``` In this case, the innermost context gets an object created if it is missing. This might replace objects being placed there earlier. In this @@ -210,173 +206,169 @@ no arguments. As such it runs standalone: -.. click:example:: - - @click.command() - @pass_repo - def cp(repo): - click.echo(isinstance(repo, Repo)) - +```python +@click.command() +@pass_repo +def cp(repo): + click.echo(isinstance(repo, Repo)) +``` As you can see: -.. click:run:: - - invoke(cp, []) +```console +$ cp +True +``` -Lazily Loading Subcommands --------------------------- +## 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:`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. +{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, +Since the primary case for this is a {class}`Group` which loads its subcommands lazily, the following example shows a lazy-group implementation. -.. warning:: +```{warning} +Lazy loading of python code can result in hard to track down bugs, circular imports +in order-dependent codebases, and other surprising behaviors. It is recommended that +this technique only be used in concert with testing which will at least run the +`--help` on each subcommand. That will guarantee that each subcommand can be loaded +successfully. +``` - Lazy loading of python code can result in hard to track down bugs, circular imports - in order-dependent codebases, and other surprising behaviors. It is recommended that - this technique only be used in concert with testing which will at least run the - ``--help`` on each subcommand. That will guarantee that each subcommand can be loaded - successfully. +### Defining the Lazy Group -Defining the Lazy Group -``````````````````````` - -The following :class:`Group` subclass adds an attribute, ``lazy_subcommands``, which +The following {class}`Group` subclass adds an attribute, `lazy_subcommands`, which stores a mapping from subcommand names to the information for importing them. -.. code-block:: python - - # in lazy_group.py - import importlib - import click - class LazyGroup(click.Group): - def __init__(self, *args, lazy_subcommands=None, **kwargs): - super().__init__(*args, **kwargs) - # lazy_subcommands is a map of the form: - # - # {command-name} -> {module-name}.{command-object-name} - # - self.lazy_subcommands = lazy_subcommands or {} - - def list_commands(self, ctx): - base = super().list_commands(ctx) - lazy = sorted(self.lazy_subcommands.keys()) - return base + lazy - - def get_command(self, ctx, cmd_name): - if cmd_name in self.lazy_subcommands: - return self._lazy_load(cmd_name) - return super().get_command(ctx, cmd_name) - - def _lazy_load(self, cmd_name): - # lazily loading a command, first get the module name and attribute name - import_path = self.lazy_subcommands[cmd_name] - modname, cmd_object_name = import_path.rsplit(".", 1) - # do the import - mod = importlib.import_module(modname) - # 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.Command): - raise ValueError( - f"Lazy loading of {import_path} failed by returning " - "a non-command object" - ) - return cmd_object - -Using LazyGroup To Define a CLI -``````````````````````````````` - -With ``LazyGroup`` defined, it's now possible to write a group which lazily loads its +```python +# in lazy_group.py +import importlib +import click + +class LazyGroup(click.Group): + def __init__(self, *args, lazy_subcommands=None, **kwargs): + super().__init__(*args, **kwargs) + # lazy_subcommands is a map of the form: + # + # {command-name} -> {module-name}.{command-object-name} + # + self.lazy_subcommands = lazy_subcommands or {} + + def list_commands(self, ctx): + base = super().list_commands(ctx) + lazy = sorted(self.lazy_subcommands.keys()) + return base + lazy + + def get_command(self, ctx, cmd_name): + if cmd_name in self.lazy_subcommands: + return self._lazy_load(cmd_name) + return super().get_command(ctx, cmd_name) + + def _lazy_load(self, cmd_name): + # lazily loading a command, first get the module name and attribute name + import_path = self.lazy_subcommands[cmd_name] + modname, cmd_object_name = import_path.rsplit(".", 1) + # do the import + mod = importlib.import_module(modname) + # 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.Command): + raise ValueError( + f"Lazy loading of {import_path} failed by returning " + "a non-command object" + ) + return cmd_object +``` + +### Using LazyGroup To Define a CLI + +With `LazyGroup` defined, it's now possible to write a group which lazily loads its subcommands like so: -.. code-block:: python - - # in main.py - import click - from lazy_group import LazyGroup - - @click.group( - cls=LazyGroup, - lazy_subcommands={"foo": "foo.cli", "bar": "bar.cli"}, - help="main CLI command for lazy example", - ) - def cli(): - pass - -.. code-block:: python - - # in foo.py - import click - - @click.group(help="foo command for lazy example") - def cli(): - pass - -.. code-block:: python - - # in bar.py - import click - from lazy_group import LazyGroup - - @click.group( - cls=LazyGroup, - lazy_subcommands={"baz": "baz.cli"}, - help="bar command for lazy example", - ) - def cli(): - pass - -.. code-block:: python - - # in baz.py - import click - - @click.group(help="baz command for lazy example") - def cli(): - pass - - -What triggers Lazy Loading? -``````````````````````````` +```python +# in main.py +import click +from lazy_group import LazyGroup + +@click.group( + cls=LazyGroup, + lazy_subcommands={"foo": "foo.cli", "bar": "bar.cli"}, + help="main CLI command for lazy example", +) +def cli(): + pass +``` + +```python +# in foo.py +import click + +@click.group(help="foo command for lazy example") +def cli(): + pass +``` + +```python +# in bar.py +import click +from lazy_group import LazyGroup + +@click.group( + cls=LazyGroup, + lazy_subcommands={"baz": "baz.cli"}, + help="bar command for lazy example", +) +def cli(): + pass +``` + +```python +# in baz.py +import click + +@click.group(help="baz command for lazy example") +def cli(): + pass +``` + +### What triggers Lazy Loading? There are several events which may trigger lazy loading by running the -:meth:`Group.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 -name is ``cli``. +name is `cli`. -1. Command resolution. If a user runs ``cli bar baz``, this must first resolve ``bar``, - and then resolve ``baz``. Each subcommand resolution step does a lazy load. +1. Command resolution. If a user runs `cli bar baz`, this must first resolve `bar`, + and then resolve `baz`. Each subcommand resolution step does a lazy load. 2. Helptext rendering. In order to get the short help description of subcommands, - ``cli --help`` will load ``foo`` and ``bar``. Note that it will still not load - ``baz``. -3. Shell completion. In order to get the subcommands of a lazy command, ``cli `` - will need to resolve the subcommands of ``cli``. This process will trigger the lazy + `cli --help` will load `foo` and `bar`. Note that it will still not load + `baz`. +3. Shell completion. In order to get the subcommands of a lazy command, `cli ` + will need to resolve the subcommands of `cli`. This process will trigger the lazy loads. -Further Deferring Imports -````````````````````````` +### 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:`Command` 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:`Command.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. -This command definition provides ``foo``, but any of the work associated with importing +This command definition provides `foo`, but any of the work associated with importing the "real" callback function is deferred until invocation time: +```{eval-rst} .. click:example:: @click.command() @@ -386,6 +378,7 @@ the "real" callback function is deferred until invocation time: from mylibrary import foo_concrete foo_concrete(n, w) +``` 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 diff --git a/docs/documentation.md b/docs/documentation.md index 25ca18dfa..a03177334 100644 --- a/docs/documentation.md +++ b/docs/documentation.md @@ -145,6 +145,22 @@ For single option boolean flags, the default remains hidden if the default value invoke(dots, args=['--help']) ``` +## Showing Environment Variables + +To control the appearance of environment variables pass `show_envvar`. + +```{eval-rst} +.. click:example:: + + @click.command() + @click.option('--username', envvar='USERNAME', show_envvar=True) + def greet(username): + click.echo(f'Hello {username}!') + +.. click:run:: + invoke(greet, args=['--help']) +``` + ## Click's Wrapping Behavior Click's default wrapping ignores single new lines and rewraps the text based on the width of the terminal to a maximum of 80 characters by default, but this can be modified with {attr}`~Context.max_content_width`. In the example notice how the second grouping of three lines is rewrapped into a single paragraph.