├── src └── asyncclick │ ├── py.typed │ ├── _utils.py │ ├── _textwrap.py │ ├── globals.py │ └── __init__.py ├── examples ├── complex │ ├── complex │ │ ├── __init__.py │ │ ├── commands │ │ │ ├── __init__.py │ │ │ ├── cmd_status.py │ │ │ └── cmd_init.py │ │ └── cli.py │ ├── pyproject.toml │ └── README ├── imagepipe │ ├── .gitignore │ ├── example01.jpg │ ├── example02.jpg │ ├── README │ └── pyproject.toml ├── aliases │ ├── aliases.ini │ ├── pyproject.toml │ ├── README │ └── aliases.py ├── README ├── termui │ ├── README │ ├── pyproject.toml │ └── termui.py ├── repo │ ├── README │ ├── pyproject.toml │ └── repo.py ├── colors │ ├── README │ ├── pyproject.toml │ └── colors.py ├── inout │ ├── README │ ├── pyproject.toml │ └── inout.py ├── validation │ ├── README │ ├── pyproject.toml │ └── validation.py ├── naval │ ├── pyproject.toml │ ├── README │ └── naval.py └── completion │ ├── pyproject.toml │ ├── README │ └── completion.py ├── requirements ├── tests.in ├── dev.in ├── build.txt ├── typing.txt ├── tests.txt ├── docs.txt └── dev.txt ├── docs ├── changes.rst ├── setuptools.md ├── license.rst ├── faqs.md ├── virtualenv.md ├── _static │ ├── click-icon.svg │ ├── click-logo.svg │ └── click-name.svg ├── conf.py ├── parameters.md ├── support-multiple-versions.md ├── option-decorators.rst ├── entry-points.rst ├── click-concepts.rst ├── wincmd.md ├── exceptions.rst ├── index.rst ├── contrib.md ├── handling-files.rst ├── api.rst ├── extending-click.rst ├── unicode-support.md ├── arguments.rst ├── testing.md ├── prompts.md ├── parameter-types.rst ├── why.md ├── quickstart.md └── documentation.md ├── .devcontainer ├── on-create-command.sh └── devcontainer.json ├── .gitignore ├── tests ├── typing │ ├── typing_group_kw_options.py │ ├── typing_help_option.py │ ├── typing_password_option.py │ ├── typing_version_option.py │ ├── typing_confirmation_option.py │ ├── typing_options.py │ ├── typing_simple_example.py │ ├── typing_progressbar.py │ └── typing_aliased_group.py ├── test_compat.py ├── conftest.py ├── test_parser.py ├── test_imports.py ├── test_normalization.py ├── test_command_decorators.py ├── test_defaults.py ├── test_custom_classes.py ├── test_chain.py └── test_types.py ├── .editorconfig ├── Makefile ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ └── bug-report.md ├── workflows │ ├── lock.yaml │ ├── pre-commit.yaml │ ├── publish.yaml │ └── tests.yaml └── pull_request_template.md ├── .readthedocs.yaml ├── .pre-commit-config.yaml ├── LICENSE.txt ├── tox.ini ├── README.md └── pyproject.toml /src/asyncclick/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/complex/complex/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/tests.in: -------------------------------------------------------------------------------- 1 | pytest 2 | trio 3 | -------------------------------------------------------------------------------- /examples/complex/complex/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/imagepipe/.gitignore: -------------------------------------------------------------------------------- 1 | processed-* 2 | -------------------------------------------------------------------------------- /examples/aliases/aliases.ini: -------------------------------------------------------------------------------- 1 | [aliases] 2 | ci=commit 3 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | .. include:: ../CHANGES.rst 5 | -------------------------------------------------------------------------------- /docs/setuptools.md: -------------------------------------------------------------------------------- 1 | --- 2 | orphan: true 3 | --- 4 | 5 | # Setuptools Integration 6 | 7 | Moved to {doc}`entry-points`. 8 | -------------------------------------------------------------------------------- /examples/imagepipe/example01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/click-contrib/asyncclick/HEAD/examples/imagepipe/example01.jpg -------------------------------------------------------------------------------- /examples/imagepipe/example02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/click-contrib/asyncclick/HEAD/examples/imagepipe/example02.jpg -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | -r docs.in 2 | -r tests.in 3 | -r typing.in 4 | pip-compile-multi 5 | pre-commit 6 | tox 7 | anyio 8 | trio 9 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | BSD-3-Clause License 2 | ==================== 3 | 4 | .. literalinclude:: ../LICENSE.txt 5 | :language: text 6 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | Click Examples 2 | 3 | This folder contains various Click examples. Note that 4 | all of these are not runnable by themselves but should be 5 | installed into a virtualenv. 6 | -------------------------------------------------------------------------------- /examples/termui/README: -------------------------------------------------------------------------------- 1 | $ termui_ 2 | 3 | termui showcases the different terminal UI helpers that 4 | Click provides. 5 | 6 | Usage: 7 | 8 | $ pip install --editable . 9 | $ termui --help 10 | -------------------------------------------------------------------------------- /examples/repo/README: -------------------------------------------------------------------------------- 1 | $ repo_ 2 | 3 | repo is a simple example of an application that looks 4 | and works similar to hg or git. 5 | 6 | Usage: 7 | 8 | $ pip install --editable . 9 | $ repo --help 10 | -------------------------------------------------------------------------------- /.devcontainer/on-create-command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | python3 -m venv --upgrade-deps .venv 4 | . .venv/bin/activate 5 | pip install -r requirements/dev.txt 6 | pip install -e . 7 | pre-commit install --install-hooks 8 | -------------------------------------------------------------------------------- /examples/colors/README: -------------------------------------------------------------------------------- 1 | $ colors_ 2 | 3 | colors is a simple example that shows how you can 4 | colorize text. 5 | 6 | Uses colorama on Windows. 7 | 8 | Usage: 9 | 10 | $ pip install --editable . 11 | $ colors 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/*.egg-info/ 2 | /.hypothesis/ 3 | /build/ 4 | /.pybuild/ 5 | .cache/ 6 | .idea/ 7 | .vscode/ 8 | __pycache__/ 9 | /dist/ 10 | /.pytest_cache/ 11 | dist/ 12 | .coverage* 13 | htmlcov/ 14 | .tox/ 15 | docs/_build/ 16 | /debian/ 17 | -------------------------------------------------------------------------------- /tests/typing/typing_group_kw_options.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import assert_type 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.group(context_settings={}) 7 | def hello() -> None: 8 | pass 9 | 10 | 11 | assert_type(hello, click.Group) 12 | -------------------------------------------------------------------------------- /examples/inout/README: -------------------------------------------------------------------------------- 1 | $ inout_ 2 | 3 | inout is a simple example of an application that 4 | can read from files and write to files but also 5 | accept input from stdin or write to stdout. 6 | 7 | Usage: 8 | 9 | $ pip install --editable . 10 | $ inout input_file.txt output_file.txt 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | charset = utf-8 10 | max_line_length = 88 11 | 12 | [*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | PACKAGE=asyncclick 4 | ifneq ($(wildcard /usr/share/sourcemgr/make/py),) 5 | include /usr/share/sourcemgr/make/py 6 | # available via http://github.com/smurfix/sourcemgr 7 | 8 | else 9 | %: 10 | @echo "Please use 'python setup.py'." 11 | @exit 1 12 | endif 13 | -------------------------------------------------------------------------------- /examples/validation/README: -------------------------------------------------------------------------------- 1 | $ validation_ 2 | 3 | validation is a simple example of an application that 4 | performs custom validation of parameters in different 5 | ways. 6 | 7 | This example requires Click 2.0 or higher. 8 | 9 | Usage: 10 | 11 | $ pip install --editable . 12 | $ validation --help 13 | -------------------------------------------------------------------------------- /tests/typing/typing_help_option.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import assert_type 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.command() 7 | @click.help_option("-h", "--help") 8 | def hello() -> None: 9 | """Simple program that greets NAME for a total of COUNT times.""" 10 | click.echo("Hello!") 11 | 12 | 13 | assert_type(hello, click.Command) 14 | -------------------------------------------------------------------------------- /tests/typing/typing_password_option.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.password_option() 10 | def encrypt(password: str) -> None: 11 | click.echo(f"encoded: to {codecs.encode(password, 'rot13')}") 12 | 13 | 14 | assert_type(encrypt, click.Command) 15 | -------------------------------------------------------------------------------- /tests/test_compat.py: -------------------------------------------------------------------------------- 1 | from asyncclick._compat import should_strip_ansi 2 | 3 | 4 | def test_is_jupyter_kernel_output(): 5 | class JupyterKernelFakeStream: 6 | pass 7 | 8 | # implementation detail, aka cheapskate test 9 | JupyterKernelFakeStream.__module__ = "ipykernel.faked" 10 | assert not should_strip_ansi(stream=JupyterKernelFakeStream()) 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Questions on Discussions 4 | url: https://github.com/pallets/click/discussions/ 5 | about: Ask questions about your own code on the Discussions tab. 6 | - name: Questions on Chat 7 | url: https://discord.gg/pallets 8 | about: Ask questions about your own code on our Discord chat. 9 | -------------------------------------------------------------------------------- /examples/imagepipe/README: -------------------------------------------------------------------------------- 1 | $ imagepipe_ 2 | 3 | imagepipe is an example application that implements some 4 | commands that chain image processing instructions 5 | together. 6 | 7 | This requires pillow. 8 | 9 | Usage: 10 | 11 | $ pip install --editable . 12 | $ imagepipe open -i example01.jpg resize -w 128 display 13 | $ imagepipe open -i example02.jpg blur save 14 | -------------------------------------------------------------------------------- /examples/complex/complex/commands/cmd_status.py: -------------------------------------------------------------------------------- 1 | from complex.cli import pass_environment 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.command("status", short_help="Shows file changes.") 7 | @pass_environment 8 | def cli(ctx): 9 | """Shows file changes in the current working directory.""" 10 | ctx.log("Changed files: none") 11 | ctx.vlog("bla bla bla, debug info") 12 | -------------------------------------------------------------------------------- /tests/typing/typing_version_option.py: -------------------------------------------------------------------------------- 1 | """ 2 | From https://click.palletsprojects.com/en/stable/options/#callbacks-and-eager-options. 3 | """ 4 | 5 | from typing_extensions import assert_type 6 | 7 | import asyncclick as click 8 | 9 | 10 | @click.command() 11 | @click.version_option("0.1") 12 | def hello() -> None: 13 | click.echo("Hello World!") 14 | 15 | 16 | assert_type(hello, click.Command) 17 | -------------------------------------------------------------------------------- /examples/repo/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-repo" 3 | version = "1.0.0" 4 | description = "Click repo example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | repo = "repo:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "repo" 19 | -------------------------------------------------------------------------------- /examples/inout/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-inout" 3 | version = "1.0.0" 4 | description = "Click inout example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | inout = "inout:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "inout" 19 | -------------------------------------------------------------------------------- /examples/naval/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-naval" 3 | version = "1.0.0" 4 | description = "Click naval example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | naval = "naval:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "naval" 19 | -------------------------------------------------------------------------------- /examples/colors/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-colors" 3 | version = "1.0.0" 4 | description = "Click colors example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | colors = "colors:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "colors" 19 | -------------------------------------------------------------------------------- /examples/termui/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-termui" 3 | version = "1.0.0" 4 | description = "Click termui example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | termui = "termui:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "termui" 19 | -------------------------------------------------------------------------------- /examples/aliases/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-aliases" 3 | version = "1.0.0" 4 | description = "Click aliases example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | aliases = "aliases:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "aliases" 19 | -------------------------------------------------------------------------------- /examples/complex/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-complex" 3 | version = "1.0.0" 4 | description = "Click complex example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | complex = "complex.cli:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "complex" 19 | -------------------------------------------------------------------------------- /examples/naval/README: -------------------------------------------------------------------------------- 1 | $ naval_ 2 | 3 | naval is a simple example of an application that 4 | is ported from the docopt example of the same name. 5 | 6 | Unlike the original this one also runs some code and 7 | prints messages and it's command line interface was 8 | changed slightly to make more sense with established 9 | POSIX semantics. 10 | 11 | Usage: 12 | 13 | $ pip install --editable . 14 | $ naval --help 15 | -------------------------------------------------------------------------------- /tests/typing/typing_confirmation_option.py: -------------------------------------------------------------------------------- 1 | """From https://click.palletsprojects.com/en/stable/options/#yes-parameters""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.confirmation_option(prompt="Are you sure you want to drop the db?") 10 | def dropdb() -> None: 11 | click.echo("Dropped all tables!") 12 | 13 | 14 | assert_type(dropdb, click.Command) 15 | -------------------------------------------------------------------------------- /examples/completion/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-completion" 3 | version = "1.0.0" 4 | description = "Click completion example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | completion = "completion:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "completion" 19 | -------------------------------------------------------------------------------- /examples/validation/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-validation" 3 | version = "1.0.0" 4 | description = "Click validation example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | ] 9 | 10 | [project.scripts] 11 | validation = "validation:cli" 12 | 13 | [build-system] 14 | requires = ["flit_core<4"] 15 | build-backend = "flit_core.buildapi" 16 | 17 | [tool.flit.module] 18 | name = "validation" 19 | -------------------------------------------------------------------------------- /examples/imagepipe/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click-example-imagepipe" 3 | version = "1.0.0" 4 | description = "Click imagepipe example" 5 | requires-python = ">=3.10" 6 | dependencies = [ 7 | "click>=8.1", 8 | "pillow", 9 | ] 10 | 11 | [project.scripts] 12 | imagepipe = "imagepipe:cli" 13 | 14 | [build-system] 15 | requires = ["flit_core<4"] 16 | build-backend = "flit_core.buildapi" 17 | 18 | [tool.flit.module] 19 | name = "imagepipe" 20 | -------------------------------------------------------------------------------- /requirements/build.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/build.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | build==1.2.2.post1 13 | # via -r requirements/build.in 14 | packaging==24.2 15 | # via build 16 | pyproject-hooks==1.2.0 17 | # via build 18 | -------------------------------------------------------------------------------- /examples/complex/complex/commands/cmd_init.py: -------------------------------------------------------------------------------- 1 | from complex.cli import pass_environment 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.command("init", short_help="Initializes a repo.") 7 | @click.argument("path", required=False, type=click.Path(resolve_path=True)) 8 | @pass_environment 9 | def cli(ctx, path): 10 | """Initializes a repository.""" 11 | if path is None: 12 | path = ctx.home 13 | ctx.log(f"Initialized the repository in {click.format_filename(path)}") 14 | -------------------------------------------------------------------------------- /tests/typing/typing_options.py: -------------------------------------------------------------------------------- 1 | """From https://click.palletsprojects.com/en/stable/quickstart/#adding-parameters""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.option("--count", default=1, help="number of greetings") 10 | @click.argument("name") 11 | def hello(count: int, name: str) -> None: 12 | for _ in range(count): 13 | click.echo(f"Hello {name}!") 14 | 15 | 16 | assert_type(hello, click.Command) 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature for Click 4 | --- 5 | 6 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pallets/click", 3 | "image": "mcr.microsoft.com/devcontainers/python:3", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv", 8 | "python.terminal.activateEnvInCurrentTerminal": true, 9 | "python.terminal.launchArgs": [ 10 | "-X", 11 | "dev" 12 | ] 13 | } 14 | } 15 | }, 16 | "onCreateCommand": ".devcontainer/on-create-command.sh" 17 | } 18 | -------------------------------------------------------------------------------- /examples/aliases/README: -------------------------------------------------------------------------------- 1 | $ aliases_ 2 | 3 | aliases is a fairly advanced example that shows how 4 | to implement command aliases with Click. It uses a 5 | subclass of the default group to customize how commands 6 | are located. 7 | 8 | It supports both aliases read from a config file as well 9 | as automatic abbreviations. 10 | 11 | The aliases from the config are read from the aliases.ini 12 | file. Try `aliases st` and `aliases ci`! 13 | 14 | Usage: 15 | 16 | $ pip install --editable . 17 | $ aliases --help 18 | -------------------------------------------------------------------------------- /examples/complex/README: -------------------------------------------------------------------------------- 1 | $ complex_ 2 | 3 | complex is an example of building very complex cli 4 | applications that load subcommands dynamically from 5 | a plugin folder and other things. 6 | 7 | All the commands are implemented as plugins in the 8 | `complex.commands` package. If a python module is 9 | placed named "cmd_foo" it will show up as "foo" 10 | command and the `cli` object within it will be 11 | loaded as nested Click command. 12 | 13 | Usage: 14 | 15 | $ pip install --editable . 16 | $ complex --help 17 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | # Do not specify sphinx key here to be in full control of build steps. 3 | # https://docs.readthedocs.com/platform/stable/build-customization.html#extend-or-override-the-build-process 4 | 5 | build: 6 | os: ubuntu-24.04 7 | tools: 8 | python: '3.13' 9 | jobs: 10 | install: 11 | - echo "Installing dependencies" 12 | - asdf plugin add uv 13 | - asdf install uv latest 14 | - asdf global uv latest 15 | build: 16 | html: 17 | - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html 18 | -------------------------------------------------------------------------------- /requirements/typing.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/typing.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | mypy==1.13.0 13 | # via -r requirements/typing.in 14 | mypy-extensions==1.0.0 15 | # via mypy 16 | nodeenv==1.9.1 17 | # via pyright 18 | pyright==1.1.390 19 | # via -r requirements/typing.in 20 | typing-extensions==4.12.2 21 | # via 22 | # mypy 23 | # pyright 24 | -------------------------------------------------------------------------------- /tests/typing/typing_simple_example.py: -------------------------------------------------------------------------------- 1 | """The simple example from https://github.com/pallets/click#a-simple-example.""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.option("--count", default=1, help="Number of greetings.") 10 | @click.option("--name", prompt="Your name", help="The person to greet.") 11 | def hello(count: int, name: str) -> None: 12 | """Simple program that greets NAME for a total of COUNT times.""" 13 | for _ in range(count): 14 | click.echo(f"Hello, {name}!") 15 | 16 | 17 | assert_type(hello, click.Command) 18 | -------------------------------------------------------------------------------- /tests/typing/typing_progressbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing_extensions import assert_type 4 | 5 | from asyncclick import progressbar 6 | from asyncclick._termui_impl import ProgressBar 7 | 8 | 9 | def test_length_is_int() -> None: 10 | with progressbar(length=5) as bar: 11 | assert_type(bar, ProgressBar[int]) 12 | for i in bar: 13 | assert_type(i, int) 14 | 15 | 16 | def it() -> tuple[str, ...]: 17 | return ("hello", "world") 18 | 19 | 20 | def test_generic_on_iterable() -> None: 21 | with progressbar(it()) as bar: 22 | assert_type(bar, ProgressBar[str]) 23 | for s in bar: 24 | assert_type(s, str) 25 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 9aeda5d1f4bbd212c557da1ea78eca9e8c829e19 # frozen: v0.11.13 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/astral-sh/uv-pre-commit 8 | rev: a621b109bab2e7e832d98c88fd3e83399f4e6657 # frozen: 0.7.12 9 | hooks: 10 | - id: uv-lock 11 | - repo: https://github.com/pre-commit/pre-commit-hooks 12 | rev: cef0300fd0fc4d2a87a85fa2093c6b283ea36f4b # frozen: v5.0.0 13 | hooks: 14 | - id: check-merge-conflict 15 | - id: debug-statements 16 | - id: fix-byte-order-marker 17 | - id: trailing-whitespace 18 | - id: end-of-file-fixer 19 | -------------------------------------------------------------------------------- /examples/completion/README: -------------------------------------------------------------------------------- 1 | $ completion 2 | ============ 3 | 4 | Demonstrates Click's shell completion support. 5 | 6 | .. code-block:: bash 7 | 8 | pip install --editable . 9 | 10 | For Bash: 11 | 12 | .. code-block:: bash 13 | 14 | eval "$(_COMPLETION_COMPLETE=bash_source completion)" 15 | 16 | For Zsh: 17 | 18 | .. code-block:: zsh 19 | 20 | eval "$(_COMPLETION_COMPLETE=zsh_source completion)" 21 | 22 | For Fish: 23 | 24 | .. code-block:: fish 25 | 26 | eval (env _COMPLETION_COMPLETE=fish_source completion) 27 | 28 | Now press tab (maybe twice) after typing something to see completions. 29 | 30 | .. code-block:: python 31 | 32 | $ completion 33 | $ completion gr 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in Click (not other projects which depend on Click) 4 | --- 5 | 6 | 12 | 13 | 19 | 20 | 23 | 24 | Environment: 25 | 26 | - Python version: 27 | - Click version: 28 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | name: Lock inactive closed issues 2 | # Lock closed issues that have not received any further activity for two weeks. 3 | # This does not close open issues, only humans may do that. It is easier to 4 | # respond to new issues with fresh examples rather than continuing discussions 5 | # on old issues. 6 | 7 | on: 8 | schedule: 9 | - cron: '0 0 * * *' 10 | permissions: 11 | issues: write 12 | pull-requests: write 13 | discussions: write 14 | concurrency: 15 | group: lock 16 | jobs: 17 | lock: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 21 | with: 22 | issue-inactive-days: 14 23 | pr-inactive-days: 14 24 | discussion-inactive-days: 14 25 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/tests.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | attrs==24.3.0 13 | # via 14 | # outcome 15 | # trio 16 | idna==3.10 17 | # via trio 18 | iniconfig==2.0.0 19 | # via pytest 20 | outcome==1.3.0.post0 21 | # via trio 22 | packaging==24.2 23 | # via pytest 24 | pluggy==1.5.0 25 | # via pytest 26 | pytest==8.3.4 27 | # via -r requirements/tests.in 28 | sniffio==1.3.1 29 | # via trio 30 | sortedcontainers==2.4.0 31 | # via trio 32 | trio==0.28.0 33 | # via -r requirements/tests.in 34 | -------------------------------------------------------------------------------- /examples/inout/inout.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | 4 | @click.command() 5 | @click.argument("input", type=click.File("rb"), nargs=-1) 6 | @click.argument("output", type=click.File("wb")) 7 | def cli(input, output): 8 | """This script works similar to the Unix `cat` command but it writes 9 | into a specific file (which could be the standard output as denoted by 10 | the ``-`` sign). 11 | 12 | \b 13 | Copy stdin to stdout: 14 | inout - - 15 | 16 | \b 17 | Copy foo.txt and bar.txt to stdout: 18 | inout foo.txt bar.txt - 19 | 20 | \b 21 | Write stdin into the file foo.txt 22 | inout - foo.txt 23 | """ 24 | for f in input: 25 | while True: 26 | chunk = f.read(1024) 27 | if not chunk: 28 | break 29 | output.write(chunk) 30 | output.flush() 31 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | import anyio 4 | import pytest 5 | 6 | from asyncclick.testing import CliRunner 7 | 8 | 9 | class SyncCliRunner(CliRunner): 10 | def invoke(self, *a, _sync=False, **k): 11 | fn = super().invoke 12 | if _sync: 13 | return fn(*a, **k) 14 | 15 | # anyio now protects against nested calls, so we use a thread 16 | result = None 17 | 18 | def f(): 19 | nonlocal result, fn 20 | 21 | async def r(): 22 | return await fn(*a, **k) 23 | 24 | result = anyio.run(r) ## , backend="trio") 25 | 26 | t = Thread(target=f, name="TEST") 27 | t.start() 28 | t.join() 29 | return result 30 | 31 | 32 | @pytest.fixture(scope="function") 33 | def runner(request): 34 | return SyncCliRunner() 35 | 36 | 37 | @pytest.fixture(scope="function") 38 | def arunner(request): 39 | return CliRunner() 40 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import asyncclick as click 4 | from asyncclick.parser import _OptionParser 5 | from asyncclick.shell_completion import split_arg_string 6 | 7 | 8 | @pytest.mark.parametrize( 9 | ("value", "expect"), 10 | [ 11 | ("cli a b c", ["cli", "a", "b", "c"]), 12 | ("cli 'my file", ["cli", "my file"]), 13 | ("cli 'my file'", ["cli", "my file"]), 14 | ("cli my\\", ["cli", "my"]), 15 | ("cli my\\ file", ["cli", "my file"]), 16 | ], 17 | ) 18 | def test_split_arg_string(value, expect): 19 | assert split_arg_string(value) == expect 20 | 21 | 22 | def test_parser_default_prefixes(): 23 | parser = _OptionParser() 24 | assert parser._opt_prefixes == {"-", "--"} 25 | 26 | 27 | def test_parser_collects_prefixes(): 28 | ctx = click.Context(click.Command("test")) 29 | parser = _OptionParser(ctx) 30 | click.Option("+p", is_flag=True).add_to_parser(parser, ctx) 31 | click.Option("!e", is_flag=True).add_to_parser(parser, ctx) 32 | assert parser._opt_prefixes == {"-", "--", "+", "!"} 33 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main, stable] 6 | jobs: 7 | main: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 11 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 12 | with: 13 | enable-cache: true 14 | prune-cache: false 15 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 16 | id: setup-python 17 | with: 18 | python-version-file: pyproject.toml 19 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 20 | with: 21 | path: ~/.cache/pre-commit 22 | key: pre-commit|${{ hashFiles('pyproject.toml', '.pre-commit-config.yaml') }} 23 | - run: uv run --locked --group pre-commit pre-commit run --show-diff-on-failure --color=always --all-files 24 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 25 | if: ${{ !cancelled() }} 26 | -------------------------------------------------------------------------------- /src/asyncclick/_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | import typing as t 5 | 6 | 7 | class Sentinel(enum.Enum): 8 | """Enum used to define sentinel values. 9 | 10 | .. seealso:: 11 | 12 | `PEP 661 - Sentinel Values `_. 13 | """ 14 | 15 | UNSET = object() 16 | FLAG_NEEDS_VALUE = object() 17 | 18 | def __repr__(self) -> str: 19 | return f"{self.__class__.__name__}.{self.name}" 20 | 21 | 22 | UNSET = Sentinel.UNSET 23 | """Sentinel used to indicate that a value is not set.""" 24 | 25 | FLAG_NEEDS_VALUE = Sentinel.FLAG_NEEDS_VALUE 26 | """Sentinel used to indicate an option was passed as a flag without a 27 | value but is not a flag option. 28 | 29 | ``Option.consume_value`` uses this to prompt or use the ``flag_value``. 30 | """ 31 | 32 | T_UNSET = t.Literal[UNSET] # type: ignore[valid-type] 33 | """Type hint for the :data:`UNSET` sentinel value.""" 34 | 35 | T_FLAG_NEEDS_VALUE = t.Literal[FLAG_NEEDS_VALUE] # type: ignore[valid-type] 36 | """Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value.""" 37 | -------------------------------------------------------------------------------- /examples/colors/colors.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | 4 | all_colors = ( 5 | "black", 6 | "red", 7 | "green", 8 | "yellow", 9 | "blue", 10 | "magenta", 11 | "cyan", 12 | "white", 13 | "bright_black", 14 | "bright_red", 15 | "bright_green", 16 | "bright_yellow", 17 | "bright_blue", 18 | "bright_magenta", 19 | "bright_cyan", 20 | "bright_white", 21 | ) 22 | 23 | 24 | @click.command() 25 | def cli(): 26 | """This script prints some colors. It will also automatically remove 27 | all ANSI styles if data is piped into a file. 28 | 29 | Give it a try! 30 | """ 31 | for color in all_colors: 32 | click.echo(click.style(f"I am colored {color}", fg=color)) 33 | for color in all_colors: 34 | click.echo(click.style(f"I am colored {color} and bold", fg=color, bold=True)) 35 | for color in all_colors: 36 | click.echo(click.style(f"I am reverse colored {color}", fg=color, reverse=True)) 37 | 38 | click.echo(click.style("I am blinking", blink=True)) 39 | click.echo(click.style("I am underlined", underline=True)) 40 | -------------------------------------------------------------------------------- /docs/faqs.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | ```{contents} 4 | :depth: 2 5 | :local: true 6 | ``` 7 | 8 | ## General 9 | 10 | ### Shell Variable Expansion On Windows 11 | 12 | I have a simple Click app : 13 | 14 | ``` 15 | import click 16 | 17 | @click.command() 18 | @click.argument('message') 19 | def main(message: str): 20 | click.echo(message) 21 | 22 | if __name__ == '__main__': 23 | main() 24 | 25 | ``` 26 | 27 | When you pass an environment variable in the argument, it expands it: 28 | 29 | ```{code-block} powershell 30 | > Desktop python foo.py '$M0/.viola/2025-01-25-17-20-23-307878' 31 | > M:/home/ramrachum/.viola/2025-01-25-17-20-23-307878 32 | > 33 | ``` 34 | Note that I used single quotes above, so my shell is not expanding the environment variable, Click does. How do I get Click to not expand it? 35 | 36 | #### Answer 37 | 38 | If you don't want Click to emulate (as best it can) unix expansion on Windows, pass windows_expand_args=False when calling the CLI. 39 | Windows command line doesn't do any *, ~, or $ENV expansion. It also doesn't distinguish between double quotes and single quotes (where the later means "don't expand here"). Click emulates the expansion so that the app behaves similarly on both platforms, but doesn't receive information about what quotes were used. 40 | -------------------------------------------------------------------------------- /tests/typing/typing_aliased_group.py: -------------------------------------------------------------------------------- 1 | """Example from https://click.palletsprojects.com/en/stable/advanced/#command-aliases""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import assert_type 6 | 7 | import asyncclick as click 8 | 9 | 10 | class AliasedGroup(click.Group): 11 | def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: 12 | rv = click.Group.get_command(self, ctx, cmd_name) 13 | if rv is not None: 14 | return rv 15 | matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] 16 | if not matches: 17 | return None 18 | elif len(matches) == 1: 19 | return click.Group.get_command(self, ctx, matches[0]) 20 | ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 21 | 22 | async def resolve_command( 23 | self, ctx: click.Context, args: list[str] 24 | ) -> tuple[str | None, click.Command, list[str]]: 25 | # always return the full command name 26 | _, cmd, args = await super().resolve_command(ctx, args) 27 | assert cmd is not None 28 | return cmd.name, cmd, args 29 | 30 | 31 | @click.command(cls=AliasedGroup) 32 | def cli() -> None: 33 | pass 34 | 35 | 36 | assert_type(cli, AliasedGroup) 37 | 38 | 39 | @cli.command() 40 | def push() -> None: 41 | pass 42 | 43 | 44 | @cli.command() 45 | def pop() -> None: 46 | pass 47 | -------------------------------------------------------------------------------- /docs/virtualenv.md: -------------------------------------------------------------------------------- 1 | (virtualenv-heading)= 2 | 3 | # Virtualenv 4 | 5 | ## Why Use Virtualenv? 6 | 7 | You should use [Virtualenv](https://virtualenv.pypa.io/en/latest/) because: 8 | 9 | - It allows you to install multiple versions of the same dependency. 10 | - If you have an operating system version of Python, it prevents you from changing its dependencies and potentially 11 | messing up your os. 12 | 13 | ## How to Use Virtualenv 14 | 15 | Create your project folder, then a virtualenv within it: 16 | 17 | ```console 18 | $ mkdir myproject 19 | $ cd myproject 20 | $ python3 -m venv .venv 21 | ``` 22 | 23 | Now, whenever you want to work on a project, you only have to activate the corresponding environment. 24 | 25 | 26 | ```{eval-rst} 27 | .. tabs:: 28 | 29 | .. group-tab:: OSX/Linux 30 | 31 | .. code-block:: text 32 | 33 | $ . .venv/bin/activate 34 | (venv) $ 35 | 36 | .. group-tab:: Windows 37 | 38 | .. code-block:: text 39 | 40 | > .venv\scripts\activate 41 | (venv) > 42 | ``` 43 | 44 | You are now using your virtualenv (notice how the prompt of your shell has changed to show the active environment). 45 | 46 | To install packages in the virtual environment: 47 | 48 | ```console 49 | $ pip install click 50 | ``` 51 | 52 | And if you want to stop using the virtualenv, use the following command: 53 | 54 | ```console 55 | $ deactivate 56 | ``` 57 | 58 | After doing this, the prompt of your shell should be as familiar as before. 59 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 Pallets 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /docs/_static/click-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/asyncclick/_textwrap.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import collections.abc as cabc 4 | import textwrap 5 | from contextlib import contextmanager 6 | 7 | 8 | class TextWrapper(textwrap.TextWrapper): 9 | def _handle_long_word( 10 | self, 11 | reversed_chunks: list[str], 12 | cur_line: list[str], 13 | cur_len: int, 14 | width: int, 15 | ) -> None: 16 | space_left = max(width - cur_len, 1) 17 | 18 | if self.break_long_words: 19 | last = reversed_chunks[-1] 20 | cut = last[:space_left] 21 | res = last[space_left:] 22 | cur_line.append(cut) 23 | reversed_chunks[-1] = res 24 | elif not cur_line: 25 | cur_line.append(reversed_chunks.pop()) 26 | 27 | @contextmanager 28 | def extra_indent(self, indent: str) -> cabc.Iterator[None]: 29 | old_initial_indent = self.initial_indent 30 | old_subsequent_indent = self.subsequent_indent 31 | self.initial_indent += indent 32 | self.subsequent_indent += indent 33 | 34 | try: 35 | yield 36 | finally: 37 | self.initial_indent = old_initial_indent 38 | self.subsequent_indent = old_subsequent_indent 39 | 40 | def indent_only(self, text: str) -> str: 41 | rv = [] 42 | 43 | for idx, line in enumerate(text.splitlines()): 44 | indent = self.initial_indent 45 | 46 | if idx > 0: 47 | indent = self.subsequent_indent 48 | 49 | rv.append(f"{indent}{line}") 50 | 51 | return "\n".join(rv) 52 | -------------------------------------------------------------------------------- /examples/validation/validation.py: -------------------------------------------------------------------------------- 1 | from urllib import parse as urlparse 2 | 3 | import asyncclick as click 4 | 5 | 6 | def validate_count(ctx, param, value): 7 | if value < 0 or value % 2 != 0: 8 | raise click.BadParameter("Should be a positive, even integer.") 9 | return value 10 | 11 | 12 | class URL(click.ParamType): 13 | name = "url" 14 | 15 | def convert(self, value, param, ctx): 16 | if not isinstance(value, tuple): 17 | value = urlparse.urlparse(value) 18 | if value.scheme not in ("http", "https"): 19 | self.fail( 20 | f"invalid URL scheme ({value.scheme}). Only HTTP URLs are allowed", 21 | param, 22 | ctx, 23 | ) 24 | return value 25 | 26 | 27 | @click.command() 28 | @click.option( 29 | "--count", default=2, callback=validate_count, help="A positive even number." 30 | ) 31 | @click.option("--foo", help="A mysterious parameter.") 32 | @click.option("--url", help="A URL", type=URL()) 33 | @click.version_option() 34 | def cli(count, foo, url): 35 | """Validation. 36 | 37 | This example validates parameters in different ways. It does it 38 | through callbacks, through a custom type as well as by validating 39 | manually in the function. 40 | """ 41 | if foo is not None and foo != "wat": 42 | raise click.BadParameter( 43 | 'If a value is provided it needs to be the value "wat".', 44 | param_hint=["--foo"], 45 | ) 46 | click.echo(f"count: {count}") 47 | click.echo(f"foo: {foo}") 48 | click.echo(f"url: {url!r}") 49 | -------------------------------------------------------------------------------- /examples/completion/completion.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import asyncclick as click 4 | from asyncclick.shell_completion import CompletionItem 5 | 6 | 7 | @click.group() 8 | def cli(): 9 | pass 10 | 11 | 12 | @cli.command() 13 | @click.option("--dir", type=click.Path(file_okay=False)) 14 | def ls(dir): 15 | click.echo("\n".join(os.listdir(dir))) 16 | 17 | 18 | def get_env_vars(ctx, param, incomplete): 19 | # Returning a list of values is a shortcut to returning a list of 20 | # CompletionItem(value). 21 | return [k for k in os.environ if incomplete in k] 22 | 23 | 24 | @cli.command(help="A command to print environment variables") 25 | @click.argument("envvar", shell_complete=get_env_vars) 26 | def show_env(envvar): 27 | click.echo(f"Environment variable: {envvar}") 28 | click.echo(f"Value: {os.environ[envvar]}") 29 | 30 | 31 | @cli.group(help="A group that holds a subcommand") 32 | def group(): 33 | pass 34 | 35 | 36 | def list_users(ctx, param, incomplete): 37 | # You can generate completions with help strings by returning a list 38 | # of CompletionItem. You can match on whatever you want, including 39 | # the help. 40 | items = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")] 41 | out = [] 42 | 43 | for value, help in items: 44 | if incomplete in value or incomplete in help: 45 | out.append(CompletionItem(value, help=help)) 46 | 47 | return out 48 | 49 | 50 | @group.command(help="Choose a user") 51 | @click.argument("user", shell_complete=list_users) 52 | def select_user(user): 53 | click.echo(f"Chosen user is {user}") 54 | 55 | 56 | cli.add_command(group) 57 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: ['*'] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 10 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 11 | with: 12 | enable-cache: true 13 | prune-cache: false 14 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 15 | with: 16 | python-version-file: pyproject.toml 17 | - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV 18 | - run: uv build 19 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 20 | with: 21 | path: ./dist 22 | create-release: 23 | needs: [build] 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: write 27 | steps: 28 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 29 | - name: create release 30 | run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} artifact/* 31 | env: 32 | GH_TOKEN: ${{ github.token }} 33 | publish-pypi: 34 | needs: [build] 35 | environment: 36 | name: publish 37 | url: https://pypi.org/project/click/${{ github.ref_name }} 38 | runs-on: ubuntu-latest 39 | permissions: 40 | id-token: write 41 | steps: 42 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 43 | - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 44 | with: 45 | packages-dir: artifact/ 46 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{13,12,11,10,9} 4 | pypy310 5 | style 6 | typing 7 | docs 8 | skip_missing_interpreters = true 9 | 10 | [testenv] 11 | package = wheel 12 | wheel_build_env = .pkg 13 | constrain_package_deps = true 14 | use_frozen_constraints = true 15 | deps = -r requirements/tests.txt 16 | commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} 17 | 18 | [testenv:py37,py3.7] 19 | deps = -r requirements/tests37.txt 20 | 21 | [testenv:style] 22 | deps = pre-commit 23 | skip_install = true 24 | commands = pre-commit run --all-files 25 | 26 | [testenv:typing] 27 | deps = -r requirements/typing.txt 28 | commands = 29 | mypy 30 | pyright tests/typing 31 | pyright --verifytypes asyncclick --ignoreexternal 32 | 33 | [testenv:docs] 34 | deps = -r requirements/docs.txt 35 | commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml 36 | 37 | [testenv:update-actions] 38 | labels = update 39 | deps = gha-update 40 | commands = gha-update 41 | 42 | [testenv:update-pre_commit] 43 | labels = update 44 | deps = pre-commit 45 | skip_install = true 46 | commands = pre-commit autoupdate -j4 47 | 48 | [testenv:update-requirements] 49 | labels = update 50 | deps = pip-tools 51 | skip_install = true 52 | change_dir = requirements 53 | commands = 54 | pip-compile build.in -q {posargs:-U} 55 | pip-compile docs.in -q {posargs:-U} 56 | pip-compile tests.in -q {posargs:-U} 57 | pip-compile typing.in -q {posargs:-U} 58 | pip-compile dev.in -q {posargs:-U} 59 | 60 | [testenv:update-requirements37] 61 | base_python = 3.7 62 | labels = update 63 | deps = pip-tools 64 | skip_install = true 65 | change_dir = requirements 66 | commands = pip-compile tests.in -q -o tests37.txt {posargs:-U} 67 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import sys 4 | 5 | from asyncclick._compat import WIN 6 | 7 | IMPORT_TEST = b"""\ 8 | import builtins 9 | 10 | found_imports = set() 11 | real_import = builtins.__import__ 12 | import sys 13 | 14 | def tracking_import(module, locals=None, globals=None, fromlist=None, 15 | level=0): 16 | rv = real_import(module, locals, globals, fromlist, level) 17 | if globals and '__name__' in globals and \\ 18 | globals['__name__'].startswith('asyncclick') and level == 0: 19 | found_imports.add(module) 20 | return rv 21 | builtins.__import__ = tracking_import 22 | 23 | import asyncclick 24 | rv = list(found_imports) 25 | import json 26 | asyncclick.echo(json.dumps(rv)) 27 | """ 28 | 29 | ALLOWED_IMPORTS = { 30 | "__future__", 31 | "anyio", 32 | "codecs", 33 | "collections", 34 | "collections.abc", 35 | "configparser", 36 | "contextlib", 37 | "datetime", 38 | "enum", 39 | "errno", 40 | "fcntl", 41 | "functools", 42 | "gettext", 43 | "inspect", 44 | "io", 45 | "itertools", 46 | "os", 47 | "re", 48 | "stat", 49 | "struct", 50 | "sys", 51 | "threading", 52 | "types", 53 | "typing", 54 | "weakref", 55 | } 56 | 57 | if WIN: 58 | ALLOWED_IMPORTS.update(["ctypes", "ctypes.wintypes", "msvcrt", "time"]) 59 | 60 | 61 | def test_light_imports(): 62 | c = subprocess.Popen( 63 | [sys.executable, "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE 64 | ) 65 | rv = c.communicate(IMPORT_TEST)[0] 66 | rv = rv.decode("utf-8") 67 | imported = json.loads(rv) 68 | 69 | for module in imported: 70 | if module == "asyncclick" or module.startswith("asyncclick."): 71 | continue 72 | assert module in ALLOWED_IMPORTS 73 | -------------------------------------------------------------------------------- /examples/complex/complex/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import asyncclick as click 5 | 6 | 7 | CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX") 8 | 9 | 10 | class Environment: 11 | def __init__(self): 12 | self.verbose = False 13 | self.home = os.getcwd() 14 | 15 | def log(self, msg, *args): 16 | """Logs a message to stderr.""" 17 | if args: 18 | msg %= args 19 | click.echo(msg, file=sys.stderr) 20 | 21 | def vlog(self, msg, *args): 22 | """Logs a message to stderr only if verbose is enabled.""" 23 | if self.verbose: 24 | self.log(msg, *args) 25 | 26 | 27 | pass_environment = click.make_pass_decorator(Environment, ensure=True) 28 | cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "commands")) 29 | 30 | 31 | class ComplexCLI(click.Group): 32 | def list_commands(self, ctx): 33 | rv = [] 34 | for filename in os.listdir(cmd_folder): 35 | if filename.endswith(".py") and filename.startswith("cmd_"): 36 | rv.append(filename[4:-3]) 37 | rv.sort() 38 | return rv 39 | 40 | def get_command(self, ctx, name): 41 | try: 42 | mod = __import__(f"complex.commands.cmd_{name}", None, None, ["cli"]) 43 | except ImportError: 44 | return 45 | return mod.cli 46 | 47 | 48 | @click.command(cls=ComplexCLI, context_settings=CONTEXT_SETTINGS) 49 | @click.option( 50 | "--home", 51 | type=click.Path(exists=True, file_okay=False, resolve_path=True), 52 | help="Changes the folder to operate on.", 53 | ) 54 | @click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode.") 55 | @pass_environment 56 | def cli(ctx, verbose, home): 57 | """A complex command line interface.""" 58 | ctx.verbose = verbose 59 | if home is not None: 60 | ctx.home = home 61 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/docs.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | alabaster==1.0.0 13 | # via sphinx 14 | babel==2.16.0 15 | # via sphinx 16 | certifi==2024.8.30 17 | # via requests 18 | charset-normalizer==3.4.0 19 | # via requests 20 | docutils==0.21.2 21 | # via 22 | # sphinx 23 | # sphinx-tabs 24 | idna==3.10 25 | # via requests 26 | imagesize==1.4.1 27 | # via sphinx 28 | jinja2==3.1.4 29 | # via sphinx 30 | markupsafe==3.0.2 31 | # via jinja2 32 | packaging==24.2 33 | # via 34 | # pallets-sphinx-themes 35 | # sphinx 36 | pallets-sphinx-themes==2.3.0 37 | # via -r requirements/docs.in 38 | pygments==2.18.0 39 | # via 40 | # sphinx 41 | # sphinx-tabs 42 | requests==2.32.3 43 | # via sphinx 44 | snowballstemmer==2.2.0 45 | # via sphinx 46 | sphinx==8.1.3 47 | # via 48 | # -r requirements/docs.in 49 | # pallets-sphinx-themes 50 | # sphinx-issues 51 | # sphinx-notfound-page 52 | # sphinx-tabs 53 | # sphinxcontrib-log-cabinet 54 | sphinx-issues==5.0.0 55 | # via -r requirements/docs.in 56 | sphinx-notfound-page==1.0.4 57 | # via pallets-sphinx-themes 58 | sphinx-tabs==3.4.7 59 | # via -r requirements/docs.in 60 | sphinxcontrib-applehelp==2.0.0 61 | # via sphinx 62 | sphinxcontrib-devhelp==2.0.0 63 | # via sphinx 64 | sphinxcontrib-htmlhelp==2.1.0 65 | # via sphinx 66 | sphinxcontrib-jsmath==1.0.1 67 | # via sphinx 68 | sphinxcontrib-log-cabinet==1.0.1 69 | # via -r requirements/docs.in 70 | sphinxcontrib-qthelp==2.0.0 71 | # via sphinx 72 | sphinxcontrib-serializinghtml==2.0.0 73 | # via sphinx 74 | urllib3==2.2.3 75 | # via requests 76 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | paths-ignore: ['docs/**', 'README.md'] 5 | push: 6 | branches: [main, stable] 7 | paths-ignore: ['docs/**', 'README.md'] 8 | jobs: 9 | tests: 10 | name: ${{ matrix.name || matrix.python }} 11 | runs-on: ${{ matrix.os || 'ubuntu-latest' }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - {python: '3.13'} 17 | - {name: Windows, python: '3.13', os: windows-latest} 18 | - {name: Mac, python: '3.13', os: macos-latest} 19 | - {python: '3.12'} 20 | - {python: '3.11'} 21 | - {python: '3.10'} 22 | - {name: PyPy, python: 'pypy-3.11', tox: pypy3.11} 23 | steps: 24 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 26 | with: 27 | enable-cache: true 28 | prune-cache: false 29 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 30 | with: 31 | python-version: ${{ matrix.python }} 32 | - run: uv run --locked tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} 33 | typing: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 37 | - uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 38 | with: 39 | enable-cache: true 40 | prune-cache: false 41 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 42 | with: 43 | python-version-file: pyproject.toml 44 | - name: cache mypy 45 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 46 | with: 47 | path: ./.mypy_cache 48 | key: mypy|${{ hashFiles('pyproject.toml') }} 49 | - run: uv run --locked tox run -e typing 50 | -------------------------------------------------------------------------------- /examples/naval/naval.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | 4 | @click.group() 5 | @click.version_option() 6 | def cli(): 7 | """Naval Fate. 8 | 9 | This is the docopt example adopted to Click but with some actual 10 | commands implemented and not just the empty parsing which really 11 | is not all that interesting. 12 | """ 13 | 14 | 15 | @cli.group() 16 | def ship(): 17 | """Manages ships.""" 18 | 19 | 20 | @ship.command("new") 21 | @click.argument("name") 22 | def ship_new(name): 23 | """Creates a new ship.""" 24 | click.echo(f"Created ship {name}") 25 | 26 | 27 | @ship.command("move") 28 | @click.argument("ship") 29 | @click.argument("x", type=float) 30 | @click.argument("y", type=float) 31 | @click.option("--speed", metavar="KN", default=10, help="Speed in knots.") 32 | def ship_move(ship, x, y, speed): 33 | """Moves SHIP to the new location X,Y.""" 34 | click.echo(f"Moving ship {ship} to {x},{y} with speed {speed}") 35 | 36 | 37 | @ship.command("shoot") 38 | @click.argument("ship") 39 | @click.argument("x", type=float) 40 | @click.argument("y", type=float) 41 | def ship_shoot(ship, x, y): 42 | """Makes SHIP fire to X,Y.""" 43 | click.echo(f"Ship {ship} fires to {x},{y}") 44 | 45 | 46 | @cli.group("mine") 47 | def mine(): 48 | """Manages mines.""" 49 | 50 | 51 | @mine.command("set") 52 | @click.argument("x", type=float) 53 | @click.argument("y", type=float) 54 | @click.option( 55 | "ty", 56 | "--moored", 57 | flag_value="moored", 58 | default=True, 59 | help="Moored (anchored) mine. Default.", 60 | ) 61 | @click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.") 62 | def mine_set(x, y, ty): 63 | """Sets a mine at a specific coordinate.""" 64 | click.echo(f"Set {ty} mine at {x},{y}") 65 | 66 | 67 | @mine.command("remove") 68 | @click.argument("x", type=float) 69 | @click.argument("y", type=float) 70 | def mine_remove(x, y): 71 | """Removes a mine at a specific coordinate.""" 72 | click.echo(f"Removed mine at {x},{y}") 73 | -------------------------------------------------------------------------------- /tests/test_normalization.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower()) 4 | 5 | 6 | def test_option_normalization(runner): 7 | @click.command(context_settings=CONTEXT_SETTINGS) 8 | @click.option("--foo") 9 | @click.option("-x") 10 | def cli(foo, x): 11 | click.echo(foo) 12 | click.echo(x) 13 | 14 | result = runner.invoke(cli, ["--FOO", "42", "-X", 23]) 15 | assert result.output == "42\n23\n" 16 | 17 | 18 | def test_choice_normalization(runner): 19 | @click.command(context_settings=CONTEXT_SETTINGS) 20 | @click.option( 21 | "--method", 22 | type=click.Choice( 23 | ["SCREAMING_SNAKE_CASE", "snake_case", "PascalCase", "kebab-case"], 24 | case_sensitive=False, 25 | ), 26 | ) 27 | def cli(method): 28 | click.echo(method) 29 | 30 | result = runner.invoke(cli, ["--METHOD=snake_case"]) 31 | assert not result.exception, result.output 32 | assert result.output == "snake_case\n" 33 | 34 | # Even though it's case sensitive, the choice's original value is preserved 35 | result = runner.invoke(cli, ["--method=pascalcase"]) 36 | assert not result.exception, result.output 37 | assert result.output == "PascalCase\n" 38 | 39 | result = runner.invoke(cli, ["--method=meh"]) 40 | assert result.exit_code == 2 41 | assert ( 42 | "Invalid value for '--method': 'meh' is not one of " 43 | "'screaming_snake_case', 'snake_case', 'pascalcase', 'kebab-case'." 44 | ) in result.output 45 | 46 | result = runner.invoke(cli, ["--help"]) 47 | assert ( 48 | "--method [screaming_snake_case|snake_case|pascalcase|kebab-case]" 49 | in result.output 50 | ) 51 | 52 | 53 | def test_command_normalization(runner): 54 | @click.group(context_settings=CONTEXT_SETTINGS) 55 | def cli(): 56 | pass 57 | 58 | @cli.command() 59 | def foo(): 60 | click.echo("here!") 61 | 62 | result = runner.invoke(cli, ["FOO"]) 63 | assert result.output == "here!\n" 64 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | from pallets_sphinx_themes import get_version 2 | from pallets_sphinx_themes import ProjectLink 3 | 4 | # Project -------------------------------------------------------------- 5 | 6 | project = "Click" 7 | copyright = "2014 Pallets" 8 | author = "Pallets" 9 | release, version = get_version("Click") 10 | 11 | # General -------------------------------------------------------------- 12 | 13 | master_doc = "index" 14 | default_role = "code" 15 | extensions = [ 16 | "sphinx.ext.autodoc", 17 | "sphinx.ext.extlinks", 18 | "sphinx.ext.intersphinx", 19 | "sphinx_tabs.tabs", 20 | "sphinxcontrib.log_cabinet", 21 | "pallets_sphinx_themes", 22 | "myst_parser", 23 | ] 24 | autodoc_member_order = "bysource" 25 | autodoc_typehints = "description" 26 | autodoc_preserve_defaults = True 27 | extlinks = { 28 | "issue": ("https://github.com/pallets/click/issues/%s", "#%s"), 29 | "pr": ("https://github.com/pallets/click/pull/%s", "#%s"), 30 | } 31 | intersphinx_mapping = { 32 | "python": ("https://docs.python.org/3/", None), 33 | } 34 | 35 | # HTML ----------------------------------------------------------------- 36 | 37 | html_theme = "click" 38 | html_theme_options = {"index_sidebar_logo": False} 39 | html_context = { 40 | "project_links": [ 41 | ProjectLink("Donate", "https://palletsprojects.com/donate"), 42 | ProjectLink("PyPI Releases", "https://pypi.org/project/click/"), 43 | ProjectLink("Source Code", "https://github.com/pallets/click/"), 44 | ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"), 45 | ProjectLink("Chat", "https://discord.gg/pallets"), 46 | ] 47 | } 48 | html_sidebars = { 49 | "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], 50 | "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], 51 | } 52 | singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} 53 | html_static_path = ["_static"] 54 | html_favicon = "_static/click-icon.svg" 55 | html_logo = "_static/click-logo.svg" 56 | html_title = f"Click Documentation ({version})" 57 | html_show_sourcelink = False 58 | -------------------------------------------------------------------------------- /src/asyncclick/globals.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing as t 4 | from threading import local 5 | 6 | if t.TYPE_CHECKING: 7 | from .core import Context 8 | 9 | _local = local() 10 | 11 | 12 | @t.overload 13 | def get_current_context(silent: t.Literal[False] = False) -> Context: ... 14 | 15 | 16 | @t.overload 17 | def get_current_context(silent: bool = ...) -> Context | None: ... 18 | 19 | 20 | def get_current_context(silent: bool = False) -> Context | None: 21 | """Returns the current click context. This can be used as a way to 22 | access the current context object from anywhere. This is a more implicit 23 | alternative to the :func:`pass_context` decorator. This function is 24 | primarily useful for helpers such as :func:`echo` which might be 25 | interested in changing its behavior based on the current context. 26 | 27 | To push the current context, :meth:`Context.scope` can be used. 28 | 29 | .. versionadded:: 5.0 30 | 31 | :param silent: if set to `True` the return value is `None` if no context 32 | is available. The default behavior is to raise a 33 | :exc:`RuntimeError`. 34 | """ 35 | try: 36 | return t.cast("Context", _local.stack[-1]) 37 | except (AttributeError, IndexError) as e: 38 | if not silent: 39 | raise RuntimeError("There is no active click context.") from e 40 | 41 | return None 42 | 43 | 44 | def push_context(ctx: Context) -> None: 45 | """Pushes a new context to the current stack.""" 46 | _local.__dict__.setdefault("stack", []).append(ctx) 47 | 48 | 49 | def pop_context() -> None: 50 | """Removes the top level from the stack.""" 51 | _local.stack.pop() 52 | 53 | 54 | def resolve_color_default(color: bool | None = None) -> bool | None: 55 | """Internal helper to get the default value of the color flag. If a 56 | value is passed it's returned unchanged, otherwise it's looked up from 57 | the current context. 58 | """ 59 | if color is not None: 60 | return color 61 | 62 | ctx = get_current_context(silent=True) 63 | 64 | if ctx is not None: 65 | return ctx.color 66 | 67 | return None 68 | -------------------------------------------------------------------------------- /docs/parameters.md: -------------------------------------------------------------------------------- 1 | (parameters)= 2 | 3 | # Parameters 4 | 5 | ```{currentmodule} click 6 | ``` 7 | 8 | Click supports only two principle types of parameters for scripts (by design): options and arguments. 9 | 10 | ## Options 11 | 12 | - Are optional. 13 | - Recommended to use for everything except subcommands, urls, or files. 14 | - Can take a fixed number of arguments. The default is 1. They may be specified multiple times using {ref}`multiple-options`. 15 | - Are fully documented by the help page. 16 | - Have automatic prompting for missing input. 17 | - Can act as flags (boolean or otherwise). 18 | - Can be pulled from environment variables. 19 | 20 | ## Arguments 21 | 22 | - Are optional with in reason, but not entirely so. 23 | - Recommended to use for subcommands, urls, or files. 24 | - Can take an arbitrary number of arguments. 25 | - Are not fully documented by the help page since they may be too specific to be automatically documented. For more see {ref}`documenting-arguments`. 26 | - Can be pulled from environment variables but only explicitly named ones. For more see {ref}`environment-variables`. 27 | 28 | On each principle type you can specify {ref}`parameter-types`. Specifying these types helps Click add details to your help pages and help with the handling of those types. 29 | 30 | (parameter-names)= 31 | 32 | ## Parameter Names 33 | 34 | Parameters (options and arguments) have a name that will be used as 35 | the Python argument name when calling the decorated function with 36 | values. 37 | 38 | In the example, the argument's name is `filename`. The name must match the python arg name. To provide a different name for use in help text, see {ref}`doc-meta-variables`. 39 | The option's names are `-t` and `--times`. More names are available for options and are covered in {ref}`options`. 40 | 41 | ```{eval-rst} 42 | .. click:example:: 43 | 44 | @click.command() 45 | @click.argument('filename') 46 | @click.option('-t', '--times', type=int) 47 | def multi_echo(filename, times): 48 | """Print value filename multiple times.""" 49 | for x in range(times): 50 | click.echo(filename) 51 | 52 | .. click:run:: 53 | 54 | invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo') 55 | ``` 56 | -------------------------------------------------------------------------------- /tests/test_command_decorators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import asyncclick as click 4 | 5 | 6 | def test_command_no_parens(runner): 7 | @click.command 8 | def cli(): 9 | click.echo("hello") 10 | 11 | result = runner.invoke(cli) 12 | assert result.exception is None 13 | assert result.output == "hello\n" 14 | 15 | 16 | def test_custom_command_no_parens(runner): 17 | class CustomCommand(click.Command): 18 | pass 19 | 20 | class CustomGroup(click.Group): 21 | command_class = CustomCommand 22 | 23 | @click.group(cls=CustomGroup) 24 | def grp(): 25 | pass 26 | 27 | @grp.command 28 | def cli(): 29 | click.echo("hello custom command class") 30 | 31 | result = runner.invoke(cli) 32 | assert result.exception is None 33 | assert result.output == "hello custom command class\n" 34 | 35 | 36 | def test_group_no_parens(runner): 37 | @click.group 38 | def grp(): 39 | click.echo("grp1") 40 | 41 | @grp.command 42 | def cmd1(): 43 | click.echo("cmd1") 44 | 45 | @grp.group 46 | def grp2(): 47 | click.echo("grp2") 48 | 49 | @grp2.command 50 | def cmd2(): 51 | click.echo("cmd2") 52 | 53 | result = runner.invoke(grp, ["cmd1"]) 54 | assert result.exception is None 55 | assert result.output == "grp1\ncmd1\n" 56 | 57 | result = runner.invoke(grp, ["grp2", "cmd2"]) 58 | assert result.exception is None 59 | assert result.output == "grp1\ngrp2\ncmd2\n" 60 | 61 | 62 | def test_params_argument(runner): 63 | opt = click.Argument(["a"]) 64 | 65 | @click.command(params=[opt]) 66 | @click.argument("b") 67 | def cli(a, b): 68 | click.echo(f"{a} {b}") 69 | 70 | assert cli.params[0].name == "a" 71 | assert cli.params[1].name == "b" 72 | result = runner.invoke(cli, ["1", "2"]) 73 | assert result.output == "1 2\n" 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "name", 78 | [ 79 | "init_data", 80 | "init_data_command", 81 | "init_data_cmd", 82 | "init_data_group", 83 | "init_data_grp", 84 | ], 85 | ) 86 | def test_generate_name(name: str) -> None: 87 | def f(): 88 | pass 89 | 90 | f.__name__ = name 91 | f = click.command(f) 92 | assert f.name == "init-data" 93 | -------------------------------------------------------------------------------- /docs/support-multiple-versions.md: -------------------------------------------------------------------------------- 1 | # Supporting Multiple Versions 2 | 3 | If you are a library maintainer, you may want to support multiple versions of 4 | Click. See the Pallets [version policy] for information about our version 5 | numbers and support policy. 6 | 7 | [version policy]: https://palletsprojects.com/versions 8 | 9 | Most features of Click are stable across releases, and don't require special 10 | handling. However, feature releases may deprecate and change APIs. Occasionally, 11 | a change will require special handling. 12 | 13 | ## Use Feature Detection 14 | 15 | Prefer using feature detection. Looking at the version can be tempting, but is 16 | often more brittle or results in more complicated code. Try to use `if` or `try` 17 | blocks to decide whether to use a new or old pattern. 18 | 19 | If you do need to look at the version, use {func}`importlib.metadata.version`, 20 | the standardized way to get versions for any installed Python package. 21 | 22 | ## Changes in 8.2 23 | 24 | ### `ParamType` methods require `ctx` 25 | 26 | In 8.2, several methods of `ParamType` now have a `ctx: click.Context` 27 | argument. Because this changes the signature of the methods from 8.1, it's not 28 | obvious how to support both when subclassing or calling. 29 | 30 | This example uses `ParamType.get_metavar`, and the same technique should be 31 | applicable to other methods such as `get_missing_message`. 32 | 33 | Update your methods overrides to take the new `ctx` argument. Use the 34 | following decorator to wrap each method. In 8.1, it will get the context where 35 | possible and pass it using the 8.2 signature. 36 | 37 | ```python 38 | import functools 39 | import typing as t 40 | import click 41 | 42 | F = t.TypeVar("F", bound=t.Callable[..., t.Any]) 43 | 44 | def add_ctx_arg(f: F) -> F: 45 | @functools.wraps(f) 46 | def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: 47 | if "ctx" not in kwargs: 48 | kwargs["ctx"] = click.get_current_context(silent=True) 49 | 50 | return f(*args, **kwargs) 51 | 52 | return wrapper # type: ignore[return-value] 53 | ``` 54 | 55 | Here's an example ``ParamType`` subclass which uses this: 56 | 57 | ```python 58 | class CommaDelimitedString(click.ParamType): 59 | @add_ctx_arg 60 | def get_metavar(self, param: click.Parameter, ctx: click.Context | None) -> str: 61 | return "TEXT,TEXT,..." 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/option-decorators.rst: -------------------------------------------------------------------------------- 1 | Options Shortcut Decorators 2 | =========================== 3 | 4 | .. currentmodule:: click 5 | 6 | For convenience commonly used combinations of options arguments are available as their own decorators. 7 | 8 | .. contents:: 9 | :depth: 2 10 | :local: 11 | 12 | Password Option 13 | ------------------ 14 | 15 | Click supports hidden prompts and asking for confirmation. This is 16 | useful for password input: 17 | 18 | .. click:example:: 19 | 20 | import codecs 21 | 22 | @click.command() 23 | @click.option( 24 | "--password", prompt=True, hide_input=True, 25 | confirmation_prompt=True 26 | ) 27 | def encode(password): 28 | click.echo(f"encoded: {codecs.encode(password, 'rot13')}") 29 | 30 | .. click:run:: 31 | 32 | invoke(encode, input=['secret', 'secret']) 33 | 34 | Because this combination of parameters is quite common, this can also be 35 | replaced with the :func:`password_option` decorator: 36 | 37 | .. code-block:: python 38 | 39 | @click.command() 40 | @click.password_option() 41 | def encrypt(password): 42 | click.echo(f"encoded: to {codecs.encode(password, 'rot13')}") 43 | 44 | Confirmation Option 45 | -------------------- 46 | 47 | For dangerous operations, it's very useful to be able to ask a user for 48 | confirmation. This can be done by adding a boolean ``--yes`` flag and 49 | asking for confirmation if the user did not provide it and to fail in a 50 | callback: 51 | 52 | .. click:example:: 53 | 54 | def abort_if_false(ctx, param, value): 55 | if not value: 56 | ctx.abort() 57 | 58 | @click.command() 59 | @click.option('--yes', is_flag=True, callback=abort_if_false, 60 | expose_value=False, 61 | prompt='Are you sure you want to drop the db?') 62 | def dropdb(): 63 | click.echo('Dropped all tables!') 64 | 65 | And what it looks like on the command line: 66 | 67 | .. click:run:: 68 | 69 | invoke(dropdb, input=['n']) 70 | invoke(dropdb, args=['--yes']) 71 | 72 | Because this combination of parameters is quite common, this can also be 73 | replaced with the :func:`confirmation_option` decorator: 74 | 75 | .. click:example:: 76 | 77 | @click.command() 78 | @click.confirmation_option(prompt='Are you sure you want to drop the db?') 79 | def dropdb(): 80 | click.echo('Dropped all tables!') 81 | 82 | Version Option 83 | ---------------- 84 | :func:`version_option` adds a ``--version`` option which immediately prints the version number and exits the program. 85 | -------------------------------------------------------------------------------- /docs/entry-points.rst: -------------------------------------------------------------------------------- 1 | Packaging Entry Points 2 | ====================== 3 | 4 | It's recommended to write command line utilities as installable packages with 5 | entry points instead of telling users to run ``python hello.py``. 6 | 7 | A distribution package is a ``.whl`` file you install with pip or another Python 8 | installer. You use a ``pyproject.toml`` file to describe the project and how it 9 | is built into a package. You might upload this package to PyPI, or distribute it 10 | to your users in another way. 11 | 12 | Python installers create executable scripts that will run a specified Python 13 | function. These are known as "entry points". The installer knows how to create 14 | an executable regardless of the operating system, so it will work on Linux, 15 | Windows, MacOS, etc. 16 | 17 | 18 | Project Files 19 | ------------- 20 | 21 | To install your app with an entry point, all you need is the script and a 22 | ``pyproject.toml`` file. Here's an example project directory: 23 | 24 | .. code-block:: text 25 | 26 | hello-project/ 27 | src/ 28 | hello/ 29 | __init__.py 30 | hello.py 31 | pyproject.toml 32 | 33 | Contents of ``hello.py``: 34 | 35 | .. click:example:: 36 | 37 | import click 38 | 39 | @click.command() 40 | def cli(): 41 | """Prints a greeting.""" 42 | click.echo("Hello, World!") 43 | 44 | Contents of ``pyproject.toml``: 45 | 46 | .. code-block:: toml 47 | 48 | [project] 49 | name = "hello" 50 | version = "1.0.0" 51 | description = "Hello CLI" 52 | requires-python = ">=3.11" 53 | dependencies = [ 54 | "click>=8.1", 55 | ] 56 | 57 | [project.scripts] 58 | hello = "hello.hello:cli" 59 | 60 | [build-system] 61 | requires = ["flit_core<4"] 62 | build-backend = "flit_core.buildapi" 63 | 64 | The magic is in the ``project.scripts`` section. Each line identifies one executable 65 | script. The first part before the equals sign (``=``) is the name of the script that 66 | should be generated, the second part is the import path followed by a colon 67 | (``:``) with the function to call (the Click command). 68 | 69 | 70 | Installation 71 | ------------ 72 | 73 | When your package is installed, the installer will create an executable script 74 | based on the configuration. During development, you can install in editable 75 | mode using the ``-e`` option. Remember to use a virtual environment! 76 | 77 | .. code-block:: console 78 | 79 | $ python -m venv .venv 80 | $ . .venv/bin/activate 81 | $ pip install -e . 82 | 83 | Afterwards, your command should be available: 84 | 85 | .. click:run:: 86 | 87 | invoke(cli, prog_name="hello") 88 | -------------------------------------------------------------------------------- /docs/_static/click-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/test_defaults.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | 4 | def test_basic_defaults(runner): 5 | @click.command() 6 | @click.option("--foo", default=42, type=click.FLOAT) 7 | def cli(foo): 8 | assert isinstance(foo, float) 9 | click.echo(f"FOO:[{foo}]") 10 | 11 | result = runner.invoke(cli, []) 12 | assert not result.exception 13 | assert "FOO:[42.0]" in result.output 14 | 15 | 16 | def test_multiple_defaults(runner): 17 | @click.command() 18 | @click.option("--foo", default=[23, 42], type=click.FLOAT, multiple=True) 19 | def cli(foo): 20 | for item in foo: 21 | assert isinstance(item, float) 22 | click.echo(item) 23 | 24 | result = runner.invoke(cli, []) 25 | assert not result.exception 26 | assert result.output.splitlines() == ["23.0", "42.0"] 27 | 28 | 29 | def test_nargs_plus_multiple(runner): 30 | @click.command() 31 | @click.option( 32 | "--arg", default=((1, 2), (3, 4)), nargs=2, multiple=True, type=click.INT 33 | ) 34 | def cli(arg): 35 | for a, b in arg: 36 | click.echo(f"<{a:d}|{b:d}>") 37 | 38 | result = runner.invoke(cli, []) 39 | assert not result.exception 40 | assert result.output.splitlines() == ["<1|2>", "<3|4>"] 41 | 42 | 43 | def test_multiple_flag_default(runner): 44 | """Default default for flags when multiple=True should be empty tuple.""" 45 | 46 | @click.command 47 | # flag due to secondary token 48 | @click.option("-y/-n", multiple=True) 49 | # flag due to is_flag 50 | @click.option("-f", is_flag=True, multiple=True) 51 | # flag due to flag_value 52 | @click.option("-v", "v", flag_value=1, multiple=True) 53 | @click.option("-q", "v", flag_value=-1, multiple=True) 54 | def cli(y, f, v): 55 | return y, f, v 56 | 57 | result = runner.invoke(cli, standalone_mode=False) 58 | assert result.return_value == ((), (), ()) 59 | 60 | result = runner.invoke(cli, ["-y", "-n", "-f", "-v", "-q"], standalone_mode=False) 61 | assert result.return_value == ((True, False), (True,), (1, -1)) 62 | 63 | 64 | def test_flag_default_map(runner): 65 | """test flag with default map""" 66 | 67 | @click.group() 68 | def cli(): 69 | pass 70 | 71 | @cli.command() 72 | @click.option("--name/--no-name", is_flag=True, show_default=True, help="name flag") 73 | def foo(name): 74 | click.echo(name) 75 | 76 | result = runner.invoke(cli, ["foo"]) 77 | assert "False" in result.output 78 | 79 | result = runner.invoke(cli, ["foo", "--help"]) 80 | assert "default: no-name" in result.output 81 | 82 | result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": True}}) 83 | assert "True" in result.output 84 | 85 | result = runner.invoke(cli, ["foo", "--help"], default_map={"foo": {"name": True}}) 86 | assert "default: name" in result.output 87 | -------------------------------------------------------------------------------- /docs/click-concepts.rst: -------------------------------------------------------------------------------- 1 | Click Concepts 2 | ================ 3 | 4 | This section covers concepts about Click's design. 5 | 6 | .. contents:: 7 | :depth: 1 8 | :local: 9 | 10 | .. _callback-evaluation-order: 11 | 12 | Callback Evaluation Order 13 | ------------------------- 14 | 15 | Click works a bit differently than some other command line parsers in that 16 | it attempts to reconcile the order of arguments as defined by the 17 | programmer with the order of arguments as defined by the user before 18 | invoking any callbacks. 19 | 20 | This is an important concept to understand when porting complex 21 | patterns to Click from optparse or other systems. A parameter 22 | callback invocation in optparse happens as part of the parsing step, 23 | whereas a callback invocation in Click happens after the parsing. 24 | 25 | The main difference is that in optparse, callbacks are invoked with the raw 26 | value as it happens, whereas a callback in Click is invoked after the 27 | value has been fully converted. 28 | 29 | Generally, the order of invocation is driven by the order in which the user 30 | provides the arguments to the script; if there is an option called ``--foo`` 31 | and an option called ``--bar`` and the user calls it as ``--bar 32 | --foo``, then the callback for ``bar`` will fire before the one for ``foo``. 33 | 34 | There are three exceptions to this rule which are important to know: 35 | 36 | Eagerness: 37 | An option can be set to be "eager". All eager parameters are 38 | evaluated before all non-eager parameters, but again in the order as 39 | they were provided on the command line by the user. 40 | 41 | This is important for parameters that execute and exit like ``--help`` 42 | and ``--version``. Both are eager parameters, but whatever parameter 43 | comes first on the command line will win and exit the program. 44 | 45 | Repeated parameters: 46 | If an option or argument is split up on the command line into multiple 47 | places because it is repeated -- for instance, ``--exclude foo --include 48 | baz --exclude bar`` -- the callback will fire based on the position of 49 | the first option. In this case, the callback will fire for 50 | ``exclude`` and it will be passed both options (``foo`` and 51 | ``bar``), then the callback for ``include`` will fire with ``baz`` 52 | only. 53 | 54 | Note that even if a parameter does not allow multiple versions, Click 55 | will still accept the position of the first, but it will ignore every 56 | value except the last. The reason for this is to allow composability 57 | through shell aliases that set defaults. 58 | 59 | Missing parameters: 60 | If a parameter is not defined on the command line, the callback will 61 | still fire. This is different from how it works in optparse where 62 | undefined values do not fire the callback. Missing parameters fire 63 | their callbacks at the very end which makes it possible for them to 64 | default to values from a parameter that came before. 65 | 66 | Most of the time you do not need to be concerned about any of this, 67 | but it is important to know how it works for some advanced cases. 68 | -------------------------------------------------------------------------------- /docs/wincmd.md: -------------------------------------------------------------------------------- 1 | # Windows Console Notes 2 | 3 | ```{versionadded} 6.0 4 | ``` 5 | 6 | Click emulates output streams on Windows to support unicode to the Windows console through separate APIs and we perform 7 | different decoding of parameters. 8 | 9 | Here is a brief overview of how this works and what it means to you. 10 | 11 | ## Unicode Arguments 12 | 13 | Click internally is generally based on the concept that any argument can come in as either byte string or unicode string 14 | and conversion is performed to the type expected value as late as possible. This has some advantages as it allows us to 15 | accept the data in the most appropriate form for the operating system and Python version. 16 | 17 | This caused some problems on Windows where initially the wrong encoding was used and garbage ended up in your input 18 | data. We not only fixed the encoding part, but we also now extract unicode parameters from `sys.argv`. 19 | 20 | There is also another limitation with this: if `sys.argv` was modified prior to invoking a click handler, we have to 21 | fall back to the regular byte input in which case not all unicode values are available but only a subset of the codepage 22 | used for parameters. 23 | 24 | ## Unicode Output and Input 25 | 26 | Unicode output and input on Windows is implemented through the concept of a dispatching text stream. What this means is 27 | that when click first needs a text output (or input) stream on windows it goes through a few checks to figure out of a 28 | windows console is connected or not. If no Windows console is present then the text output stream is returned as such 29 | and the encoding for that stream is set to `utf-8` like on all platforms. 30 | 31 | However if a console is connected the stream will instead be emulated and use the cmd.exe unicode APIs to output text 32 | information. In this case the stream will also use `utf-16-le` as internal encoding. However there is some hackery going 33 | on that the underlying raw IO buffer is still bypassing the unicode APIs and byte output through an indirection is still 34 | possible. 35 | 36 | - This unicode support is limited to `click.echo`, `click.prompt` as well as `click.get_text_stream`. 37 | - Depending on if unicode values or byte strings are passed the control flow goes completely different places internally 38 | which can have some odd artifacts if data partially ends up being buffered. Click attempts to protect against that by 39 | manually always flushing but if you are mixing and matching different string types to `stdout` or `stderr` you will 40 | need to manually flush. 41 | - The raw output stream is set to binary mode, which is a global operation on Windows, so `print` calls will be 42 | affected. Prefer `click.echo` over `print`. 43 | - On Windows 7 and below, there is a limitation where at most 64k characters can be written in one call in binary mode. 44 | In this situation, `sys.stdout` and `sys.stderr` are replaced with wrappers that work around the limitation. 45 | 46 | Another important thing to note is that the Windows console's default fonts do not support a lot of characters which 47 | means that you are mostly limited to international letters but no emojis or special characters. 48 | -------------------------------------------------------------------------------- /docs/exceptions.rst: -------------------------------------------------------------------------------- 1 | Exception Handling 2 | ================== 3 | 4 | .. currentmodule:: click 5 | 6 | Click internally uses exceptions to signal various error conditions that 7 | the user of the application might have caused. Primarily this is things 8 | like incorrect usage. 9 | 10 | Where are Errors Handled? 11 | ------------------------- 12 | 13 | Click's main error handling is happening in :meth:`Command.main`. In 14 | there it handles all subclasses of :exc:`ClickException` as well as the 15 | standard :exc:`EOFError` and :exc:`KeyboardInterrupt` exceptions. The 16 | latter are internally translated into an :exc:`Abort`. 17 | 18 | The logic applied is the following: 19 | 20 | 1. If an :exc:`EOFError` or :exc:`KeyboardInterrupt` happens, reraise it 21 | as :exc:`Abort`. 22 | 2. If a :exc:`ClickException` is raised, invoke the 23 | :meth:`ClickException.show` method on it to display it and then exit 24 | the program with :attr:`ClickException.exit_code`. 25 | 3. If an :exc:`Abort` exception is raised print the string ``Aborted!`` 26 | to standard error and exit the program with exit code ``1``. 27 | 4. If it goes through well, exit the program with exit code ``0``. 28 | 29 | What if I don't want that? 30 | -------------------------- 31 | 32 | Generally you always have the option to invoke the :meth:`invoke` method 33 | yourself. For instance if you have a :class:`Command` you can invoke it 34 | manually like this:: 35 | 36 | ctx = await command.make_context('command-name', ['args', 'go', 'here']) 37 | with ctx: 38 | result = await command.invoke(ctx) 39 | 40 | In this case exceptions will not be handled at all and bubbled up as you 41 | would expect. 42 | 43 | Starting with Click 3.0 you can also use the :meth:`Command.main` method 44 | but disable the standalone mode which will do two things: disable 45 | exception handling and disable the implicit :meth:`sys.exit` at the end. 46 | 47 | So you can do something like this:: 48 | 49 | await command.main(['command-name', 'args', 'go', 'here'], 50 | standalone_mode=False) 51 | 52 | Which Exceptions Exist? 53 | ----------------------- 54 | 55 | Click has two exception bases: :exc:`ClickException` which is raised for 56 | all exceptions that Click wants to signal to the user and :exc:`Abort` 57 | which is used to instruct Click to abort the execution. 58 | 59 | A :exc:`ClickException` has a :meth:`~ClickException.show` method which 60 | can render an error message to stderr or the given file object. If you 61 | want to use the exception yourself for doing something check the API docs 62 | about what else they provide. 63 | 64 | The following common subclasses exist: 65 | 66 | * :exc:`UsageError` to inform the user that something went wrong. 67 | * :exc:`BadParameter` to inform the user that something went wrong with 68 | a specific parameter. These are often handled internally in Click and 69 | augmented with extra information if possible. For instance if those 70 | are raised from a callback Click will automatically augment it with 71 | the parameter name if possible. 72 | * :exc:`FileError` this is an error that is raised by the 73 | :exc:`FileType` if Click encounters issues opening the file. 74 | -------------------------------------------------------------------------------- /tests/test_custom_classes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import asyncclick as click 4 | 5 | 6 | @pytest.mark.anyio 7 | async def test_command_context_class(): 8 | """A command with a custom ``context_class`` should produce a 9 | context using that type. 10 | """ 11 | 12 | class CustomContext(click.Context): 13 | pass 14 | 15 | class CustomCommand(click.Command): 16 | context_class = CustomContext 17 | 18 | command = CustomCommand("test") 19 | context = await command.make_context("test", []) 20 | assert isinstance(context, CustomContext) 21 | 22 | 23 | def test_context_invoke_type(runner): 24 | """A command invoked from a custom context should have a new 25 | context with the same type. 26 | """ 27 | 28 | class CustomContext(click.Context): 29 | pass 30 | 31 | class CustomCommand(click.Command): 32 | context_class = CustomContext 33 | 34 | @click.command() 35 | @click.argument("first_id", type=int) 36 | @click.pass_context 37 | def second(ctx, first_id): 38 | assert isinstance(ctx, CustomContext) 39 | assert id(ctx) != first_id 40 | 41 | @click.command(cls=CustomCommand) 42 | @click.pass_context 43 | async def first(ctx): 44 | assert isinstance(ctx, CustomContext) 45 | await ctx.invoke(second, first_id=id(ctx)) 46 | 47 | assert not runner.invoke(first).exception 48 | 49 | 50 | def test_context_formatter_class(): 51 | """A context with a custom ``formatter_class`` should format help 52 | using that type. 53 | """ 54 | 55 | class CustomFormatter(click.HelpFormatter): 56 | def write_heading(self, heading): 57 | heading = click.style(heading, fg="yellow") 58 | return super().write_heading(heading) 59 | 60 | class CustomContext(click.Context): 61 | formatter_class = CustomFormatter 62 | 63 | context = CustomContext( 64 | click.Command("test", params=[click.Option(["--value"])]), color=True 65 | ) 66 | assert "\x1b[33mOptions\x1b[0m:" in context.get_help() 67 | 68 | 69 | def test_group_command_class(runner): 70 | """A group with a custom ``command_class`` should create subcommands 71 | of that type by default. 72 | """ 73 | 74 | class CustomCommand(click.Command): 75 | pass 76 | 77 | class CustomGroup(click.Group): 78 | command_class = CustomCommand 79 | 80 | group = CustomGroup() 81 | subcommand = group.command()(lambda: None) 82 | assert type(subcommand) is CustomCommand 83 | subcommand = group.command(cls=click.Command)(lambda: None) 84 | assert type(subcommand) is click.Command 85 | 86 | 87 | def test_group_group_class(runner): 88 | """A group with a custom ``group_class`` should create subgroups 89 | of that type by default. 90 | """ 91 | 92 | class CustomSubGroup(click.Group): 93 | pass 94 | 95 | class CustomGroup(click.Group): 96 | group_class = CustomSubGroup 97 | 98 | group = CustomGroup() 99 | subgroup = group.group()(lambda: None) 100 | assert type(subgroup) is CustomSubGroup 101 | subgroup = group.command(cls=click.Group)(lambda: None) 102 | assert type(subgroup) is click.Group 103 | 104 | 105 | def test_group_group_class_self(runner): 106 | """A group with ``group_class = type`` should create subgroups of 107 | the same type as itself. 108 | """ 109 | 110 | class CustomGroup(click.Group): 111 | group_class = type 112 | 113 | group = CustomGroup() 114 | subgroup = group.group()(lambda: None) 115 | assert type(subgroup) is CustomGroup 116 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. rst-class:: hide-header 2 | 3 | .. image:: _static/click-logo.png 4 | Welcome to the AsyncClick Documentation 5 | ======================================= 6 | 7 | .. image:: _static/click-name.svg 8 | :align: center 9 | :height: 200px 10 | 11 | AsyncClick is a fork of Click that works well with (some) async 12 | frameworks. Supported: asyncio, trio, and curio. 13 | 14 | Click, in turn, is a Python package for creating beautiful command line interfaces 15 | in a composable way with as little code as necessary. It's the "Command 16 | Line Interface Creation Kit". It's highly configurable but comes with 17 | sensible defaults out of the box. 18 | 19 | It aims to make the process of writing command line tools quick and fun 20 | while also preventing any frustration caused by the inability to implement 21 | an intended CLI API. 22 | 23 | Click in three points: 24 | 25 | - arbitrary nesting of commands 26 | - automatic help page generation 27 | - supports lazy loading of subcommands at runtime 28 | 29 | What does it look like? Here is an example of a simple Click program: 30 | 31 | .. click:example:: 32 | 33 | import asyncclick as click 34 | import anyio 35 | 36 | @click.command() 37 | @click.option('--count', default=1, help='Number of greetings.') 38 | @click.option('--name', prompt='Your name', 39 | help='The person to greet.') 40 | async def hello(count, name): 41 | """Simple program that greets NAME for a total of COUNT times.""" 42 | for x in range(count): 43 | if x: await anyio.sleep(0.1) 44 | click.echo(f"Hello {name}!") 45 | 46 | if __name__ == '__main__': 47 | hello() 48 | 49 | And what it looks like when run: 50 | 51 | .. click:run:: 52 | 53 | invoke(hello, ['--count=3'], prog_name='python hello.py', input='John\n') 54 | 55 | It automatically generates nicely formatted help pages: 56 | 57 | .. click:run:: 58 | 59 | invoke(hello, ['--help'], prog_name='python hello.py') 60 | 61 | You can get the library directly from PyPI:: 62 | 63 | pip install asyncclick 64 | 65 | Documentation 66 | ============== 67 | 68 | .. toctree:: 69 | :maxdepth: 2 70 | 71 | faqs 72 | 73 | Tutorials 74 | ------------ 75 | .. toctree:: 76 | :maxdepth: 1 77 | 78 | quickstart 79 | virtualenv 80 | 81 | How to Guides 82 | --------------- 83 | .. toctree:: 84 | :maxdepth: 1 85 | 86 | entry-points 87 | setuptools 88 | support-multiple-versions 89 | 90 | Conceptual Guides 91 | ------------------- 92 | .. toctree:: 93 | :maxdepth: 1 94 | 95 | why 96 | click-concepts 97 | 98 | General Reference 99 | -------------------- 100 | 101 | .. toctree:: 102 | :maxdepth: 1 103 | 104 | parameters 105 | parameter-types 106 | options 107 | option-decorators 108 | arguments 109 | commands-and-groups 110 | commands 111 | documentation 112 | prompts 113 | handling-files 114 | advanced 115 | complex 116 | extending-click 117 | testing 118 | utils 119 | shell-completion 120 | exceptions 121 | unicode-support 122 | wincmd 123 | 124 | API Reference 125 | ------------------- 126 | 127 | .. toctree:: 128 | :maxdepth: 2 129 | 130 | api 131 | 132 | About Project 133 | =============== 134 | 135 | * This documentation is structured according to `Diataxis `_ 136 | 137 | * `Version Policy `_ 138 | 139 | * `Contributing `_ 140 | 141 | * `Donate `_ 142 | 143 | .. toctree:: 144 | :maxdepth: 1 145 | 146 | contrib 147 | license 148 | changes 149 | -------------------------------------------------------------------------------- /docs/contrib.md: -------------------------------------------------------------------------------- 1 | (contrib)= 2 | 3 | # click-contrib 4 | 5 | As the user number of Click grows, more and more major feature requests are 6 | made. To users, it may seem reasonable to include those features with Click; 7 | however, many of them are experimental or aren't practical to support 8 | generically. Maintainers have to choose what is reasonable to maintain in Click 9 | core. 10 | 11 | The [click-contrib](https://github.com/click-contrib/) GitHub organization exists as a place to collect third-party 12 | packages that extend Click's features. It is also meant to ease the effort of 13 | searching for such extensions. 14 | 15 | Please note that the quality and stability of those packages may be different 16 | from Click itself. While published under a common organization, they are still 17 | separate from Click and the Pallets maintainers. 18 | 19 | ## Third-party projects 20 | 21 | Other projects that extend Click's features are available outside the 22 | [click-contrib](https://github.com/click-contrib/) organization. 23 | 24 | Some of the most popular and actively maintained are listed below: 25 | 26 | | Project | Description | Popularity | Activity | 27 | |---------------------------------------------------------|--------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| 28 | | [Typer](https://github.com/fastapi/typer) | Use Python type hints to create CLI apps. | ![GitHub stars](https://img.shields.io/github/stars/fastapi/typer?label=%20&style=flat-square) | ![Last commit](https://img.shields.io/github/last-commit/fastapi/typer?label=%20&style=flat-square) | 29 | | [rich-click](https://github.com/ewels/rich-click) | Format help output with Rich. | ![GitHub stars](https://img.shields.io/github/stars/ewels/rich-click?label=%20&style=flat-square) | ![Last commit](https://img.shields.io/github/last-commit/ewels/rich-click?label=%20&style=flat-square) | 30 | | [click-app](https://github.com/simonw/click-app) | Cookiecutter template for creating new CLIs. | ![GitHub stars](https://img.shields.io/github/stars/simonw/click-app?label=%20&style=flat-square) | ![Last commit](https://img.shields.io/github/last-commit/simonw/click-app?label=%20&style=flat-square) | 31 | | [Cloup](https://github.com/janluke/cloup) | Adds option groups, constraints, command aliases, help themes, suggestions and more. | ![GitHub stars](https://img.shields.io/github/stars/janluke/cloup?label=%20&style=flat-square) | ![Last commit](https://img.shields.io/github/last-commit/janluke/cloup?label=%20&style=flat-square) | 32 | | [Click Extra](https://github.com/kdeldycke/click-extra) | Cloup + colorful `--help`, `--config`, `--show-params`, `--verbosity` options, etc. | ![GitHub stars](https://img.shields.io/github/stars/kdeldycke/click-extra?label=%20&style=flat-square) | ![Last commit](https://img.shields.io/github/last-commit/kdeldycke/click-extra?label=%20&style=flat-square) | 33 | 34 | ```{note} 35 | To make it into the list above, a project: 36 | 37 | - must be actively maintained (at least one commit in the last year) 38 | - must have a reasonable number of stars (at least 20) 39 | 40 | If you have a project that meets these criteria, please open a pull request 41 | to add it to the list. 42 | 43 | If a project is no longer maintained or does not meet the criteria above, 44 | please open a pull request to remove it from the list. 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/handling-files.rst: -------------------------------------------------------------------------------- 1 | .. _handling-files: 2 | 3 | Handling Files 4 | ================ 5 | 6 | .. currentmodule:: click 7 | 8 | Click has built in features to support file and file path handling. The examples use arguments but the same principle applies to options as well. 9 | 10 | .. _file-args: 11 | 12 | File Arguments 13 | ----------------- 14 | 15 | Click supports working with files with the :class:`File` type. Some notable features are: 16 | 17 | * Support for ``-`` to mean a special file that refers to stdin when used for reading, and stdout when used for writing. This is a common pattern for POSIX command line utilities. 18 | * Deals with ``str`` and ``bytes`` correctly for all versions of Python. 19 | 20 | Example: 21 | 22 | .. click:example:: 23 | 24 | @click.command() 25 | @click.argument('input', type=click.File('rb')) 26 | @click.argument('output', type=click.File('wb')) 27 | def inout(input, output): 28 | """Copy contents of INPUT to OUTPUT.""" 29 | while True: 30 | chunk = input.read(1024) 31 | if not chunk: 32 | break 33 | output.write(chunk) 34 | 35 | And from the command line: 36 | 37 | .. click:run:: 38 | 39 | with isolated_filesystem(): 40 | invoke(inout, args=['-', 'hello.txt'], input=['hello'], 41 | terminate_input=True) 42 | invoke(inout, args=['hello.txt', '-']) 43 | 44 | File Path Arguments 45 | ---------------------- 46 | 47 | For handling paths, the :class:`Path` type is better than a ``str``. Some notable features are: 48 | 49 | * The ``exists`` argument will verify whether the path exists. 50 | * ``readable``, ``writable``, and ``executable`` can perform permission checks. 51 | * ``file_okay`` and ``dir_okay`` allow specifying whether files/directories are accepted. 52 | * Error messages are nicely formatted using :func:`format_filename` so any undecodable bytes will be printed nicely. 53 | 54 | See :class:`Path` for all features. 55 | 56 | Example: 57 | 58 | .. click:example:: 59 | 60 | @click.command() 61 | @click.argument('filename', type=click.Path(exists=True)) 62 | def touch(filename): 63 | """Print FILENAME if the file exists.""" 64 | click.echo(click.format_filename(filename)) 65 | 66 | And from the command line: 67 | 68 | .. click:run:: 69 | 70 | with isolated_filesystem(): 71 | with open('hello.txt', 'w') as f: 72 | f.write('Hello World!\n') 73 | invoke(touch, args=['hello.txt']) 74 | println() 75 | invoke(touch, args=['missing.txt']) 76 | 77 | 78 | File Opening Behaviors 79 | ----------------------------- 80 | 81 | The :class:`File` type attempts to be "intelligent" about when to open a file. Stdin/stdout and files opened for reading will be opened immediately. This will give the user direct feedback when a file cannot be opened. Files opened for writing will only be open on the first IO operation. This is done by automatically wrapping the file in a special wrapper. 82 | 83 | File open behavior can be controlled by the boolean kwarg ``lazy``. If a file is opened lazily: 84 | 85 | * A failure at first IO operation will happen by raising an :exc:`FileError`. 86 | * It can help minimize resource handling confusion. If a file is opened in lazy mode, it will call :meth:`LazyFile.close_intelligently` to help figure out if the file needs closing or not. This is not needed for parameters, but is necessary for manually prompting. For manual prompts with the :func:`prompt` function you do not know if a stream like stdout was opened (which was already open before) or a real file was opened (that needs closing). 87 | 88 | Since files opened for writing will typically empty the file, the lazy mode should only be disabled if the developer is absolutely sure that this is intended behavior. 89 | 90 | It is also possible to open files in atomic mode by passing ``atomic=True``. In atomic mode, all writes go into a separate file in the same folder, and upon completion, the file will be moved over to the original location. This is useful if a file regularly read by other users is modified. 91 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. module:: asyncclick 5 | 6 | This part of the documentation lists the full API reference of all public 7 | classes and functions. 8 | 9 | .. contents:: 10 | :depth: 1 11 | :local: 12 | 13 | Decorators 14 | ---------- 15 | 16 | .. autofunction:: command 17 | 18 | .. autofunction:: group 19 | 20 | .. autofunction:: argument 21 | 22 | .. autofunction:: option 23 | 24 | .. autofunction:: password_option 25 | 26 | .. autofunction:: confirmation_option 27 | 28 | .. autofunction:: version_option 29 | 30 | .. autofunction:: help_option 31 | 32 | .. autofunction:: pass_context 33 | 34 | .. autofunction:: pass_obj 35 | 36 | .. autofunction:: make_pass_decorator 37 | 38 | .. autofunction:: asyncclick.decorators.pass_meta_key 39 | 40 | 41 | Utilities 42 | --------- 43 | 44 | .. autofunction:: echo 45 | 46 | .. autofunction:: echo_via_pager 47 | 48 | .. autofunction:: prompt 49 | 50 | .. autofunction:: confirm 51 | 52 | .. autofunction:: progressbar 53 | 54 | .. autofunction:: clear 55 | 56 | .. autofunction:: style 57 | 58 | .. autofunction:: unstyle 59 | 60 | .. autofunction:: secho 61 | 62 | .. autofunction:: edit 63 | 64 | .. autofunction:: launch 65 | 66 | .. autofunction:: getchar 67 | 68 | .. autofunction:: pause 69 | 70 | .. autofunction:: get_binary_stream 71 | 72 | .. autofunction:: get_text_stream 73 | 74 | .. autofunction:: open_file 75 | 76 | .. autofunction:: get_app_dir 77 | 78 | .. autofunction:: format_filename 79 | 80 | Commands 81 | -------- 82 | 83 | .. autoclass:: BaseCommand 84 | :members: 85 | 86 | .. autoclass:: Command 87 | :members: 88 | 89 | .. autoclass:: MultiCommand 90 | :members: 91 | 92 | .. autoclass:: Group 93 | :members: 94 | 95 | .. autoclass:: CommandCollection 96 | :members: 97 | 98 | Parameters 99 | ---------- 100 | 101 | .. autoclass:: Parameter 102 | :members: 103 | 104 | .. autoclass:: Option 105 | 106 | .. autoclass:: Argument 107 | 108 | Context 109 | ------- 110 | 111 | .. autoclass:: Context 112 | :members: 113 | 114 | .. autofunction:: get_current_context 115 | 116 | .. autoclass:: asyncclick.core.ParameterSource 117 | :members: 118 | :member-order: bysource 119 | 120 | .. _click-api-types: 121 | 122 | Types 123 | ----- 124 | 125 | .. autodata:: STRING 126 | 127 | .. autodata:: INT 128 | 129 | .. autodata:: FLOAT 130 | 131 | .. autodata:: BOOL 132 | 133 | .. autodata:: UUID 134 | 135 | .. autodata:: UNPROCESSED 136 | 137 | .. autoclass:: File 138 | 139 | .. autoclass:: Path 140 | 141 | .. autoclass:: Choice 142 | :members: 143 | 144 | .. autoclass:: IntRange 145 | 146 | .. autoclass:: FloatRange 147 | 148 | .. autoclass:: DateTime 149 | 150 | .. autoclass:: Tuple 151 | 152 | .. autoclass:: ParamType 153 | :members: 154 | 155 | Exceptions 156 | ---------- 157 | 158 | .. autoexception:: ClickException 159 | 160 | .. autoexception:: Abort 161 | 162 | .. autoexception:: UsageError 163 | 164 | .. autoexception:: BadParameter 165 | 166 | .. autoexception:: FileError 167 | 168 | .. autoexception:: NoSuchOption 169 | 170 | .. autoexception:: BadOptionUsage 171 | 172 | .. autoexception:: BadArgumentUsage 173 | 174 | Formatting 175 | ---------- 176 | 177 | .. autoclass:: HelpFormatter 178 | :members: 179 | 180 | .. autofunction:: wrap_text 181 | 182 | Parsing 183 | ------- 184 | 185 | .. autoclass:: OptionParser 186 | :members: 187 | 188 | 189 | Shell Completion 190 | ---------------- 191 | 192 | See :doc:`/shell-completion` for information about enabling and 193 | customizing Click's shell completion system. 194 | 195 | .. currentmodule:: asyncclick.shell_completion 196 | 197 | .. autoclass:: CompletionItem 198 | 199 | .. autoclass:: ShellComplete 200 | :members: 201 | :member-order: bysource 202 | 203 | .. autofunction:: add_completion_class 204 | 205 | 206 | .. _testing: 207 | 208 | Testing 209 | ------- 210 | 211 | .. currentmodule:: asyncclick.testing 212 | 213 | .. autoclass:: CliRunner 214 | :members: 215 | 216 | .. autoclass:: Result 217 | :members: 218 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/dev.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | alabaster==1.0.0 13 | # via sphinx 14 | anyio==4.8.0 15 | # via -r requirements/dev.in 16 | attrs==24.3.0 17 | # via 18 | # outcome 19 | # trio 20 | babel==2.16.0 21 | # via sphinx 22 | build==1.2.2.post1 23 | # via pip-tools 24 | cachetools==5.5.0 25 | # via tox 26 | certifi==2024.8.30 27 | # via requests 28 | cfgv==3.4.0 29 | # via pre-commit 30 | chardet==5.2.0 31 | # via tox 32 | charset-normalizer==3.4.0 33 | # via requests 34 | click==8.1.7 35 | # via 36 | # pip-compile-multi 37 | # pip-tools 38 | colorama==0.4.6 39 | # via tox 40 | distlib==0.3.9 41 | # via virtualenv 42 | docutils==0.21.2 43 | # via 44 | # sphinx 45 | # sphinx-tabs 46 | filelock==3.16.1 47 | # via 48 | # tox 49 | # virtualenv 50 | identify==2.6.3 51 | # via pre-commit 52 | idna==3.10 53 | # via 54 | # anyio 55 | # requests 56 | # trio 57 | imagesize==1.4.1 58 | # via sphinx 59 | iniconfig==2.0.0 60 | # via pytest 61 | jinja2==3.1.4 62 | # via sphinx 63 | markupsafe==3.0.2 64 | # via jinja2 65 | mypy==1.13.0 66 | # via -r /src/asyncclick/requirements/typing.in 67 | mypy-extensions==1.0.0 68 | # via mypy 69 | nodeenv==1.9.1 70 | # via 71 | # pre-commit 72 | # pyright 73 | outcome==1.3.0.post0 74 | # via trio 75 | packaging==24.2 76 | # via 77 | # build 78 | # pallets-sphinx-themes 79 | # pyproject-api 80 | # pytest 81 | # sphinx 82 | # tox 83 | pallets-sphinx-themes==2.3.0 84 | # via -r /src/asyncclick/requirements/docs.in 85 | pip-compile-multi==2.7.1 86 | # via -r requirements/dev.in 87 | pip-tools==7.4.1 88 | # via pip-compile-multi 89 | platformdirs==4.3.6 90 | # via 91 | # tox 92 | # virtualenv 93 | pluggy==1.5.0 94 | # via 95 | # pytest 96 | # tox 97 | pre-commit==4.0.1 98 | # via -r requirements/dev.in 99 | pygments==2.18.0 100 | # via 101 | # sphinx 102 | # sphinx-tabs 103 | pyproject-api==1.8.0 104 | # via tox 105 | pyproject-hooks==1.2.0 106 | # via 107 | # build 108 | # pip-tools 109 | pyright==1.1.390 110 | # via -r /src/asyncclick/requirements/typing.in 111 | pytest==8.3.4 112 | # via -r /src/asyncclick/requirements/tests.in 113 | pyyaml==6.0.2 114 | # via pre-commit 115 | requests==2.32.3 116 | # via sphinx 117 | sniffio==1.3.1 118 | # via 119 | # anyio 120 | # trio 121 | snowballstemmer==2.2.0 122 | # via sphinx 123 | sortedcontainers==2.4.0 124 | # via trio 125 | sphinx==8.1.3 126 | # via 127 | # -r /src/asyncclick/requirements/docs.in 128 | # pallets-sphinx-themes 129 | # sphinx-issues 130 | # sphinx-notfound-page 131 | # sphinx-tabs 132 | # sphinxcontrib-log-cabinet 133 | sphinx-issues==5.0.0 134 | # via -r /src/asyncclick/requirements/docs.in 135 | sphinx-notfound-page==1.0.4 136 | # via pallets-sphinx-themes 137 | sphinx-tabs==3.4.7 138 | # via -r /src/asyncclick/requirements/docs.in 139 | sphinxcontrib-applehelp==2.0.0 140 | # via sphinx 141 | sphinxcontrib-devhelp==2.0.0 142 | # via sphinx 143 | sphinxcontrib-htmlhelp==2.1.0 144 | # via sphinx 145 | sphinxcontrib-jsmath==1.0.1 146 | # via sphinx 147 | sphinxcontrib-log-cabinet==1.0.1 148 | # via -r /src/asyncclick/requirements/docs.in 149 | sphinxcontrib-qthelp==2.0.0 150 | # via sphinx 151 | sphinxcontrib-serializinghtml==2.0.0 152 | # via sphinx 153 | toposort==1.10 154 | # via pip-compile-multi 155 | tox==4.23.2 156 | # via -r requirements/dev.in 157 | trio==0.28.0 158 | # via 159 | # -r /src/asyncclick/requirements/tests.in 160 | # -r requirements/dev.in 161 | typing-extensions==4.12.2 162 | # via 163 | # anyio 164 | # mypy 165 | # pyright 166 | urllib3==2.2.3 167 | # via requests 168 | virtualenv==20.28.0 169 | # via 170 | # pre-commit 171 | # tox 172 | wheel==0.45.1 173 | # via pip-tools 174 | 175 | # The following packages are considered to be unsafe in a requirements file: 176 | # pip 177 | # setuptools 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # $ asyncclick_ 2 | 3 | Asyncclick is a fork of Click (described below) that works with trio or asyncio. 4 | 5 | AsyncClick allows you to seamlessly use async command and subcommand handlers. 6 | 7 | 8 |
9 | 10 | # Click 11 | 12 | Click is a Python package for creating beautiful command line interfaces 13 | in a composable way with as little code as necessary. It's the "Command 14 | Line Interface Creation Kit". It's highly configurable but comes with 15 | sensible defaults out of the box. 16 | 17 | It aims to make the process of writing command line tools quick and fun 18 | while also preventing any frustration caused by the inability to 19 | implement an intended CLI API. 20 | 21 | Click in three points: 22 | 23 | - Arbitrary nesting of commands 24 | - Automatic help page generation 25 | - Supports lazy loading of subcommands at runtime 26 | 27 | 28 | ## A Simple Example 29 | 30 | ```python 31 | import asyncclick as click 32 | import anyio 33 | 34 | @click.command() 35 | @click.option("--count", default=1, help="Number of greetings.") 36 | @click.option("--name", prompt="Your name", help="The person to greet.") 37 | async def hello(count, name): 38 | """Simple program that greets NAME for a total of COUNT times.""" 39 | for _ in range(count): 40 | click.echo(f"Hello, {name}!") 41 | await anyio.sleep(0.2) 42 | 43 | if __name__ == '__main__': 44 | hello() 45 | # alternately: anyio.run(hello.main) 46 | ``` 47 | 48 | ``` 49 | $ python hello.py --count=3 50 | Your name: Click 51 | Hello, Click! 52 | Hello, Click! 53 | Hello, Click! 54 | ``` 55 | 56 | ## Differences to Click 57 | 58 | This async-ized version of Click is mostly backwards compatible for "normal" use: 59 | you can freely mix sync and async versions of your command handlers and callbacks. 60 | 61 | Several advanced methods, most notably :meth:`BaseCommand.main`, and 62 | :meth:`Context.invoke`, are now asynchronous. 63 | 64 | The :meth:`BaseCommand.__call__` alias now invokes the main entry point via 65 | `anyio.run`. If you already have an async main program, simply use 66 | ``await cmd.main()`` instead of ``cmd()``. 67 | 68 | :func:`asyncclick.prompt` is asyncronous and accepts a ``blocking`` parameter 69 | that switches between "doesn't affect your event loop but has unwanted effects when 70 | interrupted" (bugfix pending) and "pauses your event loop but is safe to interrupt" 71 | with Control-C". The latter is the default until we fix that bug. 72 | 73 | You cannot use Click and AsyncClick in the same program. This is not a problem 74 | in practice, as replacing ``import click`` with ``import asyncclick as click``, and 75 | ``from click import ...`` with ``from asyncclick import ...``, should be all that's 76 | required. 77 | 78 | ### Notable packages supporting asyncclick 79 | 80 | * [OpenTelemetry][opentelemetry] supports instrumenting asyncclick. 81 | 82 | [opentelemetry]: https://pypi.org/project/opentelemetry-instrumentation-asyncclick/ 83 | 84 | 85 | ## Donate 86 | 87 | The Pallets organization develops and supports Click and other popular 88 | packages. In order to grow the community of contributors and users, and 89 | allow the maintainers to devote more time to the projects, [please 90 | donate today][]. 91 | 92 | [please donate today]: https://palletsprojects.com/donate 93 | 94 | The AsyncClick fork is maintained by Matthias Urlichs . 95 | 96 | ## Contributing 97 | 98 | ### Click 99 | 100 | See our [detailed contributing documentation][contrib] for many ways to 101 | contribute, including reporting issues, requesting features, asking or answering 102 | questions, and making PRs. 103 | 104 | [contrib]: https://palletsprojects.com/contributing/ 105 | 106 | ### AsyncClick 107 | 108 | You can file async-specific issues, ideally including a corresponding fix, 109 | to the [MoaT/asyncclick][moat] repository on github. 110 | 111 | [moat]: https://github.com/M-o-a-T/asyncclick 112 | 113 | #### Testing 114 | 115 | If you find a bug, please add a testcase to prevent it from recurring. 116 | 117 | In tests, you might wonder why `runner.invoke` is not called asynchronously. 118 | The reason is that there are far too many of these calls to modify them all. 119 | Thus ``tests/conftest.py`` contains a monkeypatch that turns this call 120 | into a thread that runs this call using `anyio.run`. 121 | -------------------------------------------------------------------------------- /docs/extending-click.rst: -------------------------------------------------------------------------------- 1 | Extending Click 2 | ================= 3 | 4 | .. currentmodule:: click 5 | 6 | In addition to common functionality that is implemented in the library 7 | itself, there are countless patterns that can be implemented by extending 8 | Click. This page should give some insight into what can be accomplished. 9 | 10 | .. contents:: 11 | :depth: 2 12 | :local: 13 | 14 | .. _custom-groups: 15 | 16 | Custom Groups 17 | ------------- 18 | 19 | You can customize the behavior of a group beyond the arguments it accepts by 20 | subclassing :class:`click.Group`. 21 | 22 | The most common methods to override are :meth:`~click.Group.get_command` and 23 | :meth:`~click.Group.list_commands`. 24 | 25 | The following example implements a basic plugin system that loads commands from 26 | Python files in a folder. The command is lazily loaded to avoid slow startup. 27 | 28 | .. code-block:: python 29 | 30 | import importlib.util 31 | import os 32 | import click 33 | 34 | class PluginGroup(click.Group): 35 | def __init__(self, name=None, plugin_folder="commands", **kwargs): 36 | super().__init__(name=name, **kwargs) 37 | self.plugin_folder = plugin_folder 38 | 39 | def list_commands(self, ctx): 40 | rv = [] 41 | 42 | for filename in os.listdir(self.plugin_folder): 43 | if filename.endswith(".py"): 44 | rv.append(filename[:-3]) 45 | 46 | rv.sort() 47 | return rv 48 | 49 | def get_command(self, ctx, name): 50 | path = os.path.join(self.plugin_folder, f"{name}.py") 51 | spec = importlib.util.spec_from_file_location(name, path) 52 | module = importlib.util.module_from_spec(spec) 53 | spec.loader.exec_module(module) 54 | return module.cli 55 | 56 | cli = PluginGroup( 57 | plugin_folder=os.path.join(os.path.dirname(__file__), "commands") 58 | ) 59 | 60 | if __name__ == "__main__": 61 | cli() 62 | 63 | Custom classes can also be used with decorators: 64 | 65 | .. code-block:: python 66 | 67 | @click.group( 68 | cls=PluginGroup, 69 | plugin_folder=os.path.join(os.path.dirname(__file__), "commands") 70 | ) 71 | def cli(): 72 | pass 73 | 74 | .. _aliases: 75 | 76 | Command Aliases 77 | --------------- 78 | 79 | Many tools support aliases for commands. For example, you can configure 80 | ``git`` to accept ``git ci`` as alias for ``git commit``. Other tools also 81 | support auto-discovery for aliases by automatically shortening them. 82 | 83 | It's possible to customize :class:`Group` to provide this functionality. As 84 | explained in :ref:`custom-groups`, a group provides two methods: 85 | :meth:`~Group.list_commands` and :meth:`~Group.get_command`. In this particular 86 | case, you only need to override the latter as you generally don't want to 87 | enumerate the aliases on the help page in order to avoid confusion. 88 | 89 | The following example implements a subclass of :class:`Group` that accepts a 90 | prefix for a command. If there was a command called ``push``, it would accept 91 | ``pus`` as an alias (so long as it was unique): 92 | 93 | .. click:example:: 94 | 95 | class AliasedGroup(click.Group): 96 | def get_command(self, ctx, cmd_name): 97 | rv = super().get_command(ctx, cmd_name) 98 | 99 | if rv is not None: 100 | return rv 101 | 102 | matches = [ 103 | x for x in self.list_commands(ctx) 104 | if x.startswith(cmd_name) 105 | ] 106 | 107 | if not matches: 108 | return None 109 | 110 | if len(matches) == 1: 111 | return click.Group.get_command(self, ctx, matches[0]) 112 | 113 | ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 114 | 115 | def resolve_command(self, ctx, args): 116 | # always return the full command name 117 | _, cmd, args = super().resolve_command(ctx, args) 118 | return cmd.name, cmd, args 119 | 120 | It can be used like this: 121 | 122 | .. click:example:: 123 | 124 | @click.group(cls=AliasedGroup) 125 | def cli(): 126 | pass 127 | 128 | @cli.command 129 | def push(): 130 | pass 131 | 132 | @cli.command 133 | def pop(): 134 | pass 135 | 136 | See the `alias example`_ in Click's repository for another example. 137 | 138 | .. _alias example: https://github.com/pallets/click/tree/main/examples/aliases 139 | -------------------------------------------------------------------------------- /docs/unicode-support.md: -------------------------------------------------------------------------------- 1 | # Unicode Support 2 | 3 | ```{currentmodule} click 4 | ``` 5 | 6 | Click has to take extra care to support Unicode text in different environments. 7 | 8 | - The command line in Unix is traditionally bytes, not Unicode. While there are encoding hints, there are some 9 | situations where this can break. The most common one is SSH connections to machines with different locales. 10 | 11 | Misconfigured environments can cause a wide range of Unicode problems due to the lack of support for roundtripping 12 | surrogate escapes. This will not be fixed in Click itself! 13 | 14 | - Standard input and output is opened in text mode by default. Click has to reopen the stream in binary mode in certain 15 | situations. Because there is no standard way to do this, it might not always work. Primarily this can become a problem 16 | when testing command-line applications. 17 | 18 | This is not supported: 19 | 20 | ```python 21 | sys.stdin = io.StringIO('Input here') 22 | sys.stdout = io.StringIO() 23 | ``` 24 | 25 | Instead you need to do this: 26 | 27 | ```python 28 | input = 'Input here' 29 | in_stream = io.BytesIO(input.encode('utf-8')) 30 | sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8') 31 | out_stream = io.BytesIO() 32 | sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8') 33 | ``` 34 | 35 | Remember in that case, you need to use `out_stream.getvalue()` and not `sys.stdout.getvalue()` if you want to access 36 | the buffer contents as the wrapper will not forward that method. 37 | 38 | - `sys.stdin`, `sys.stdout` and `sys.stderr` are by default text-based. When Click needs a binary stream, it attempts to 39 | discover the underlying binary stream. 40 | 41 | - `sys.argv` is always text. This means that the native type for input values to the types in Click is Unicode, not 42 | bytes. 43 | 44 | This causes problems if the terminal is incorrectly set and Python does not figure out the encoding. In that case, the 45 | Unicode string will contain error bytes encoded as surrogate escapes. 46 | 47 | - When dealing with files, Click will always use the Unicode file system API by using the operating system's reported or 48 | guessed filesystem encoding. Surrogates are supported for filenames, so it should be possible to open files through 49 | the {func}`File` type even if the environment is misconfigured. 50 | 51 | ## Surrogate Handling 52 | 53 | Click does all the Unicode handling in the standard library and is subject to its behavior. Unicode requires extra care. 54 | The reason for this is that the encoding detection is done in the interpreter, and on Linux and certain other operating 55 | systems, its encoding handling is problematic. 56 | 57 | The biggest source of frustration is that Click scripts invoked by init systems, deployment tools, or cron jobs will 58 | refuse to work unless a Unicode locale is exported. 59 | 60 | If Click encounters such an environment it will prevent further execution to force you to set a locale. This is done 61 | because Click cannot know about the state of the system once it's invoked and restore the values before Python's Unicode 62 | handling kicked in. 63 | 64 | If you see something like this error: 65 | 66 | ```console 67 | Traceback (most recent call last): 68 | ... 69 | RuntimeError: Click will abort further execution because Python was 70 | configured to use ASCII as encoding for the environment. Consult 71 | https://click.palletsprojects.com/unicode-support/ for mitigation 72 | steps. 73 | ``` 74 | 75 | You are dealing with an environment where Python thinks you are restricted to ASCII data. The solution to these problems 76 | is different depending on which locale your computer is running in. 77 | 78 | For instance, if you have a German Linux machine, you can fix the problem by exporting the locale to `de_DE.utf-8`: 79 | 80 | ```console 81 | export LC_ALL=de_DE.utf-8 82 | export LANG=de_DE.utf-8 83 | ``` 84 | 85 | If you are on a US machine, `en_US.utf-8` is the encoding of choice. On some newer Linux systems, you could also try 86 | `C.UTF-8` as the locale: 87 | 88 | ```console 89 | export LC_ALL=C.UTF-8 90 | export LANG=C.UTF-8 91 | ``` 92 | 93 | On some systems it was reported that `UTF-8` has to be written as `UTF8` and vice versa. To see which locales are 94 | supported you can invoke `locale -a`. 95 | 96 | You need to export the values before you invoke your Python script. 97 | 98 | In Python 3.7 and later you will no longer get a `RuntimeError` in many cases thanks to {pep}`538` and {pep}`540`, which 99 | changed the default assumption in unconfigured environments. This doesn't change the general issue that your locale may 100 | be misconfigured. 101 | -------------------------------------------------------------------------------- /examples/aliases/aliases.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | 4 | import asyncclick as click 5 | 6 | 7 | class Config: 8 | """The config in this example only holds aliases.""" 9 | 10 | def __init__(self): 11 | self.path = os.getcwd() 12 | self.aliases = {} 13 | 14 | def add_alias(self, alias, cmd): 15 | self.aliases.update({alias: cmd}) 16 | 17 | def read_config(self, filename): 18 | parser = configparser.RawConfigParser() 19 | parser.read([filename]) 20 | try: 21 | self.aliases.update(parser.items("aliases")) 22 | except configparser.NoSectionError: 23 | pass 24 | 25 | def write_config(self, filename): 26 | parser = configparser.RawConfigParser() 27 | parser.add_section("aliases") 28 | for key, value in self.aliases.items(): 29 | parser.set("aliases", key, value) 30 | with open(filename, "wb") as file: 31 | parser.write(file) 32 | 33 | 34 | pass_config = click.make_pass_decorator(Config, ensure=True) 35 | 36 | 37 | class AliasedGroup(click.Group): 38 | """This subclass of a group supports looking up aliases in a config 39 | file and with a bit of magic. 40 | """ 41 | 42 | def get_command(self, ctx, cmd_name): 43 | # Step one: bulitin commands as normal 44 | rv = click.Group.get_command(self, ctx, cmd_name) 45 | if rv is not None: 46 | return rv 47 | 48 | # Step two: find the config object and ensure it's there. This 49 | # will create the config object is missing. 50 | cfg = ctx.ensure_object(Config) 51 | 52 | # Step three: look up an explicit command alias in the config 53 | if cmd_name in cfg.aliases: 54 | actual_cmd = cfg.aliases[cmd_name] 55 | return click.Group.get_command(self, ctx, actual_cmd) 56 | 57 | # Alternative option: if we did not find an explicit alias we 58 | # allow automatic abbreviation of the command. "status" for 59 | # instance will match "st". We only allow that however if 60 | # there is only one command. 61 | matches = [ 62 | x for x in self.list_commands(ctx) if x.lower().startswith(cmd_name.lower()) 63 | ] 64 | if not matches: 65 | return None 66 | elif len(matches) == 1: 67 | return click.Group.get_command(self, ctx, matches[0]) 68 | ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 69 | 70 | def resolve_command(self, ctx, args): 71 | # always return the command's name, not the alias 72 | _, cmd, args = super().resolve_command(ctx, args) 73 | return cmd.name, cmd, args 74 | 75 | 76 | def read_config(ctx, param, value): 77 | """Callback that is used whenever --config is passed. We use this to 78 | always load the correct config. This means that the config is loaded 79 | even if the group itself never executes so our aliases stay always 80 | available. 81 | """ 82 | cfg = ctx.ensure_object(Config) 83 | if value is None: 84 | value = os.path.join(os.path.dirname(__file__), "aliases.ini") 85 | cfg.read_config(value) 86 | return value 87 | 88 | 89 | @click.command(cls=AliasedGroup) 90 | @click.option( 91 | "--config", 92 | type=click.Path(exists=True, dir_okay=False), 93 | callback=read_config, 94 | expose_value=False, 95 | help="The config file to use instead of the default.", 96 | ) 97 | def cli(): 98 | """An example application that supports aliases.""" 99 | 100 | 101 | @cli.command() 102 | def push(): 103 | """Pushes changes.""" 104 | click.echo("Push") 105 | 106 | 107 | @cli.command() 108 | def pull(): 109 | """Pulls changes.""" 110 | click.echo("Pull") 111 | 112 | 113 | @cli.command() 114 | def clone(): 115 | """Clones a repository.""" 116 | click.echo("Clone") 117 | 118 | 119 | @cli.command() 120 | def commit(): 121 | """Commits pending changes.""" 122 | click.echo("Commit") 123 | 124 | 125 | @cli.command() 126 | @pass_config 127 | def status(config): 128 | """Shows the status.""" 129 | click.echo(f"Status for {config.path}") 130 | 131 | 132 | @cli.command() 133 | @pass_config 134 | @click.argument("alias_", metavar="ALIAS", type=click.STRING) 135 | @click.argument("cmd", type=click.STRING) 136 | @click.option( 137 | "--config_file", type=click.Path(exists=True, dir_okay=False), default="aliases.ini" 138 | ) 139 | def alias(config, alias_, cmd, config_file): 140 | """Adds an alias to the specified configuration file.""" 141 | config.add_alias(alias_, cmd) 142 | config.write_config(config_file) 143 | click.echo(f"Added '{alias_}' as alias for '{cmd}'") 144 | -------------------------------------------------------------------------------- /docs/_static/click-name.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /examples/termui/termui.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import time 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.group() 9 | def cli(): 10 | """This script showcases different terminal UI helpers in Click.""" 11 | pass 12 | 13 | 14 | @cli.command() 15 | def colordemo(): 16 | """Demonstrates ANSI color support.""" 17 | for color in "red", "green", "blue": 18 | click.echo(click.style(f"I am colored {color}", fg=color)) 19 | click.echo(click.style(f"I am background colored {color}", bg=color)) 20 | 21 | 22 | @cli.command() 23 | def pager(): 24 | """Demonstrates using the pager.""" 25 | lines = [] 26 | for x in range(200): 27 | lines.append(f"{click.style(str(x), fg='green')}. Hello World!") 28 | click.echo_via_pager("\n".join(lines)) 29 | 30 | 31 | @cli.command() 32 | @click.option( 33 | "--count", 34 | default=8000, 35 | type=click.IntRange(1, 100000), 36 | help="The number of items to process.", 37 | ) 38 | def progress(count): 39 | """Demonstrates the progress bar.""" 40 | items = range(count) 41 | 42 | def process_slowly(item): 43 | time.sleep(0.002 * random.random()) 44 | 45 | def filter(items): 46 | for item in items: 47 | if random.random() > 0.3: 48 | yield item 49 | 50 | with click.progressbar( 51 | items, label="Processing accounts", fill_char=click.style("#", fg="green") 52 | ) as bar: 53 | for item in bar: 54 | process_slowly(item) 55 | 56 | def show_item(item): 57 | if item is not None: 58 | return f"Item #{item}" 59 | 60 | with click.progressbar( 61 | filter(items), 62 | label="Committing transaction", 63 | fill_char=click.style("#", fg="yellow"), 64 | item_show_func=show_item, 65 | ) as bar: 66 | for item in bar: 67 | process_slowly(item) 68 | 69 | with click.progressbar( 70 | length=count, 71 | label="Counting", 72 | bar_template="%(label)s %(bar)s | %(info)s", 73 | fill_char=click.style("█", fg="cyan"), 74 | empty_char=" ", 75 | ) as bar: 76 | for item in bar: 77 | process_slowly(item) 78 | 79 | with click.progressbar( 80 | length=count, 81 | width=0, 82 | show_percent=False, 83 | show_eta=False, 84 | fill_char=click.style("#", fg="magenta"), 85 | ) as bar: 86 | for item in bar: 87 | process_slowly(item) 88 | 89 | # 'Non-linear progress bar' 90 | steps = [math.exp(x * 1.0 / 20) - 1 for x in range(20)] 91 | count = int(sum(steps)) 92 | with click.progressbar( 93 | length=count, 94 | show_percent=False, 95 | label="Slowing progress bar", 96 | fill_char=click.style("█", fg="green"), 97 | ) as bar: 98 | for item in steps: 99 | time.sleep(item) 100 | bar.update(item) 101 | 102 | 103 | @cli.command() 104 | @click.argument("url") 105 | def open(url): 106 | """Opens a file or URL In the default application.""" 107 | click.launch(url) 108 | 109 | 110 | @cli.command() 111 | @click.argument("url") 112 | def locate(url): 113 | """Opens a file or URL In the default application.""" 114 | click.launch(url, locate=True) 115 | 116 | 117 | @cli.command() 118 | def edit(): 119 | """Opens an editor with some text in it.""" 120 | MARKER = "# Everything below is ignored\n" 121 | message = click.edit(f"\n\n{MARKER}") 122 | if message is not None: 123 | msg = message.split(MARKER, 1)[0].rstrip("\n") 124 | if not msg: 125 | click.echo("Empty message!") 126 | else: 127 | click.echo(f"Message:\n{msg}") 128 | else: 129 | click.echo("You did not enter anything!") 130 | 131 | 132 | @cli.command() 133 | def clear(): 134 | """Clears the entire screen.""" 135 | click.clear() 136 | 137 | 138 | @cli.command() 139 | def pause(): 140 | """Waits for the user to press a button.""" 141 | click.pause() 142 | 143 | 144 | @cli.command() 145 | def menu(): 146 | """Shows a simple menu.""" 147 | menu = "main" 148 | while True: 149 | if menu == "main": 150 | click.echo("Main menu:") 151 | click.echo(" d: debug menu") 152 | click.echo(" q: quit") 153 | char = click.getchar() 154 | if char == "d": 155 | menu = "debug" 156 | elif char == "q": 157 | menu = "quit" 158 | else: 159 | click.echo("Invalid input") 160 | elif menu == "debug": 161 | click.echo("Debug menu") 162 | click.echo(" b: back") 163 | char = click.getchar() 164 | if char == "b": 165 | menu = "main" 166 | else: 167 | click.echo("Invalid input") 168 | elif menu == "quit": 169 | return 170 | -------------------------------------------------------------------------------- /src/asyncclick/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Click is a simple Python module inspired by the stdlib optparse to make 3 | writing command line scripts fun. Unlike other modules, it's based 4 | around a simple API that does not come with too much magic and is 5 | composable. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from .core import Argument as Argument 11 | from .core import Command as Command 12 | from .core import CommandCollection as CommandCollection 13 | from .core import Context as Context 14 | from .core import Group as Group 15 | from .core import Option as Option 16 | from .core import Parameter as Parameter 17 | from .decorators import argument as argument 18 | from .decorators import command as command 19 | from .decorators import confirmation_option as confirmation_option 20 | from .decorators import group as group 21 | from .decorators import help_option as help_option 22 | from .decorators import make_pass_decorator as make_pass_decorator 23 | from .decorators import option as option 24 | from .decorators import pass_context as pass_context 25 | from .decorators import pass_obj as pass_obj 26 | from .decorators import password_option as password_option 27 | from .decorators import version_option as version_option 28 | from .exceptions import Abort as Abort 29 | from .exceptions import BadArgumentUsage as BadArgumentUsage 30 | from .exceptions import BadOptionUsage as BadOptionUsage 31 | from .exceptions import BadParameter as BadParameter 32 | from .exceptions import ClickException as ClickException 33 | from .exceptions import FileError as FileError 34 | from .exceptions import MissingParameter as MissingParameter 35 | from .exceptions import NoSuchOption as NoSuchOption 36 | from .exceptions import UsageError as UsageError 37 | from .formatting import HelpFormatter as HelpFormatter 38 | from .formatting import wrap_text as wrap_text 39 | from .globals import get_current_context as get_current_context 40 | from .termui import clear as clear 41 | from .termui import confirm as confirm 42 | from .termui import echo_via_pager as echo_via_pager 43 | from .termui import edit as edit 44 | from .termui import getchar as getchar 45 | from .termui import launch as launch 46 | from .termui import pause as pause 47 | from .termui import progressbar as progressbar 48 | from .termui import prompt as prompt 49 | from .termui import secho as secho 50 | from .termui import style as style 51 | from .termui import unstyle as unstyle 52 | from .types import BOOL as BOOL 53 | from .types import Choice as Choice 54 | from .types import DateTime as DateTime 55 | from .types import File as File 56 | from .types import FLOAT as FLOAT 57 | from .types import FloatRange as FloatRange 58 | from .types import INT as INT 59 | from .types import IntRange as IntRange 60 | from .types import ParamType as ParamType 61 | from .types import Path as Path 62 | from .types import STRING as STRING 63 | from .types import Tuple as Tuple 64 | from .types import UNPROCESSED as UNPROCESSED 65 | from .types import UUID as UUID 66 | from .utils import echo as echo 67 | from .utils import format_filename as format_filename 68 | from .utils import get_app_dir as get_app_dir 69 | from .utils import get_binary_stream as get_binary_stream 70 | from .utils import get_text_stream as get_text_stream 71 | from .utils import open_file as open_file 72 | 73 | 74 | def __getattr__(name: str) -> object: 75 | import warnings 76 | 77 | if name == "BaseCommand": 78 | from .core import _BaseCommand 79 | 80 | warnings.warn( 81 | "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" 82 | " 'Command' instead.", 83 | DeprecationWarning, 84 | stacklevel=2, 85 | ) 86 | return _BaseCommand 87 | 88 | if name == "MultiCommand": 89 | from .core import _MultiCommand 90 | 91 | warnings.warn( 92 | "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" 93 | " 'Group' instead.", 94 | DeprecationWarning, 95 | stacklevel=2, 96 | ) 97 | return _MultiCommand 98 | 99 | if name == "OptionParser": 100 | from .parser import _OptionParser 101 | 102 | warnings.warn( 103 | "'OptionParser' is deprecated and will be removed in Click 9.0. The" 104 | " old parser is available in 'optparse'.", 105 | DeprecationWarning, 106 | stacklevel=2, 107 | ) 108 | return _OptionParser 109 | 110 | if name == "__version__": 111 | import importlib.metadata 112 | import warnings 113 | 114 | warnings.warn( 115 | "The '__version__' attribute is deprecated and will be removed in" 116 | " Click 9.1. Use feature detection or" 117 | " 'importlib.metadata.version(\"click\")' instead.", 118 | DeprecationWarning, 119 | stacklevel=2, 120 | ) 121 | return importlib.metadata.version("click") 122 | 123 | raise AttributeError(name) 124 | -------------------------------------------------------------------------------- /examples/repo/repo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import posixpath 3 | import sys 4 | 5 | import asyncclick as click 6 | 7 | 8 | class Repo: 9 | def __init__(self, home): 10 | self.home = home 11 | self.config = {} 12 | self.verbose = False 13 | 14 | def set_config(self, key, value): 15 | self.config[key] = value 16 | if self.verbose: 17 | click.echo(f" config[{key}] = {value}", file=sys.stderr) 18 | 19 | def __repr__(self): 20 | return f"" 21 | 22 | 23 | pass_repo = click.make_pass_decorator(Repo) 24 | 25 | 26 | @click.group() 27 | @click.option( 28 | "--repo-home", 29 | envvar="REPO_HOME", 30 | default=".repo", 31 | metavar="PATH", 32 | help="Changes the repository folder location.", 33 | ) 34 | @click.option( 35 | "--config", 36 | nargs=2, 37 | multiple=True, 38 | metavar="KEY VALUE", 39 | help="Overrides a config key/value pair.", 40 | ) 41 | @click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode.") 42 | @click.version_option("1.0") 43 | @click.pass_context 44 | def cli(ctx, repo_home, config, verbose): 45 | """Repo is a command line tool that showcases how to build complex 46 | command line interfaces with Click. 47 | 48 | This tool is supposed to look like a distributed version control 49 | system to show how something like this can be structured. 50 | """ 51 | # Create a repo object and remember it as as the context object. From 52 | # this point onwards other commands can refer to it by using the 53 | # @pass_repo decorator. 54 | ctx.obj = Repo(os.path.abspath(repo_home)) 55 | ctx.obj.verbose = verbose 56 | for key, value in config: 57 | ctx.obj.set_config(key, value) 58 | 59 | 60 | @cli.command() 61 | @click.argument("src") 62 | @click.argument("dest", required=False) 63 | @click.option( 64 | "--shallow/--deep", 65 | default=False, 66 | help="Makes a checkout shallow or deep. Deep by default.", 67 | ) 68 | @click.option( 69 | "--rev", "-r", default="HEAD", help="Clone a specific revision instead of HEAD." 70 | ) 71 | @pass_repo 72 | def clone(repo, src, dest, shallow, rev): 73 | """Clones a repository. 74 | 75 | This will clone the repository at SRC into the folder DEST. If DEST 76 | is not provided this will automatically use the last path component 77 | of SRC and create that folder. 78 | """ 79 | if dest is None: 80 | dest = posixpath.split(src)[-1] or "." 81 | click.echo(f"Cloning repo {src} to {os.path.basename(dest)}") 82 | repo.home = dest 83 | if shallow: 84 | click.echo("Making shallow checkout") 85 | click.echo(f"Checking out revision {rev}") 86 | 87 | 88 | @cli.command() 89 | @click.confirmation_option() 90 | @pass_repo 91 | def delete(repo): 92 | """Deletes a repository. 93 | 94 | This will throw away the current repository. 95 | """ 96 | click.echo(f"Destroying repo {repo.home}") 97 | click.echo("Deleted!") 98 | 99 | 100 | @cli.command() 101 | @click.option("--username", prompt=True, help="The developer's shown username.") 102 | @click.option("--email", prompt="E-Mail", help="The developer's email address") 103 | @click.password_option(help="The login password.") 104 | @pass_repo 105 | def setuser(repo, username, email, password): 106 | """Sets the user credentials. 107 | 108 | This will override the current user config. 109 | """ 110 | repo.set_config("username", username) 111 | repo.set_config("email", email) 112 | repo.set_config("password", "*" * len(password)) 113 | click.echo("Changed credentials.") 114 | 115 | 116 | @cli.command() 117 | @click.option( 118 | "--message", 119 | "-m", 120 | multiple=True, 121 | help="The commit message. If provided multiple times each" 122 | " argument gets converted into a new line.", 123 | ) 124 | @click.argument("files", nargs=-1, type=click.Path()) 125 | @pass_repo 126 | def commit(repo, files, message): 127 | """Commits outstanding changes. 128 | 129 | Commit changes to the given files into the repository. You will need to 130 | "repo push" to push up your changes to other repositories. 131 | 132 | If a list of files is omitted, all changes reported by "repo status" 133 | will be committed. 134 | """ 135 | if not message: 136 | marker = "# Files to be committed:" 137 | hint = ["", "", marker, "#"] 138 | for file in files: 139 | hint.append(f"# U {file}") 140 | message = click.edit("\n".join(hint)) 141 | if message is None: 142 | click.echo("Aborted!") 143 | return 144 | msg = message.split(marker)[0].rstrip() 145 | if not msg: 146 | click.echo("Aborted! Empty commit message") 147 | return 148 | else: 149 | msg = "\n".join(message) 150 | click.echo(f"Files to be committed: {files}") 151 | click.echo(f"Commit message:\n{msg}") 152 | 153 | 154 | @cli.command(short_help="Copies files.") 155 | @click.option( 156 | "--force", is_flag=True, help="forcibly copy over an existing managed file" 157 | ) 158 | @click.argument("src", nargs=-1, type=click.Path()) 159 | @click.argument("dst", type=click.Path()) 160 | @pass_repo 161 | def copy(repo, src, dst, force): 162 | """Copies one or multiple files to a new location. This copies all 163 | files from SRC to DST. 164 | """ 165 | for fn in src: 166 | click.echo(f"Copy from {fn} -> {dst}") 167 | -------------------------------------------------------------------------------- /docs/arguments.rst: -------------------------------------------------------------------------------- 1 | .. _arguments: 2 | 3 | Arguments 4 | ========= 5 | 6 | .. currentmodule:: click 7 | 8 | Arguments are: 9 | 10 | * Are positional in nature. 11 | * Similar to a limited version of :ref:`options ` that can take an arbitrary number of inputs 12 | * :ref:`Documented manually `. 13 | 14 | Useful and often used kwargs are: 15 | 16 | * ``default``: Passes a default. 17 | * ``nargs``: Sets the number of arguments. Set to -1 to take an arbitrary number. 18 | 19 | Basic Arguments 20 | --------------- 21 | 22 | A minimal :class:`click.Argument` solely takes one string argument: the name of the argument. This will assume the argument is required, has no default, and is of the type ``str``. 23 | 24 | Example: 25 | 26 | .. click:example:: 27 | 28 | @click.command() 29 | @click.argument('filename') 30 | def touch(filename: str): 31 | """Print FILENAME.""" 32 | click.echo(filename) 33 | 34 | And from the command line: 35 | 36 | .. click:run:: 37 | 38 | invoke(touch, args=['foo.txt']) 39 | 40 | 41 | An argument may be assigned a :ref:`parameter type `. If no type is provided, the type of the default value is used. If no default value is provided, the type is assumed to be :data:`STRING`. 42 | 43 | .. admonition:: Note on Required Arguments 44 | 45 | It is possible to make an argument required by setting ``required=True``. It is not recommended since we think command line tools should gracefully degrade into becoming no ops. We think this because command line tools are often invoked with wildcard inputs and they should not error out if the wildcard is empty. 46 | 47 | Multiple Arguments 48 | ----------------------------------- 49 | 50 | To set the number of argument use the ``nargs`` kwarg. It can be set to any positive integer and -1. Setting it to -1, makes the number of arguments arbitrary (which is called variadic) and can only be used once. The arguments are then packed as a tuple and passed to the function. 51 | 52 | .. click:example:: 53 | 54 | @click.command() 55 | @click.argument('src', nargs=1) 56 | @click.argument('dsts', nargs=-1) 57 | def copy(src: str, dsts: tuple[str, ...]): 58 | """Move file SRC to DST.""" 59 | for destination in dsts: 60 | click.echo(f"Copy {src} to folder {destination}") 61 | 62 | And from the command line: 63 | 64 | .. click:run:: 65 | 66 | invoke(copy, args=['foo.txt', 'usr/david/foo.txt', 'usr/mitsuko/foo.txt']) 67 | 68 | .. admonition:: Note on Handling Files 69 | 70 | This is not how you should handle files and files paths. This merely used as a simple example. See :ref:`handling-files` to learn more about how to handle files in parameters. 71 | 72 | Argument Escape Sequences 73 | --------------------------- 74 | 75 | If you want to process arguments that look like options, like a file named ``-foo.txt`` or ``--foo.txt`` , you must pass the ``--`` separator first. After you pass the ``--``, you may only pass arguments. This is a common feature for POSIX command line tools. 76 | 77 | Example usage: 78 | 79 | .. click:example:: 80 | 81 | @click.command() 82 | @click.argument('files', nargs=-1, type=click.Path()) 83 | def touch(files): 84 | """Print all FILES file names.""" 85 | for filename in files: 86 | click.echo(filename) 87 | 88 | And from the command line: 89 | 90 | .. click:run:: 91 | 92 | invoke(touch, ['--', '-foo.txt', 'bar.txt']) 93 | 94 | If you don't like the ``--`` marker, you can set ignore_unknown_options to True to avoid checking unknown options: 95 | 96 | .. click:example:: 97 | 98 | @click.command(context_settings={"ignore_unknown_options": True}) 99 | @click.argument('files', nargs=-1, type=click.Path()) 100 | def touch(files): 101 | """Print all FILES file names.""" 102 | for filename in files: 103 | click.echo(filename) 104 | 105 | And from the command line: 106 | 107 | .. click:run:: 108 | 109 | invoke(touch, ['-foo.txt', 'bar.txt']) 110 | 111 | 112 | .. _environment-variables: 113 | 114 | Environment Variables 115 | --------------------- 116 | 117 | Arguments can use environment variables. To do so, pass the name(s) of the environment variable(s) via `envvar` in ``click.argument``. 118 | 119 | Checking one environment variable: 120 | 121 | .. click:example:: 122 | 123 | @click.command() 124 | @click.argument('src', envvar='SRC', type=click.File('r')) 125 | def echo(src): 126 | """Print value of SRC environment variable.""" 127 | click.echo(src.read()) 128 | 129 | And from the command line: 130 | 131 | .. click:run:: 132 | 133 | with isolated_filesystem(): 134 | # Writing the file in the filesystem. 135 | with open('hello.txt', 'w') as f: 136 | f.write('Hello World!') 137 | invoke(echo, env={'SRC': 'hello.txt'}) 138 | 139 | 140 | Checking multiple environment variables: 141 | 142 | .. click:example:: 143 | 144 | @click.command() 145 | @click.argument('src', envvar=['SRC', 'SRC_2'], type=click.File('r')) 146 | def echo(src): 147 | """Print value of SRC environment variable.""" 148 | click.echo(src.read()) 149 | 150 | And from the command line: 151 | 152 | .. click:run:: 153 | 154 | with isolated_filesystem(): 155 | # Writing the file in the filesystem. 156 | with open('hello.txt', 'w') as f: 157 | f.write('Hello World from second variable!') 158 | invoke(echo, env={'SRC_2': 'hello.txt'}) 159 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing Click Applications 2 | 3 | ```{eval-rst} 4 | .. currentmodule:: click.testing 5 | ``` 6 | 7 | Click provides the {ref}`click.testing ` module to help you invoke command line applications and check their behavior. 8 | 9 | These tools should only be used for testing since they change 10 | the entire interpreter state for simplicity. They are not thread-safe! 11 | 12 | The examples use [pytest](https://docs.pytest.org/en/stable/) style tests. 13 | 14 | ```{contents} 15 | :depth: 1 16 | :local: true 17 | ``` 18 | 19 | ## Basic Example 20 | 21 | The key pieces are: 22 | - {class}`CliRunner` - used to invoke commands as command line scripts. 23 | - {class}`Result` - returned from {meth}`CliRunner.invoke`. Captures output data, exit code, optional exception, and captures the output as bytes and binary data. 24 | 25 | ```{code-block} python 26 | :caption: hello.py 27 | 28 | import click 29 | 30 | @click.command() 31 | @click.argument('name') 32 | def hello(name): 33 | click.echo(f'Hello {name}!') 34 | ``` 35 | 36 | ```{code-block} python 37 | :caption: test_hello.py 38 | 39 | from click.testing import CliRunner 40 | from hello import hello 41 | 42 | def test_hello_world(): 43 | runner = CliRunner() 44 | result = runner.invoke(hello, ['Peter']) 45 | assert result.exit_code == 0 46 | assert result.output == 'Hello Peter!\n' 47 | ``` 48 | 49 | ## Subcommands 50 | 51 | A subcommand name must be specified in the `args` parameter {meth}`CliRunner.invoke`: 52 | 53 | ```{code-block} python 54 | :caption: sync.py 55 | 56 | import click 57 | 58 | @click.group() 59 | @click.option('--debug/--no-debug', default=False) 60 | def cli(debug): 61 | click.echo(f"Debug mode is {'on' if debug else 'off'}") 62 | 63 | @cli.command() 64 | def sync(): 65 | click.echo('Syncing') 66 | ``` 67 | 68 | ```{code-block} python 69 | :caption: test_sync.py 70 | 71 | from click.testing import CliRunner 72 | from sync import cli 73 | 74 | def test_sync(): 75 | runner = CliRunner() 76 | result = runner.invoke(cli, ['--debug', 'sync']) 77 | assert result.exit_code == 0 78 | assert 'Debug mode is on' in result.output 79 | assert 'Syncing' in result.output 80 | ``` 81 | 82 | ## Context Settings 83 | 84 | Additional keyword arguments passed to {meth}`CliRunner.invoke` will be used to construct the initial {class}`Context object `. 85 | For example, setting a fixed terminal width equal to 60: 86 | 87 | ```{code-block} python 88 | :caption: sync.py 89 | 90 | import click 91 | 92 | @click.group() 93 | def cli(): 94 | pass 95 | 96 | @cli.command() 97 | def sync(): 98 | click.echo('Syncing') 99 | ``` 100 | 101 | ```{code-block} python 102 | :caption: test_sync.py 103 | 104 | from click.testing import CliRunner 105 | from sync import cli 106 | 107 | def test_sync(): 108 | runner = CliRunner() 109 | result = runner.invoke(cli, ['sync'], terminal_width=60) 110 | assert result.exit_code == 0 111 | assert 'Debug mode is on' in result.output 112 | assert 'Syncing' in result.output 113 | ``` 114 | 115 | ## File System Isolation 116 | 117 | The {meth}`CliRunner.isolated_filesystem` context manager sets the current working directory to a new, empty folder. 118 | 119 | ```{code-block} python 120 | :caption: cat.py 121 | 122 | import click 123 | 124 | @click.command() 125 | @click.argument('f', type=click.File()) 126 | def cat(f): 127 | click.echo(f.read()) 128 | ``` 129 | 130 | ```{code-block} python 131 | :caption: test_cat.py 132 | 133 | from click.testing import CliRunner 134 | from cat import cat 135 | 136 | def test_cat(): 137 | runner = CliRunner() 138 | with runner.isolated_filesystem(): 139 | with open('hello.txt', 'w') as f: 140 | f.write('Hello World!') 141 | 142 | result = runner.invoke(cat, ['hello.txt']) 143 | assert result.exit_code == 0 144 | assert result.output == 'Hello World!\n' 145 | ``` 146 | 147 | Pass in a path to control where the temporary directory is created. 148 | In this case, the directory will not be removed by Click. Its useful 149 | to integrate with a framework like Pytest that manages temporary files. 150 | 151 | ```{code-block} python 152 | :caption: test_cat.py 153 | 154 | from click.testing import CliRunner 155 | from cat import cat 156 | 157 | def test_cat_with_path_specified(): 158 | runner = CliRunner() 159 | with runner.isolated_filesystem('~/test_folder'): 160 | with open('hello.txt', 'w') as f: 161 | f.write('Hello World!') 162 | 163 | result = runner.invoke(cat, ['hello.txt']) 164 | assert result.exit_code == 0 165 | assert result.output == 'Hello World!\n' 166 | ``` 167 | 168 | ## Input Streams 169 | 170 | The test wrapper can provide input data for the input stream (stdin). This is very useful for testing prompts. 171 | 172 | ```{code-block} python 173 | :caption: prompt.py 174 | 175 | import click 176 | 177 | @click.command() 178 | @click.option('--foo', prompt=True) 179 | def prompt(foo): 180 | click.echo(f"foo={foo}") 181 | ``` 182 | 183 | ```{code-block} python 184 | :caption: test_prompt.py 185 | 186 | from click.testing import CliRunner 187 | from prompt import prompt 188 | 189 | def test_prompts(): 190 | runner = CliRunner() 191 | result = runner.invoke(prompt, input='wau wau\n') 192 | assert not result.exception 193 | assert result.output == 'Foo: wau wau\nfoo=wau wau\n' 194 | ``` 195 | 196 | Prompts will be emulated so they write the input data to 197 | the output stream as well. If hidden input is expected then this 198 | does not happen. 199 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "asyncclick" 3 | description = "Composable command line interface toolkit" 4 | readme = "README.md" 5 | license = "BSD-3-Clause" 6 | license-files = ["LICENSE.txt"] 7 | maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] 8 | classifiers = [ 9 | "Development Status :: 5 - Production/Stable", 10 | "Intended Audience :: Developers", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python", 13 | "Typing :: Typed", 14 | ] 15 | requires-python = ">=3.10" 16 | dependencies = [ 17 | "colorama; platform_system == 'Windows'", 18 | ] 19 | version = "8.3.0.3" 20 | 21 | [project.urls] 22 | Donate = "https://palletsprojects.com/donate" 23 | Documentation = "https://click.palletsprojects.com/" 24 | Changes = "https://click.palletsprojects.com/page/changes/" 25 | Source = "https://github.com/pallets/click/" 26 | Chat = "https://discord.gg/pallets" 27 | 28 | [dependency-groups] 29 | dev = [ 30 | "ruff", 31 | "tox", 32 | "tox-uv", 33 | ] 34 | docs = [ 35 | "myst-parser", 36 | "pallets-sphinx-themes", 37 | "sphinx", 38 | "sphinx-tabs", 39 | "sphinxcontrib-log-cabinet", 40 | ] 41 | docs-auto = [ 42 | "sphinx-autobuild", 43 | ] 44 | gha-update = [ 45 | "gha-update ; python_full_version >= '3.12'", 46 | ] 47 | pre-commit = [ 48 | "pre-commit", 49 | "pre-commit-uv", 50 | ] 51 | tests = [ 52 | "anyio", 53 | "pytest", 54 | ] 55 | typing = [ 56 | "anyio", 57 | "mypy", 58 | "pyright", 59 | "pytest", 60 | ] 61 | anyio = [ 62 | "anyio ~= 4.0", 63 | ] 64 | trio = [ 65 | "trio ~= 0.22", 66 | ] 67 | 68 | [build-system] 69 | requires = ["flit_core<4"] 70 | build-backend = "flit_core.buildapi" 71 | 72 | [tool.flit.module] 73 | name = "asyncclick" 74 | 75 | [tool.flit.sdist] 76 | include = [ 77 | "docs/", 78 | "tests/", 79 | "CHANGES.rst", 80 | "uv.lock" 81 | ] 82 | exclude = [ 83 | "docs/_build/", 84 | ] 85 | 86 | [tool.uv] 87 | default-groups = ["dev", "pre-commit", "tests", "typing"] 88 | 89 | [tool.pytest.ini_options] 90 | testpaths = ["tests"] 91 | filterwarnings = [ 92 | "error", 93 | ] 94 | 95 | [tool.coverage.run] 96 | branch = true 97 | source = ["asyncclick", "tests"] 98 | 99 | [tool.coverage.paths] 100 | source = ["src", "*/site-packages"] 101 | 102 | [tool.coverage.report] 103 | exclude_also = [ 104 | "if t.TYPE_CHECKING", 105 | "raise NotImplementedError", 106 | ": \\.{3}", 107 | ] 108 | 109 | [tool.mypy] 110 | python_version = "3.10" 111 | files = ["src", "tests/typing"] 112 | show_error_codes = true 113 | pretty = true 114 | strict = true 115 | 116 | [[tool.mypy.overrides]] 117 | module = [ 118 | "colorama.*", 119 | ] 120 | ignore_missing_imports = true 121 | 122 | [tool.pyright] 123 | pythonVersion = "3.10" 124 | include = ["src", "tests/typing"] 125 | typeCheckingMode = "basic" 126 | 127 | [tool.ruff] 128 | extend-exclude = ["examples/"] 129 | src = ["src"] 130 | fix = true 131 | show-fixes = true 132 | output-format = "full" 133 | 134 | [tool.ruff.lint] 135 | select = [ 136 | "B", # flake8-bugbear 137 | "E", # pycodestyle error 138 | "F", # pyflakes 139 | "I", # isort 140 | "UP", # pyupgrade 141 | "W", # pycodestyle warning 142 | ] 143 | ignore = [ 144 | "UP038", # keep isinstance tuple 145 | ] 146 | 147 | [tool.ruff.lint.isort] 148 | force-single-line = true 149 | order-by-type = false 150 | 151 | [tool.tox] 152 | env_list = [ 153 | "py3.13", "py3.12", "py3.11", "py3.10", 154 | "pypy3.11", 155 | "style", 156 | "typing", 157 | "docs", 158 | ] 159 | 160 | [tool.tox.env_run_base] 161 | description = "pytest on latest dependency versions" 162 | runner = "uv-venv-lock-runner" 163 | package = "wheel" 164 | wheel_build_env = ".pkg" 165 | constrain_package_deps = true 166 | use_frozen_constraints = true 167 | dependency_groups = ["tests"] 168 | commands = [[ 169 | "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", 170 | {replace = "posargs", default = [], extend = true}, 171 | ]] 172 | 173 | [tool.tox.env.style] 174 | description = "run all pre-commit hooks on all files" 175 | dependency_groups = ["pre-commit"] 176 | skip_install = true 177 | commands = [["pre-commit", "run", "--all-files"]] 178 | 179 | [tool.tox.env.typing] 180 | description = "run static type checkers" 181 | dependency_groups = ["typing"] 182 | commands = [ 183 | ["mypy"], 184 | ] 185 | 186 | [tool.tox.env.docs] 187 | description = "build docs" 188 | dependency_groups = ["docs"] 189 | commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]] 190 | 191 | [tool.tox.env.docs-auto] 192 | description = "continuously rebuild docs and start a local server" 193 | dependency_groups = ["docs", "docs-auto"] 194 | commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]] 195 | 196 | [tool.tox.env.update-actions] 197 | description = "update GitHub Actions pins" 198 | labels = ["update"] 199 | dependency_groups = ["gha-update"] 200 | skip_install = true 201 | commands = [["gha-update"]] 202 | 203 | [tool.tox.env.update-pre_commit] 204 | description = "update pre-commit pins" 205 | labels = ["update"] 206 | dependency_groups = ["pre-commit"] 207 | skip_install = true 208 | commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]] 209 | 210 | [tool.tox.env.update-requirements] 211 | description = "update uv lock" 212 | labels = ["update"] 213 | dependency_groups = [] 214 | no_default_groups = true 215 | skip_install = true 216 | commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]] 217 | -------------------------------------------------------------------------------- /docs/prompts.md: -------------------------------------------------------------------------------- 1 | # User Input Prompts 2 | 3 | ```{currentmodule} click 4 | ``` 5 | 6 | Click supports prompts in two different places. The first is automated prompts when the parameter handling happens, and 7 | the second is to ask for prompts at a later point independently. 8 | 9 | This can be accomplished with the {func}`prompt` function, which asks for valid input according to a type, or the 10 | {func}`confirm` function, which asks for confirmation (yes/no). 11 | 12 | ```{contents} 13 | --- 14 | depth: 2 15 | local: true 16 | --- 17 | ``` 18 | 19 | ## AsyncClick changes 20 | 21 | AsyncClick now async-izes :func:`asyncclick.prompt`. This may require changes 22 | in your program. We are sorry to have to do this in a minor release, but there 23 | is no way around this change because the validation callback might be async. 24 | 25 | On the positive side, :func:`asyncclick.prompt` gained a new parameter ``blocking``. 26 | When it is set to `False`, interaction with the user no longer blocks the async 27 | event loop. 28 | 29 | However, this currently interacts badly with interrupting the program, e.g. by 30 | pressing Control-C. As most programs prompt the user first and run async tasks later, 31 | the default for ``blocking`` is `True` until we can solve the interrupt problem. 32 | 33 | 34 | (option-prompting)= 35 | 36 | ## Option Prompts 37 | 38 | Option prompts are integrated into the option interface. Internally, it automatically calls either {func}`prompt` or 39 | {func}`confirm` as necessary. 40 | 41 | In some cases, you want parameters that can be provided from the command line, but if not provided, ask for user input 42 | instead. This can be implemented with Click by defining a prompt string. 43 | 44 | Example: 45 | 46 | ```{eval-rst} 47 | .. click:example:: 48 | 49 | @click.command() 50 | @click.option('--name', prompt=True) 51 | def hello(name): 52 | click.echo(f"Hello {name}!") 53 | 54 | And what it looks like: 55 | 56 | .. click:run:: 57 | 58 | invoke(hello, args=['--name=John']) 59 | invoke(hello, input=['John']) 60 | ``` 61 | 62 | If you are not happy with the default prompt string, you can ask for 63 | a different one: 64 | 65 | ```{eval-rst} 66 | .. click:example:: 67 | 68 | @click.command() 69 | @click.option('--name', prompt='Your name please') 70 | def hello(name): 71 | click.echo(f"Hello {name}!") 72 | 73 | What it looks like: 74 | 75 | .. click:run:: 76 | 77 | invoke(hello, input=['John']) 78 | ``` 79 | 80 | It is advised that prompt not be used in conjunction with the multiple flag set to True. Instead, prompt in the function 81 | interactively. 82 | 83 | By default, the user will be prompted for an input if one was not passed through the command line. To turn this behavior 84 | off, see {ref}`optional-value`. 85 | 86 | ## Input Prompts 87 | 88 | To manually ask for user input, you can use the {func}`prompt` function. By default, it accepts any Unicode string, but 89 | you can ask for any other type. For instance, you can ask for a valid integer: 90 | 91 | ```python 92 | value = await click.prompt('Please enter a valid integer', type=int) 93 | ``` 94 | 95 | Additionally, the type will be determined automatically if a default value is provided. For instance, the following will 96 | only accept floats: 97 | 98 | ```python 99 | value = await click.prompt('Please enter a number', default=42.0) 100 | ``` 101 | 102 | ## Optional Prompts 103 | 104 | If the option has `prompt` enabled, then setting `prompt_required=False` tells Click to only show the prompt if the 105 | option's flag is given, instead of if the option is not provided at all. 106 | 107 | ```{eval-rst} 108 | .. click:example:: 109 | 110 | @click.command() 111 | @click.option('--name', prompt=True, prompt_required=False, default="Default") 112 | def hello(name): 113 | click.echo(f"Hello {name}!") 114 | 115 | .. click:run:: 116 | 117 | invoke(hello) 118 | invoke(hello, args=["--name", "Value"]) 119 | invoke(hello, args=["--name"], input="Prompt") 120 | ``` 121 | 122 | If `required=True`, then the option will still prompt if it is not given, but it will also prompt if only the flag is 123 | given. 124 | 125 | ## Confirmation Prompts 126 | 127 | To ask if a user wants to continue with an action, the {func}`confirm` function comes in handy. By default, it returns 128 | the result of the prompt as a boolean value: 129 | 130 | ```python 131 | if click.confirm('Do you want to continue?'): 132 | click.echo('Well done!') 133 | ``` 134 | 135 | There is also the option to make the function automatically abort the execution of the program if it does not return 136 | `True`: 137 | 138 | ```python 139 | click.confirm('Do you want to continue?', abort=True) 140 | ``` 141 | 142 | ## Dynamic Defaults for Prompts 143 | 144 | The `auto_envvar_prefix` and `default_map` options for the context allow the program to read option values from the 145 | environment or a configuration file. However, this overrides the prompting mechanism, so that the user does not get the 146 | option to change the value interactively. 147 | 148 | If you want to let the user configure the default value, but still be prompted if the option isn't specified on the 149 | command line, you can do so by supplying a callable as the default value. For example, to get a default from the 150 | environment: 151 | 152 | ```python 153 | import os 154 | 155 | @click.command() 156 | @click.option( 157 | "--username", prompt=True, 158 | default=lambda: os.environ.get("USER", "") 159 | ) 160 | def hello(username): 161 | click.echo(f"Hello, {username}!") 162 | ``` 163 | 164 | To describe what the default value will be, set it in ``show_default``. 165 | 166 | ```{eval-rst} 167 | .. click:example:: 168 | 169 | import os 170 | 171 | @click.command() 172 | @click.option( 173 | "--username", prompt=True, 174 | default=lambda: os.environ.get("USER", ""), 175 | show_default="current user" 176 | ) 177 | def hello(username): 178 | click.echo(f"Hello, {username}!") 179 | 180 | .. click:run:: 181 | 182 | invoke(hello, args=["--help"]) 183 | ``` 184 | -------------------------------------------------------------------------------- /docs/parameter-types.rst: -------------------------------------------------------------------------------- 1 | .. _parameter-types: 2 | 3 | Parameter Types 4 | ================== 5 | 6 | .. currentmodule:: click 7 | 8 | When the parameter type is set using ``type``, Click will leverage the type to make your life easier, for example adding data to your help pages. Most examples are done with options, but types are available to options and arguments. 9 | 10 | .. contents:: 11 | :depth: 2 12 | :local: 13 | 14 | Built-in Types Examples 15 | ------------------------ 16 | 17 | .. _choice-opts: 18 | 19 | Choice 20 | ^^^^^^^^^^^^^^^^^^^^^^ 21 | 22 | Sometimes, you want to have a parameter be a choice of a list of values. 23 | In that case you can use :class:`Choice` type. It can be instantiated 24 | with a list of valid values. The originally passed choice will be returned, 25 | not the str passed on the command line. Token normalization functions and 26 | ``case_sensitive=False`` can cause the two to be different but still match. 27 | :meth:`Choice.normalize_choice` for more info. 28 | 29 | Example: 30 | 31 | .. click:example:: 32 | 33 | import enum 34 | 35 | class HashType(enum.Enum): 36 | MD5 = enum.auto() 37 | SHA1 = enum.auto() 38 | 39 | @click.command() 40 | @click.option('--hash-type', 41 | type=click.Choice(HashType, case_sensitive=False)) 42 | def digest(hash_type: HashType): 43 | click.echo(hash_type) 44 | 45 | What it looks like: 46 | 47 | .. click:run:: 48 | 49 | invoke(digest, args=['--hash-type=MD5']) 50 | println() 51 | invoke(digest, args=['--hash-type=md5']) 52 | println() 53 | invoke(digest, args=['--hash-type=foo']) 54 | println() 55 | invoke(digest, args=['--help']) 56 | 57 | Any iterable may be passed to :class:`Choice`. If an ``Enum`` is passed, the 58 | names of the enum members will be used as valid choices. 59 | 60 | Choices work with options that have ``multiple=True``. If a ``default`` 61 | value is given with ``multiple=True``, it should be a list or tuple of 62 | valid choices. 63 | 64 | Choices should be unique after normalization, see 65 | :meth:`Choice.normalize_choice` for more info. 66 | 67 | .. versionchanged:: 7.1 68 | The resulting value from an option will always be one of the 69 | originally passed choices regardless of ``case_sensitive``. 70 | 71 | .. _ranges: 72 | 73 | Int and Float Ranges 74 | ^^^^^^^^^^^^^^^^^^^^^^^ 75 | 76 | The :class:`IntRange` type extends the :data:`INT` type to ensure the 77 | value is contained in the given range. The :class:`FloatRange` type does 78 | the same for :data:`FLOAT`. 79 | 80 | If ``min`` or ``max`` is omitted, that side is *unbounded*. Any value in 81 | that direction is accepted. By default, both bounds are *closed*, which 82 | means the boundary value is included in the accepted range. ``min_open`` 83 | and ``max_open`` can be used to exclude that boundary from the range. 84 | 85 | If ``clamp`` mode is enabled, a value that is outside the range is set 86 | to the boundary instead of failing. For example, the range ``0, 5`` 87 | would return ``5`` for the value ``10``, or ``0`` for the value ``-1``. 88 | When using :class:`FloatRange`, ``clamp`` can only be enabled if both 89 | bounds are *closed* (the default). 90 | 91 | .. click:example:: 92 | 93 | @click.command() 94 | @click.option("--count", type=click.IntRange(0, 20, clamp=True)) 95 | @click.option("--digit", type=click.IntRange(0, 9)) 96 | def repeat(count, digit): 97 | click.echo(str(digit) * count) 98 | 99 | .. click:run:: 100 | 101 | invoke(repeat, args=['--count=100', '--digit=5']) 102 | invoke(repeat, args=['--count=6', '--digit=12']) 103 | 104 | 105 | Built-in Types Listing 106 | ----------------------- 107 | The supported parameter :ref:`click-api-types` are: 108 | 109 | * ``str`` / :data:`click.STRING`: The default parameter type which indicates unicode strings. 110 | 111 | * ``int`` / :data:`click.INT`: A parameter that only accepts integers. 112 | 113 | * ``float`` / :data:`click.FLOAT`: A parameter that only accepts floating point values. 114 | 115 | * ``bool`` / :data:`click.BOOL`: A parameter that accepts boolean values. This is automatically used 116 | for boolean flags. The string values "1", "true", "t", "yes", "y", 117 | and "on" convert to ``True``. "0", "false", "f", "no", "n", and 118 | "off" convert to ``False``. 119 | 120 | * :data:`click.UUID`: 121 | A parameter that accepts UUID values. This is not automatically 122 | guessed but represented as :class:`uuid.UUID`. 123 | 124 | * .. autoclass:: Choice 125 | :noindex: 126 | 127 | * .. autoclass:: DateTime 128 | :noindex: 129 | 130 | * .. autoclass:: File 131 | :noindex: 132 | 133 | * .. autoclass:: FloatRange 134 | :noindex: 135 | 136 | * .. autoclass:: IntRange 137 | :noindex: 138 | 139 | * .. autoclass:: Path 140 | :noindex: 141 | 142 | How to Implement Custom Types 143 | ------------------------------- 144 | 145 | To implement a custom type, you need to subclass the :class:`ParamType` class. For simple cases, passing a Python function that fails with a `ValueError` is also supported, though discouraged. Override the :meth:`~ParamType.convert` method to convert the value from a string to the correct type. 146 | 147 | The following code implements an integer type that accepts hex and octal 148 | numbers in addition to normal integers, and converts them into regular 149 | integers. 150 | 151 | .. code-block:: python 152 | 153 | import click 154 | 155 | class BasedIntParamType(click.ParamType): 156 | name = "integer" 157 | 158 | def convert(self, value, param, ctx): 159 | if isinstance(value, int): 160 | return value 161 | 162 | try: 163 | if value[:2].lower() == "0x": 164 | return int(value[2:], 16) 165 | elif value[:1] == "0": 166 | return int(value, 8) 167 | return int(value, 10) 168 | except ValueError: 169 | self.fail(f"{value!r} is not a valid integer", param, ctx) 170 | 171 | BASED_INT = BasedIntParamType() 172 | 173 | The :attr:`~ParamType.name` attribute is optional and is used for 174 | documentation. Call :meth:`~ParamType.fail` if conversion fails. The 175 | ``param`` and ``ctx`` arguments may be ``None`` in some cases such as 176 | prompts. 177 | 178 | Values from user input or the command line will be strings, but default 179 | values and Python arguments may already be the correct type. The custom 180 | type should check at the top if the value is already valid and pass it 181 | through to support those cases. 182 | -------------------------------------------------------------------------------- /tests/test_chain.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | import asyncclick as click 6 | 7 | 8 | def debug(): 9 | click.echo( 10 | f"{sys._getframe(1).f_code.co_name}" 11 | f"={'|'.join(click.get_current_context().args)}" 12 | ) 13 | 14 | 15 | def test_basic_chaining(runner): 16 | @click.group(chain=True) 17 | def cli(): 18 | pass 19 | 20 | @cli.command("sdist") 21 | def sdist(): 22 | click.echo("sdist called") 23 | 24 | @cli.command("bdist") 25 | def bdist(): 26 | click.echo("bdist called") 27 | 28 | result = runner.invoke(cli, ["bdist", "sdist", "bdist"]) 29 | assert not result.exception 30 | assert result.output.splitlines() == [ 31 | "bdist called", 32 | "sdist called", 33 | "bdist called", 34 | ] 35 | 36 | 37 | @pytest.mark.parametrize( 38 | ("args", "expect"), 39 | [ 40 | (["--help"], "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."), 41 | (["--help"], "ROOT HELP"), 42 | (["sdist", "--help"], "SDIST HELP"), 43 | (["bdist", "--help"], "BDIST HELP"), 44 | (["bdist", "sdist", "--help"], "SDIST HELP"), 45 | ], 46 | ) 47 | def test_chaining_help(runner, args, expect): 48 | @click.group(chain=True) 49 | def cli(): 50 | """ROOT HELP""" 51 | pass 52 | 53 | @cli.command("sdist") 54 | def sdist(): 55 | """SDIST HELP""" 56 | click.echo("sdist called") 57 | 58 | @cli.command("bdist") 59 | def bdist(): 60 | """BDIST HELP""" 61 | click.echo("bdist called") 62 | 63 | result = runner.invoke(cli, args) 64 | assert not result.exception 65 | assert expect in result.output 66 | 67 | 68 | def test_chaining_with_options(runner): 69 | @click.group(chain=True) 70 | def cli(): 71 | pass 72 | 73 | @cli.command("sdist") 74 | @click.option("--format") 75 | def sdist(format): 76 | click.echo(f"sdist called {format}") 77 | 78 | @cli.command("bdist") 79 | @click.option("--format") 80 | def bdist(format): 81 | click.echo(f"bdist called {format}") 82 | 83 | result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"]) 84 | assert not result.exception 85 | assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] 86 | 87 | 88 | @pytest.mark.parametrize(("chain", "expect"), [(False, "1"), (True, "[]")]) 89 | def test_no_command_result_callback(runner, chain, expect): 90 | """When a group has ``invoke_without_command=True``, the result 91 | callback is always invoked. A regular group invokes it with 92 | its return value, a chained group with ``[]``. 93 | """ 94 | 95 | @click.group(invoke_without_command=True, chain=chain) 96 | def cli(): 97 | return 1 98 | 99 | @cli.result_callback() 100 | def process_result(result): 101 | click.echo(result, nl=False) 102 | 103 | result = runner.invoke(cli, []) 104 | assert result.output == expect 105 | 106 | 107 | def test_chaining_with_arguments(runner): 108 | @click.group(chain=True) 109 | def cli(): 110 | pass 111 | 112 | @cli.command("sdist") 113 | @click.argument("format") 114 | def sdist(format): 115 | click.echo(f"sdist called {format}") 116 | 117 | @cli.command("bdist") 118 | @click.argument("format") 119 | def bdist(format): 120 | click.echo(f"bdist called {format}") 121 | 122 | result = runner.invoke(cli, ["bdist", "1", "sdist", "2"]) 123 | assert not result.exception 124 | assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] 125 | 126 | 127 | @pytest.mark.parametrize( 128 | ("args", "input", "expect"), 129 | [ 130 | (["-f", "-"], "foo\nbar", ["foo", "bar"]), 131 | (["-f", "-", "strip"], "foo \n bar", ["foo", "bar"]), 132 | (["-f", "-", "strip", "uppercase"], "foo \n bar", ["FOO", "BAR"]), 133 | ], 134 | ) 135 | def test_pipeline(runner, args, input, expect): 136 | @click.group(chain=True, invoke_without_command=True) 137 | @click.option("-f", type=click.File("r")) 138 | def cli(f): 139 | pass 140 | 141 | @cli.result_callback() 142 | def process_pipeline(processors, f): 143 | iterator = (x.rstrip("\r\n") for x in f) 144 | for processor in processors: 145 | iterator = processor(iterator) 146 | for item in iterator: 147 | click.echo(item) 148 | 149 | @cli.command("uppercase") 150 | def make_uppercase(): 151 | def processor(iterator): 152 | for line in iterator: 153 | yield line.upper() 154 | 155 | return processor 156 | 157 | @cli.command("strip") 158 | def make_strip(): 159 | def processor(iterator): 160 | for line in iterator: 161 | yield line.strip() 162 | 163 | return processor 164 | 165 | result = runner.invoke(cli, args, input=input) 166 | assert not result.exception 167 | assert result.output.splitlines() == expect 168 | 169 | 170 | def test_args_and_chain(runner): 171 | @click.group(chain=True) 172 | def cli(): 173 | debug() 174 | 175 | @cli.command() 176 | def a(): 177 | debug() 178 | 179 | @cli.command() 180 | def b(): 181 | debug() 182 | 183 | @cli.command() 184 | def c(): 185 | debug() 186 | 187 | result = runner.invoke(cli, ["a", "b", "c"]) 188 | assert not result.exception 189 | assert result.output.splitlines() == ["cli=", "a=", "b=", "c="] 190 | 191 | 192 | def test_group_arg_behavior(runner): 193 | with pytest.raises(RuntimeError): 194 | 195 | @click.group(chain=True) 196 | @click.argument("forbidden", required=False) 197 | def bad_cli(): 198 | pass 199 | 200 | with pytest.raises(RuntimeError): 201 | 202 | @click.group(chain=True) 203 | @click.argument("forbidden", nargs=-1) 204 | def bad_cli2(): 205 | pass 206 | 207 | @click.group(chain=True) 208 | @click.argument("arg") 209 | def cli(arg): 210 | click.echo(f"cli:{arg}") 211 | 212 | @cli.command() 213 | def a(): 214 | click.echo("a") 215 | 216 | result = runner.invoke(cli, ["foo", "a"]) 217 | assert not result.exception 218 | assert result.output.splitlines() == ["cli:foo", "a"] 219 | 220 | 221 | @pytest.mark.xfail 222 | def test_group_chaining(runner): 223 | @click.group(chain=True) 224 | def cli(): 225 | debug() 226 | 227 | @cli.group() 228 | def l1a(): 229 | debug() 230 | 231 | @l1a.command() 232 | def l2a(): 233 | debug() 234 | 235 | @l1a.command() 236 | def l2b(): 237 | debug() 238 | 239 | @cli.command() 240 | def l1b(): 241 | debug() 242 | 243 | result = runner.invoke(cli, ["l1a", "l2a", "l1b"]) 244 | assert not result.exception 245 | assert result.output.splitlines() == ["cli=", "l1a=", "l2a=", "l1b="] 246 | -------------------------------------------------------------------------------- /docs/why.md: -------------------------------------------------------------------------------- 1 | # Why Click? 2 | 3 | There are so many libraries out there for writing command line utilities; why does Click exist? 4 | 5 | This question is easy to answer: because there is not a single command line utility for Python out there which ticks the 6 | following boxes: 7 | 8 | - Is lazily composable without restrictions. 9 | - Supports implementation of Unix/POSIX command line conventions. 10 | - Supports loading values from environment variables out of the box. 11 | - Support for prompting of custom values. 12 | - Is fully nestable and composable. 13 | - Supports file handling out of the box. 14 | - Comes with useful common helpers (getting terminal dimensions, ANSI colors, fetching direct keyboard input, screen 15 | clearing, finding config paths, launching apps and editors, etc.). 16 | 17 | There are many alternatives to Click; the obvious ones are `optparse` and `argparse` from the standard library. Have a 18 | look to see if something else resonates with you. 19 | 20 | Click actually implements its own parsing of arguments and does not use `optparse` or `argparse` following the 21 | `optparse` parsing behavior. The reason it's not based on `argparse` is that `argparse` does not allow proper nesting of 22 | commands by design and has some deficiencies when it comes to POSIX compliant argument handling. 23 | 24 | Click is designed to be fun and customizable but not overly flexible. For instance, the customizability of help pages is 25 | constrained. This constraint is intentional because Click promises multiple Click instances will continue to function as 26 | intended when strung together. 27 | 28 | Too much customizability would break this promise. 29 | 30 | Click was written to support the [Flask](https://palletsprojects.com/p/flask/) microframework ecosystem because no tool 31 | could provide it with the functionality it needed. 32 | 33 | To get an understanding of what Click is all about, I strongly recommend looking at the {ref}`complex-guide` chapter. 34 | 35 | ## Why not Argparse? 36 | 37 | Click is internally based on `optparse` instead of `argparse`. This is an implementation detail that a user does not 38 | have to be concerned with. Click is not based on `argparse` because it has some behaviors that make handling arbitrary 39 | command line interfaces hard: 40 | 41 | - `argparse` has built-in behavior to guess if something is an argument or an option. This becomes a problem when 42 | dealing with incomplete command lines; the behaviour becomes unpredictable without full knowledge of a command line. 43 | This goes against Click's ambitions of dispatching to subparsers. 44 | - `argparse` does not support disabling interspersed arguments. Without this feature, it's not possible to safely 45 | implement Click's nested parsing. 46 | 47 | ## Why not Docopt etc.? 48 | 49 | Docopt, and many tools like it, are cool in how they work, but very few of these tools deal with nesting of commands and 50 | composability in a way like Click. To the best of the developer's knowledge, Click is the first Python library that aims 51 | to create a level of composability of applications that goes beyond what the system itself supports. 52 | 53 | Docopt, for instance, acts by parsing your help pages and then parsing according to those rules. The side effect of this 54 | is that docopt is quite rigid in how it handles the command line interface. The upside of docopt is that it gives you 55 | strong control over your help page; the downside is that due to this it cannot rewrap your output for the current 56 | terminal width, and it makes translations hard. On top of that, docopt is restricted to basic parsing. It does not 57 | handle argument dispatching and callback invocation or types. This means there is a lot of code that needs to be written 58 | in addition to the basic help page to handle the parsing results. 59 | 60 | Most of all, however, it makes composability hard. While docopt does support dispatching to subcommands, it, for 61 | instance, does not directly support any kind of automatic subcommand enumeration based on what's available or it does 62 | not enforce subcommands to work in a consistent way. 63 | 64 | This is fine, but it's different from how Click wants to work. Click aims to support fully composable command line user 65 | interfaces by doing the following: 66 | 67 | - Click does not just parse, it also dispatches to the appropriate code. 68 | - Click has a strong concept of an invocation context that allows subcommands to respond to data from the parent 69 | command. 70 | - Click has strong information available for all parameters and commands, so it can generate unified help pages for the 71 | full CLI and assist the user in converting the input data as necessary. 72 | - Click has a strong understanding of what types are, and it can give the user consistent error messages if something 73 | goes wrong. A subcommand written by a different developer will not suddenly die with a different error message because 74 | it's manually handled. 75 | - Click has enough meta information available for its whole program to evolve over time and improve the user experience 76 | without forcing developers to adjust their programs. For instance, if Click decides to change how help pages are 77 | formatted, all Click programs will automatically benefit from this. 78 | 79 | The aim of Click is to make composable systems. Whereas, the aim of docopt is to build the most beautiful and 80 | hand-crafted command line interfaces. These two goals conflict with one another in subtle ways. Click actively prevents 81 | people from implementing certain patterns in order to achieve unified command line interfaces. For instance, as a 82 | developer, you are given very little choice in formatting your help pages. 83 | 84 | ## Why Hardcoded Behaviors? 85 | 86 | The other question is why Click goes away from optparse and hardcodes certain behaviors instead of staying configurable. 87 | There are multiple reasons for this. The biggest one is that too much configurability makes it hard to achieve a 88 | consistent command line experience. 89 | 90 | The best example for this is optparse's `callback` functionality for accepting an arbitrary number of arguments. Due to 91 | syntactical ambiguities on the command line, there is no way to implement fully variadic arguments. There are always 92 | tradeoffs that need to be made and in case of `argparse` these tradeoffs have been critical enough, that a system like 93 | Click cannot even be implemented on top of it. 94 | 95 | In this particular case, Click attempts to stay with a handful of accepted paradigms for building command line 96 | interfaces that can be well documented and tested. 97 | 98 | ## Why No Auto Correction? 99 | 100 | The question came up why Click does not auto correct parameters given that even optparse and `argparse` support 101 | automatic expansion of long arguments. The reason for this is that it's a liability for backwards compatibility. If 102 | people start relying on automatically modified parameters and someone adds a new parameter in the future, the script 103 | might stop working. These kinds of problems are hard to find, so Click does not attempt to be magical about this. 104 | 105 | This sort of behavior however can be implemented on a higher level to support things such as explicit aliases. For more 106 | information see {ref}`aliases`. 107 | -------------------------------------------------------------------------------- /docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart 2 | 3 | ```{currentmodule} click 4 | ``` 5 | 6 | ## Install 7 | 8 | Install from PyPI: 9 | 10 | ```console 11 | pip install click 12 | ``` 13 | 14 | Installing into a virtual environment is highly recommended. We suggest {ref}`virtualenv-heading`. 15 | 16 | ## Examples 17 | 18 | Some standalone examples of Click applications are packaged with Click. They are available in the 19 | [examples folder](https://github.com/pallets/click/tree/main/examples) of the repo. 20 | 21 | - [inout](https://github.com/pallets/click/tree/main/examples/inout) : A very simple example of an application that can 22 | read from files and write to files and also accept input from stdin or write to stdout. 23 | - [validation](https://github.com/pallets/click/tree/main/examples/validation) : A simple example of an application that 24 | performs custom validation of parameters in different ways. 25 | - [naval](https://github.com/pallets/click/tree/main/examples/naval) : Port of the [docopt](http://docopt.org/) naval 26 | example. 27 | - [colors](https://github.com/pallets/click/tree/main/examples/colors) : A simple example that colorizes text. Uses 28 | colorama on Windows. 29 | - [aliases](https://github.com/pallets/click/tree/main/examples/aliases) : An advanced example that implements 30 | {ref}`aliases`. 31 | - [imagepipe](https://github.com/pallets/click/tree/main/examples/imagepipe) : A complex example that implements some 32 | {ref}`command-pipelines` . It chains together image processing instructions. Requires pillow. 33 | - [repo](https://github.com/pallets/click/tree/main/examples/repo) : An advanced example that implements a 34 | Git-/Mercurial-like command line interface. 35 | - [complex](https://github.com/pallets/click/tree/main/examples/complex) : A very advanced example that implements 36 | loading subcommands dynamically from a plugin folder. 37 | - [termui](https://github.com/pallets/click/tree/main/examples/termui) : A simple example that showcases terminal UI 38 | helpers provided by click. 39 | 40 | ## Basic Concepts - Creating a Command 41 | 42 | Click is based on declaring commands through decorators. Internally, there is a non-decorator interface for advanced use 43 | cases, but it's discouraged for high-level usage. 44 | 45 | A function becomes a Click command line tool by decorating it through {func}`command`. At its simplest, just 46 | decorating a function with this decorator will make it into a callable script: 47 | 48 | 49 | ```{eval-rst} 50 | .. click:example:: 51 | import click 52 | 53 | @click.command() 54 | def hello(): 55 | click.echo('Hello World!') 56 | 57 | What's happening is that the decorator converts the function into a :class:`Command` which then can be invoked: 58 | 59 | .. click:example:: 60 | if __name__ == '__main__': 61 | hello() 62 | 63 | And what it looks like: 64 | 65 | .. click:run:: 66 | invoke(hello, args=[], prog_name='python hello.py') 67 | 68 | And the corresponding help page: 69 | 70 | .. click:run:: 71 | invoke(hello, args=['--help'], prog_name='python hello.py') 72 | ``` 73 | 74 | ## Echoing 75 | 76 | Why does this example use {func}`echo` instead of the regular {func}`print` function? The answer to this question is 77 | that Click attempts to support different environments consistently and to be very robust even when the environment is 78 | misconfigured. Click wants to be functional at least on a basic level even if everything is completely broken. 79 | 80 | What this means is that the {func}`echo` function applies some error correction in case the terminal is misconfigured 81 | instead of dying with a {exc}`UnicodeError`. 82 | 83 | The echo function also supports color and other styles in output. It will automatically remove styles if the output 84 | stream is a file. On Windows, colorama is automatically installed and used. See {ref}`ansi-colors`. 85 | 86 | If you don't need this, you can also use the `print()` construct / function. 87 | 88 | ## Nesting Commands 89 | 90 | Commands can be attached to other commands of type {class}`Group`. This allows arbitrary nesting of scripts. As an 91 | example here is a script that implements two commands for managing databases: 92 | 93 | ```{eval-rst} 94 | .. click:example:: 95 | @click.group() 96 | def cli(): 97 | pass 98 | 99 | @click.command() 100 | def initdb(): 101 | click.echo('Initialized the database') 102 | 103 | @click.command() 104 | def dropdb(): 105 | click.echo('Dropped the database') 106 | 107 | cli.add_command(initdb) 108 | cli.add_command(dropdb) 109 | ``` 110 | 111 | As you can see, the {func}`group` decorator works like the {func}`command` decorator, but creates a {class}`Group` 112 | object instead which can be given multiple subcommands that can be attached with {meth}`Group.add_command`. 113 | 114 | For simple scripts, it's also possible to automatically attach and create a command by using the {meth}`Group.command` 115 | decorator instead. The above script can instead be written like this: 116 | 117 | ```{eval-rst} 118 | .. click:example:: 119 | @click.group() 120 | def cli(): 121 | pass 122 | 123 | @cli.command() 124 | def initdb(): 125 | click.echo('Initialized the database') 126 | 127 | @cli.command() 128 | def dropdb(): 129 | click.echo('Dropped the database') 130 | 131 | You would then invoke the :class:`Group` in your entry points or other invocations: 132 | 133 | .. click:example:: 134 | if __name__ == '__main__': 135 | cli() 136 | ``` 137 | 138 | ## Registering Commands Later 139 | 140 | Instead of using the `@group.command()` decorator, commands can be decorated with the plain `@command()` decorator 141 | and registered with a group later with `group.add_command()`. This could be used to split commands into multiple Python 142 | modules. 143 | 144 | ```{code-block} python 145 | @click.command() 146 | def greet(): 147 | click.echo("Hello, World!") 148 | ``` 149 | 150 | ```{code-block} python 151 | @click.group() 152 | def group(): 153 | pass 154 | 155 | group.add_command(greet) 156 | ``` 157 | 158 | ## Adding Parameters 159 | 160 | To add parameters, use the {func}`option` and {func}`argument` decorators: 161 | 162 | ```{eval-rst} 163 | .. click:example:: 164 | @click.command() 165 | @click.option('--count', default=1, help='number of greetings') 166 | @click.argument('name') 167 | def hello(count, name): 168 | for x in range(count): 169 | click.echo(f"Hello {name}!") 170 | 171 | What it looks like: 172 | 173 | .. click:run:: 174 | invoke(hello, args=['--help'], prog_name='python hello.py') 175 | ``` 176 | 177 | ## Switching to Entry Points 178 | 179 | In the code you wrote so far there is a block at the end of the file which looks like this: 180 | `if __name__ == '__main__':`. This is traditionally how a standalone Python file looks like. With Click you can continue 181 | doing that, but a better way is to package your app with an entry point. 182 | 183 | There are two main (and many more) reasons for this: 184 | 185 | The first one is that installers automatically generate executable wrappers for Windows so your command line utilities 186 | work on Windows too. 187 | 188 | The second reason is that entry point scripts work with virtualenv on Unix without the virtualenv having to be 189 | activated. This is a very useful concept which allows you to bundle your scripts with all requirements into a 190 | virtualenv. 191 | 192 | Click is perfectly equipped to work with that and in fact the rest of the documentation will assume that you are writing 193 | applications as distributed packages. 194 | 195 | Look at the {doc}`entry-points` chapter before reading the rest as the examples assume that you will be using entry 196 | points. 197 | -------------------------------------------------------------------------------- /docs/documentation.md: -------------------------------------------------------------------------------- 1 | # Help Pages 2 | 3 | ```{currentmodule} click 4 | ``` 5 | 6 | Click makes it very easy to document your command line tools. For most things Click automatically generates help pages for you. By design the text is customizable, but the layout is not. 7 | 8 | ## Help Texts 9 | 10 | Commands and options accept help arguments. For commands, the docstring of the function is automatically used if provided. 11 | 12 | Simple example: 13 | 14 | ```{eval-rst} 15 | .. click:example:: 16 | 17 | @click.command() 18 | @click.argument('name') 19 | @click.option('--count', default=1, help='number of greetings') 20 | def hello(name: str, count: int): 21 | """This script prints hello and a name one or more times.""" 22 | for x in range(count): 23 | if name: 24 | click.echo(f"Hello {name}!") 25 | else: 26 | click.echo("Hello!") 27 | 28 | .. click:run:: 29 | invoke(hello, args=['--help']) 30 | ``` 31 | 32 | ## Command Short Help 33 | 34 | For subcommands, a short help snippet is generated. By default, it's the first sentence of the docstring. If too long, then it will ellipsize what cannot be fit on a single line with `...`. The short help snippet can also be overridden with `short_help`: 35 | 36 | ```{eval-rst} 37 | .. click:example:: 38 | 39 | import click 40 | 41 | @click.group() 42 | def cli(): 43 | """A simple command line tool.""" 44 | 45 | @cli.command('init', short_help='init the repo') 46 | def init(): 47 | """Initializes the repository.""" 48 | 49 | .. click:run:: 50 | invoke(cli, args=['--help']) 51 | ``` 52 | 53 | ## Command Epilog Help 54 | 55 | The help epilog is printed at the end of the help and is useful for showing example command usages or referencing additional help resources. 56 | 57 | ```{eval-rst} 58 | .. click:example:: 59 | 60 | import click 61 | 62 | @click.command( 63 | epilog='See https://example.com for more details', 64 | ) 65 | def init(): 66 | """Initializes the repository.""" 67 | 68 | .. click:run:: 69 | invoke(init, args=['--help']) 70 | ``` 71 | 72 | (documenting-arguments)= 73 | 74 | ## Documenting Arguments 75 | 76 | {class}`click.argument` does not take a `help` parameter. This follows the Unix Command Line Tools convention of using arguments only for necessary things and documenting them in the command help text 77 | by name. This should then be done via the docstring. 78 | 79 | A brief example: 80 | 81 | ```{eval-rst} 82 | .. click:example:: 83 | 84 | @click.command() 85 | @click.argument('filename') 86 | def touch(filename): 87 | """Print FILENAME.""" 88 | click.echo(filename) 89 | 90 | .. click:run:: 91 | invoke(touch, args=['--help']) 92 | ``` 93 | 94 | Or more explicitly: 95 | 96 | ```{eval-rst} 97 | .. click:example:: 98 | 99 | @click.command() 100 | @click.argument('filename') 101 | def touch(filename): 102 | """Print FILENAME. 103 | 104 | FILENAME is the name of the file to check. 105 | """ 106 | click.echo(filename) 107 | 108 | .. click:run:: 109 | invoke(touch, args=['--help']) 110 | ``` 111 | 112 | ## Showing Defaults 113 | 114 | To control the appearance of defaults pass `show_default`. 115 | 116 | ```{eval-rst} 117 | .. click:example:: 118 | 119 | @click.command() 120 | @click.option('--n', default=1, show_default=False, help='number of dots') 121 | def dots(n): 122 | click.echo('.' * n) 123 | 124 | .. click:run:: 125 | invoke(dots, args=['--help']) 126 | ``` 127 | 128 | For single option boolean flags, the default remains hidden if the default value is False, even if show default is set to true. 129 | 130 | ```{eval-rst} 131 | .. click:example:: 132 | 133 | @click.command() 134 | @click.option('--n', default=1, show_default=True) 135 | @click.option("--gr", is_flag=True, show_default=True, default=False, help="Greet the world.") 136 | @click.option("--br", is_flag=True, show_default=True, default=True, help="Add a thematic break") 137 | def dots(n, gr, br): 138 | if gr: 139 | click.echo('Hello world!') 140 | click.echo('.' * n) 141 | if br: 142 | click.echo('-' * n) 143 | 144 | .. click:run:: 145 | invoke(dots, args=['--help']) 146 | ``` 147 | 148 | ## Click's Wrapping Behavior 149 | 150 | 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. 151 | 152 | ```{eval-rst} 153 | .. click:example:: 154 | 155 | import click 156 | 157 | @click.command() 158 | def cli(): 159 | """ 160 | This is a very long paragraph and as you 161 | can see wrapped very early in the source text 162 | but will be rewrapped to the terminal width in 163 | the final output. 164 | 165 | This is 166 | a paragraph 167 | that is compacted. 168 | """ 169 | 170 | .. click:run:: 171 | invoke(cli, args=['--help']) 172 | ``` 173 | 174 | ## Escaping Click's Wrapping 175 | 176 | Sometimes Click's wrapping can be a problem, such as when showing code examples where new lines are significant. This behavior can be escaped on a per-paragraph basis by adding a line with only `\b` . The `\b` is removed from the rendered help text. 177 | 178 | Example: 179 | 180 | ```{eval-rst} 181 | .. click:example:: 182 | 183 | import click 184 | 185 | @click.command() 186 | def cli(): 187 | """First paragraph. 188 | 189 | \b 190 | This is 191 | a paragraph 192 | without rewrapping. 193 | 194 | And this is a paragraph 195 | that will be rewrapped again. 196 | """ 197 | 198 | .. click:run:: 199 | invoke(cli, args=['--help']) 200 | ``` 201 | 202 | To change the rendering maximum width, pass `max_content_width` when calling the command. 203 | 204 | ```bash 205 | cli(max_content_width=120) 206 | ``` 207 | 208 | ## Truncating Help Texts 209 | 210 | Click gets {class}`Command` help text from the docstring. If you do not want to include part of the docstring, add the `\f` escape marker to have Click truncate the help text after the marker. 211 | 212 | Example: 213 | 214 | ```{eval-rst} 215 | .. click:example:: 216 | 217 | import click 218 | 219 | @click.command() 220 | def cli(): 221 | """First paragraph. 222 | \f 223 | 224 | Words to not be included. 225 | """ 226 | 227 | .. click:run:: 228 | invoke(cli, args=['--help']) 229 | ``` 230 | 231 | (doc-meta-variables)= 232 | 233 | ## Placeholder / Meta Variable 234 | 235 | The default placeholder variable ([meta variable](https://en.wikipedia.org/wiki/Metasyntactic_variable#IETF_Requests_for_Comments)) in the help pages is the parameter name in uppercase with underscores. This can be changed for Commands and Parameters with the `options_metavar` and `metavar` kwargs. 236 | 237 | ```{eval-rst} 238 | .. click:example:: 239 | 240 | # This controls entry on the usage line. 241 | @click.command(options_metavar='[[options]]') 242 | @click.option('--count', default=1, help='number of greetings', 243 | metavar='') 244 | @click.argument('name', metavar='') 245 | def hello(name: str, count: int) -> None: 246 | """This script prints 'hello ' a total of times.""" 247 | for x in range(count): 248 | click.echo(f"Hello {name}!") 249 | 250 | # Example usage: 251 | 252 | .. click:run:: 253 | invoke(hello, args=['--help']) 254 | 255 | ``` 256 | 257 | ## Help Parameter Customization 258 | 259 | Help parameters are automatically added by Click for any command. The default is `--help` but can be overridden by the context setting {attr}`~Context.help_option_names`. Click also performs automatic conflict resolution on the default help parameter, so if a command itself implements a parameter named `help` then the default help will not be run. 260 | 261 | This example changes the default parameters to `-h` and `--help` 262 | instead of just `--help`: 263 | 264 | ```{eval-rst} 265 | .. click:example:: 266 | 267 | import click 268 | 269 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 270 | 271 | @click.command(context_settings=CONTEXT_SETTINGS) 272 | def cli(): 273 | pass 274 | 275 | .. click:run:: 276 | invoke(cli, ['-h']) 277 | ``` 278 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pathlib 3 | import platform 4 | import tempfile 5 | 6 | import pytest 7 | 8 | import asyncclick as click 9 | from asyncclick import FileError 10 | 11 | 12 | @pytest.mark.parametrize( 13 | ("type", "value", "expect"), 14 | [ 15 | (click.IntRange(0, 5), "3", 3), 16 | (click.IntRange(5), "5", 5), 17 | (click.IntRange(5), "100", 100), 18 | (click.IntRange(max=5), "5", 5), 19 | (click.IntRange(max=5), "-100", -100), 20 | (click.IntRange(0, clamp=True), "-1", 0), 21 | (click.IntRange(max=5, clamp=True), "6", 5), 22 | (click.IntRange(0, min_open=True, clamp=True), "0", 1), 23 | (click.IntRange(max=5, max_open=True, clamp=True), "5", 4), 24 | (click.FloatRange(0.5, 1.5), "1.2", 1.2), 25 | (click.FloatRange(0.5, min_open=True), "0.51", 0.51), 26 | (click.FloatRange(max=1.5, max_open=True), "1.49", 1.49), 27 | (click.FloatRange(0.5, clamp=True), "-0.0", 0.5), 28 | (click.FloatRange(max=1.5, clamp=True), "inf", 1.5), 29 | ], 30 | ) 31 | def test_range(type, value, expect): 32 | assert type.convert(value, None, None) == expect 33 | 34 | 35 | @pytest.mark.parametrize( 36 | ("type", "value", "expect"), 37 | [ 38 | (click.IntRange(0, 5), "6", "6 is not in the range 0<=x<=5."), 39 | (click.IntRange(5), "4", "4 is not in the range x>=5."), 40 | (click.IntRange(max=5), "6", "6 is not in the range x<=5."), 41 | (click.IntRange(0, 5, min_open=True), 0, "00.5"), 44 | (click.FloatRange(max=1.5, max_open=True), 1.5, "x<1.5"), 45 | ], 46 | ) 47 | def test_range_fail(type, value, expect): 48 | with pytest.raises(click.BadParameter) as exc_info: 49 | type.convert(value, None, None) 50 | 51 | assert expect in exc_info.value.message 52 | 53 | 54 | def test_float_range_no_clamp_open(): 55 | with pytest.raises(TypeError): 56 | click.FloatRange(0, 1, max_open=True, clamp=True) 57 | 58 | sneaky = click.FloatRange(0, 1, max_open=True) 59 | sneaky.clamp = True 60 | 61 | with pytest.raises(RuntimeError): 62 | sneaky.convert("1.5", None, None) 63 | 64 | 65 | @pytest.mark.parametrize( 66 | ("nargs", "multiple", "default", "expect"), 67 | [ 68 | (2, False, None, None), 69 | (2, False, (None, None), (None, None)), 70 | (None, True, None, ()), 71 | (None, True, (None, None), (None, None)), 72 | (2, True, None, ()), 73 | (2, True, [(None, None)], ((None, None),)), 74 | (-1, None, None, ()), 75 | ], 76 | ) 77 | def test_cast_multi_default(runner, nargs, multiple, default, expect): 78 | if nargs == -1: 79 | param = click.Argument(["a"], nargs=nargs, default=default) 80 | else: 81 | param = click.Option(["-a"], nargs=nargs, multiple=multiple, default=default) 82 | 83 | cli = click.Command("cli", params=[param], callback=lambda a: a) 84 | result = runner.invoke(cli, standalone_mode=False) 85 | assert result.exception is None 86 | assert result.return_value == expect 87 | 88 | 89 | @pytest.mark.parametrize( 90 | ("cls", "expect"), 91 | [ 92 | (None, "a/b/c.txt"), 93 | (str, "a/b/c.txt"), 94 | (bytes, b"a/b/c.txt"), 95 | (pathlib.Path, pathlib.Path("a", "b", "c.txt")), 96 | ], 97 | ) 98 | def test_path_type(runner, cls, expect): 99 | cli = click.Command( 100 | "cli", 101 | params=[click.Argument(["p"], type=click.Path(path_type=cls))], 102 | callback=lambda p: p, 103 | ) 104 | result = runner.invoke(cli, ["a/b/c.txt"], standalone_mode=False) 105 | assert result.exception is None 106 | assert result.return_value == expect 107 | 108 | 109 | def _symlinks_supported(): 110 | with tempfile.TemporaryDirectory(prefix="click-pytest-") as tempdir: 111 | target = os.path.join(tempdir, "target") 112 | open(target, "w").close() 113 | link = os.path.join(tempdir, "link") 114 | 115 | try: 116 | os.symlink(target, link) 117 | return True 118 | except OSError: 119 | return False 120 | 121 | 122 | @pytest.mark.skipif( 123 | not _symlinks_supported(), reason="The current OS or FS doesn't support symlinks." 124 | ) 125 | def test_path_resolve_symlink(tmp_path, runner): 126 | test_file = tmp_path / "file" 127 | test_file_str = os.fspath(test_file) 128 | test_file.write_text("") 129 | 130 | path_type = click.Path(resolve_path=True) 131 | param = click.Argument(["a"], type=path_type) 132 | ctx = click.Context(click.Command("cli", params=[param])) 133 | 134 | test_dir = tmp_path / "dir" 135 | test_dir.mkdir() 136 | 137 | abs_link = test_dir / "abs" 138 | abs_link.symlink_to(test_file) 139 | abs_rv = path_type.convert(os.fspath(abs_link), param, ctx) 140 | assert abs_rv == test_file_str 141 | 142 | rel_link = test_dir / "rel" 143 | rel_link.symlink_to(pathlib.Path("..") / "file") 144 | rel_rv = path_type.convert(os.fspath(rel_link), param, ctx) 145 | assert rel_rv == test_file_str 146 | 147 | 148 | def _non_utf8_filenames_supported(): 149 | with tempfile.TemporaryDirectory(prefix="click-pytest-") as tempdir: 150 | try: 151 | f = open(os.path.join(tempdir, "\udcff"), "w") 152 | except OSError: 153 | return False 154 | 155 | f.close() 156 | return True 157 | 158 | 159 | @pytest.mark.skipif( 160 | not _non_utf8_filenames_supported(), 161 | reason="The current OS or FS doesn't support non-UTF-8 filenames.", 162 | ) 163 | def test_path_surrogates(tmp_path, monkeypatch): 164 | monkeypatch.chdir(tmp_path) 165 | type = click.Path(exists=True) 166 | path = pathlib.Path("\udcff") 167 | 168 | with pytest.raises(click.BadParameter, match="'�' does not exist"): 169 | type.convert(path, None, None) 170 | 171 | type = click.Path(file_okay=False) 172 | path.touch() 173 | 174 | with pytest.raises(click.BadParameter, match="'�' is a file"): 175 | type.convert(path, None, None) 176 | 177 | path.unlink() 178 | type = click.Path(dir_okay=False) 179 | path.mkdir() 180 | 181 | with pytest.raises(click.BadParameter, match="'�' is a directory"): 182 | type.convert(path, None, None) 183 | 184 | path.rmdir() 185 | 186 | def no_access(*args, **kwargs): 187 | """Test environments may be running as root, so we have to fake the result of 188 | the access tests that use os.access 189 | """ 190 | p = args[0] 191 | assert p == path, f"unexpected os.access call on file not under test: {p!r}" 192 | return False 193 | 194 | path.touch() 195 | type = click.Path(readable=True) 196 | 197 | with pytest.raises(click.BadParameter, match="'�' is not readable"): 198 | with monkeypatch.context() as m: 199 | m.setattr(os, "access", no_access) 200 | type.convert(path, None, None) 201 | 202 | type = click.Path(readable=False, writable=True) 203 | 204 | with pytest.raises(click.BadParameter, match="'�' is not writable"): 205 | with monkeypatch.context() as m: 206 | m.setattr(os, "access", no_access) 207 | type.convert(path, None, None) 208 | 209 | type = click.Path(readable=False, executable=True) 210 | 211 | with pytest.raises(click.BadParameter, match="'�' is not executable"): 212 | with monkeypatch.context() as m: 213 | m.setattr(os, "access", no_access) 214 | type.convert(path, None, None) 215 | 216 | path.unlink() 217 | 218 | 219 | @pytest.mark.parametrize( 220 | "type", 221 | [ 222 | click.File(mode="r"), 223 | click.File(mode="r", lazy=True), 224 | ], 225 | ) 226 | def test_file_surrogates(type, tmp_path): 227 | path = tmp_path / "\udcff" 228 | 229 | # - common case: �': No such file or directory 230 | # - special case: Illegal byte sequence 231 | # The spacial case is seen with rootless Podman. The root cause is most 232 | # likely that the path is handled by a user-space program (FUSE). 233 | match = r"(�': No such file or directory|Illegal byte sequence)" 234 | with pytest.raises(click.BadParameter, match=match): 235 | type.convert(path, None, None) 236 | 237 | 238 | def test_file_error_surrogates(): 239 | message = FileError(filename="\udcff").format_message() 240 | assert message == "Could not open file '�': unknown error" 241 | 242 | 243 | @pytest.mark.skipif( 244 | platform.system() == "Windows", reason="Filepath syntax differences." 245 | ) 246 | def test_invalid_path_with_esc_sequence(): 247 | with pytest.raises(click.BadParameter) as exc_info: 248 | with tempfile.TemporaryDirectory(prefix="my\ndir") as tempdir: 249 | click.Path(dir_okay=False).convert(tempdir, None, None) 250 | 251 | assert "my\\ndir" in exc_info.value.message 252 | 253 | 254 | def test_choice_get_invalid_choice_message(): 255 | choice = click.Choice(["a", "b", "c"]) 256 | message = choice.get_invalid_choice_message("d", ctx=None) 257 | assert message == "'d' is not one of 'a', 'b', 'c'." 258 | --------------------------------------------------------------------------------