├── .devcontainer ├── devcontainer.json └── on-create-command.sh ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ ├── config.yml │ └── feature-request.md ├── pull_request_template.md └── workflows │ ├── lock.yaml │ ├── pre-commit.yaml │ ├── publish.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.rst ├── LICENSE.txt ├── README.md ├── artwork └── logo.svg ├── docs ├── Makefile ├── _static │ ├── click-icon.png │ ├── click-logo-sidebar.png │ └── click-logo.png ├── advanced.rst ├── api.rst ├── arguments.rst ├── changes.rst ├── click-concepts.rst ├── commands-and-groups.rst ├── commands.rst ├── complex.rst ├── conf.py ├── contrib.rst ├── documentation.rst ├── entry-points.rst ├── exceptions.rst ├── extending-click.rst ├── faqs.md ├── handling-files.rst ├── index.rst ├── license.rst ├── make.bat ├── option-decorators.rst ├── options.md ├── parameter-types.rst ├── parameters.rst ├── prompts.rst ├── quickstart.rst ├── setuptools.rst ├── shell-completion.rst ├── support-multiple-versions.md ├── testing.md ├── unicode-support.rst ├── utils.rst ├── virtualenv.rst ├── why.rst └── wincmd.rst ├── examples ├── README ├── aliases │ ├── README │ ├── aliases.ini │ ├── aliases.py │ └── pyproject.toml ├── colors │ ├── README │ ├── colors.py │ └── pyproject.toml ├── completion │ ├── README │ ├── completion.py │ └── pyproject.toml ├── complex │ ├── README │ ├── complex │ │ ├── __init__.py │ │ ├── cli.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── cmd_init.py │ │ │ └── cmd_status.py │ └── pyproject.toml ├── imagepipe │ ├── .gitignore │ ├── README │ ├── example01.jpg │ ├── example02.jpg │ ├── imagepipe.py │ └── pyproject.toml ├── inout │ ├── README │ ├── inout.py │ └── pyproject.toml ├── naval │ ├── README │ ├── naval.py │ └── pyproject.toml ├── repo │ ├── README │ ├── pyproject.toml │ └── repo.py ├── termui │ ├── README │ ├── pyproject.toml │ └── termui.py └── validation │ ├── README │ ├── pyproject.toml │ └── validation.py ├── pyproject.toml ├── src └── click │ ├── __init__.py │ ├── _compat.py │ ├── _termui_impl.py │ ├── _textwrap.py │ ├── _winconsole.py │ ├── core.py │ ├── decorators.py │ ├── exceptions.py │ ├── formatting.py │ ├── globals.py │ ├── parser.py │ ├── py.typed │ ├── shell_completion.py │ ├── termui.py │ ├── testing.py │ ├── types.py │ └── utils.py ├── tests ├── conftest.py ├── test_arguments.py ├── test_basic.py ├── test_chain.py ├── test_command_decorators.py ├── test_commands.py ├── test_compat.py ├── test_context.py ├── test_custom_classes.py ├── test_defaults.py ├── test_formatting.py ├── test_imports.py ├── test_info_dict.py ├── test_normalization.py ├── test_options.py ├── test_parser.py ├── test_shell_completion.py ├── test_termui.py ├── test_testing.py ├── test_types.py ├── test_utils.py └── typing │ ├── typing_aliased_group.py │ ├── typing_confirmation_option.py │ ├── typing_group_kw_options.py │ ├── typing_help_option.py │ ├── typing_options.py │ ├── typing_password_option.py │ ├── typing_progressbar.py │ ├── typing_simple_example.py │ └── typing_version_option.py └── uv.lock /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 26 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 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 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: ['*'] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | outputs: 9 | hash: ${{ steps.hash.outputs.hash }} 10 | steps: 11 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 12 | - uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 13 | with: 14 | enable-cache: true 15 | prune-cache: false 16 | - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 17 | with: 18 | python-version-file: pyproject.toml 19 | - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV 20 | - run: uv build 21 | - name: generate hash 22 | id: hash 23 | run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT 24 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 25 | with: 26 | path: ./dist 27 | provenance: 28 | needs: [build] 29 | permissions: 30 | actions: read 31 | id-token: write 32 | contents: write 33 | # Can't pin with hash due to how this workflow works. 34 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 35 | with: 36 | base64-subjects: ${{ needs.build.outputs.hash }} 37 | create-release: 38 | needs: [provenance] 39 | runs-on: ubuntu-latest 40 | permissions: 41 | contents: write 42 | steps: 43 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 44 | - name: create release 45 | run: > 46 | gh release create --draft --repo ${{ github.repository }} 47 | ${{ github.ref_name }} 48 | *.intoto.jsonl/* artifact/* 49 | env: 50 | GH_TOKEN: ${{ github.token }} 51 | publish-pypi: 52 | needs: [provenance] 53 | environment: 54 | name: publish 55 | url: https://pypi.org/project/click/${{ github.ref_name }} 56 | runs-on: ubuntu-latest 57 | permissions: 58 | id-token: write 59 | steps: 60 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 61 | - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 62 | with: 63 | packages-dir: artifact/ 64 | -------------------------------------------------------------------------------- /.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@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 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@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | __pycache__/ 4 | dist/ 5 | .coverage* 6 | htmlcov/ 7 | .tox/ 8 | docs/_build/ 9 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 24e02b24b8ab2b7c76225602d13fa60e12d114e6 # frozen: v0.11.9 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/astral-sh/uv-pre-commit 8 | rev: 14ac15b122e538e407d036ff45e3895b7cf4a2bf # frozen: 0.7.3 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 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-24.04 4 | tools: 5 | python: '3.13' 6 | commands: 7 | - asdf plugin add uv 8 | - asdf install uv latest 9 | - asdf global uv latest 10 | - uv run --group docs sphinx-build -W -b dirhtml docs $READTHEDOCS_OUTPUT/html 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # $ click_ 2 | 3 | Click is a Python package for creating beautiful command line interfaces 4 | in a composable way with as little code as necessary. It's the "Command 5 | Line Interface Creation Kit". It's highly configurable but comes with 6 | sensible defaults out of the box. 7 | 8 | It aims to make the process of writing command line tools quick and fun 9 | while also preventing any frustration caused by the inability to 10 | implement an intended CLI API. 11 | 12 | Click in three points: 13 | 14 | - Arbitrary nesting of commands 15 | - Automatic help page generation 16 | - Supports lazy loading of subcommands at runtime 17 | 18 | 19 | ## A Simple Example 20 | 21 | ```python 22 | import click 23 | 24 | @click.command() 25 | @click.option("--count", default=1, help="Number of greetings.") 26 | @click.option("--name", prompt="Your name", help="The person to greet.") 27 | def hello(count, name): 28 | """Simple program that greets NAME for a total of COUNT times.""" 29 | for _ in range(count): 30 | click.echo(f"Hello, {name}!") 31 | 32 | if __name__ == '__main__': 33 | hello() 34 | ``` 35 | 36 | ``` 37 | $ python hello.py --count=3 38 | Your name: Click 39 | Hello, Click! 40 | Hello, Click! 41 | Hello, Click! 42 | ``` 43 | 44 | 45 | ## Donate 46 | 47 | The Pallets organization develops and supports Click and other popular 48 | packages. In order to grow the community of contributors and users, and 49 | allow the maintainers to devote more time to the projects, [please 50 | donate today][]. 51 | 52 | [please donate today]: https://palletsprojects.com/donate 53 | 54 | ## Contributing 55 | 56 | See our [detailed contributing documentation][contrib] for many ways to 57 | contribute, including reporting issues, requesting features, asking or answering 58 | questions, and making PRs. 59 | 60 | [contrib]: https://palletsprojects.com/contributing/ 61 | -------------------------------------------------------------------------------- /artwork/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 64 | 69 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = Jinja 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/click-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/docs/_static/click-icon.png -------------------------------------------------------------------------------- /docs/_static/click-logo-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/docs/_static/click-logo-sidebar.png -------------------------------------------------------------------------------- /docs/_static/click-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/docs/_static/click-logo.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. module:: click 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:: click.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:: click.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:: click.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:: click.testing 212 | 213 | .. autoclass:: CliRunner 214 | :members: 215 | 216 | .. autoclass:: Result 217 | :members: 218 | -------------------------------------------------------------------------------- /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/changes.rst: -------------------------------------------------------------------------------- 1 | Changes 2 | ======= 3 | 4 | .. include:: ../CHANGES.rst 5 | -------------------------------------------------------------------------------- /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/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.png" 55 | html_logo = "_static/click-logo-sidebar.png" 56 | html_title = f"Click Documentation ({version})" 57 | html_show_sourcelink = False 58 | -------------------------------------------------------------------------------- /docs/contrib.rst: -------------------------------------------------------------------------------- 1 | .. _contrib: 2 | 3 | ============= 4 | click-contrib 5 | ============= 6 | 7 | As the number of users of Click grows, more and more major feature requests are 8 | made. To users it may seem reasonable to include those features with Click; 9 | however, many of them are experimental or aren't practical to support 10 | generically. Maintainers have to choose what is reasonable to maintain in Click 11 | core. 12 | 13 | The click-contrib_ GitHub organization exists as a place to collect third-party 14 | packages that extend Click's features. It is also meant to ease the effort of 15 | searching for such extensions. 16 | 17 | Please note that the quality and stability of those packages may be different 18 | than Click itself. While published under a common organization, they are still 19 | separate from Click and the Pallets maintainers. 20 | 21 | 22 | Third-party projects 23 | -------------------- 24 | 25 | Other projects that extend Click's features are available outside of the 26 | click-contrib_ organization. 27 | 28 | Some of the most popular and actively maintained are listed below: 29 | 30 | ========================================================== =========================================================================================== ================================================================================================= ====================================================================================================== 31 | Project Description Popularity Activity 32 | ========================================================== =========================================================================================== ================================================================================================= ====================================================================================================== 33 | `Typer `_ Use Python type hints to create CLI apps. .. image:: https://img.shields.io/github/stars/fastapi/typer?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/fastapi/typer?label=%20&style=flat-square 34 | :alt: GitHub stars :alt: Last commit 35 | `rich-click `_ Format help outputwith Rich. .. image:: https://img.shields.io/github/stars/ewels/rich-click?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/ewels/rich-click?label=%20&style=flat-square 36 | :alt: GitHub stars :alt: Last commit 37 | `click-app `_ Cookiecutter template for creating new CLIs. .. image:: https://img.shields.io/github/stars/simonw/click-app?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/simonw/click-app?label=%20&style=flat-square 38 | :alt: GitHub stars :alt: Last commit 39 | `Cloup `_ Adds option groups, constraints, command aliases, help themes, suggestions and more. .. image:: https://img.shields.io/github/stars/janluke/cloup?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/janluke/cloup?label=%20&style=flat-square 40 | :alt: GitHub stars :alt: Last commit 41 | `Click Extra `_ Cloup + colorful ``--help``, ``--config``, ``--show-params``, ``--verbosity`` options, etc. .. image:: https://img.shields.io/github/stars/kdeldycke/click-extra?label=%20&style=flat-square .. image:: https://img.shields.io/github/last-commit/kdeldycke/click-extra?label=%20&style=flat-square 42 | :alt: GitHub stars :alt: Last commit 43 | ========================================================== =========================================================================================== ================================================================================================= ====================================================================================================== 44 | 45 | .. note:: 46 | 47 | To make it into the list above, a project: 48 | 49 | - must be actively maintained (at least one commit in the last year) 50 | - must have a reasonable number of stars (at least 20) 51 | 52 | If you have a project that meets these criteria, please open a pull request 53 | to add it to the list. 54 | 55 | If a project is no longer maintained or does not meet the criteria above, 56 | please open a pull request to remove it from the list. 57 | 58 | .. _click-contrib: https://github.com/click-contrib/ 59 | -------------------------------------------------------------------------------- /docs/documentation.rst: -------------------------------------------------------------------------------- 1 | Help Pages 2 | =================== 3 | 4 | .. currentmodule:: click 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 | 11 | Commands and options accept help arguments. For commands, the docstring of the function is automatically used if provided. 12 | 13 | Simple example: 14 | 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 | 30 | invoke(hello, args=['--help']) 31 | 32 | 33 | Command Short Help 34 | ------------------ 35 | 36 | 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``: 37 | 38 | .. click:example:: 39 | 40 | @click.group() 41 | def cli(): 42 | """A simple command line tool.""" 43 | 44 | @cli.command('init', short_help='init the repo') 45 | def init(): 46 | """Initializes the repository.""" 47 | 48 | 49 | .. click:run:: 50 | 51 | invoke(cli, args=['--help']) 52 | 53 | Command Epilog Help 54 | ------------------- 55 | 56 | The help epilog is printed at the end of the help and is useful for showing example command usages or referencing additional help resources. 57 | 58 | .. click:example:: 59 | 60 | @click.command( 61 | epilog='See https://example.com for more details', 62 | ) 63 | def init(): 64 | """Initializes the repository.""" 65 | 66 | 67 | .. click:run:: 68 | 69 | invoke(init, args=['--help']) 70 | 71 | .. _documenting-arguments: 72 | 73 | Documenting Arguments 74 | ---------------------- 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 | .. click:example:: 82 | 83 | @click.command() 84 | @click.argument('filename') 85 | def touch(filename): 86 | """Print FILENAME.""" 87 | click.echo(filename) 88 | 89 | 90 | .. click:run:: 91 | 92 | invoke(touch, args=['--help']) 93 | 94 | Or more explicitly: 95 | 96 | .. click:example:: 97 | 98 | @click.command() 99 | @click.argument('filename') 100 | def touch(filename): 101 | """Print FILENAME. 102 | 103 | FILENAME is the name of the file to check. 104 | """ 105 | click.echo(filename) 106 | 107 | .. click:run:: 108 | 109 | invoke(touch, args=['--help']) 110 | 111 | 112 | Showing Defaults 113 | --------------------------- 114 | To control the appearance of defaults pass ``show_default``. 115 | 116 | .. click:example:: 117 | 118 | @click.command() 119 | @click.option('--n', default=1, show_default=False, help='number of dots') 120 | def dots(n): 121 | click.echo('.' * n) 122 | 123 | .. click:run:: 124 | 125 | invoke(dots, args=['--help']) 126 | 127 | For single option boolean flags, the default remains hidden if the default value is False even if show default is true. 128 | 129 | .. click:example:: 130 | 131 | @click.command() 132 | @click.option('--n', default=1, show_default=True) 133 | @click.option("--gr", is_flag=True, show_default=True, default=False, help="Greet the world.") 134 | @click.option("--br", is_flag=True, show_default=True, default=True, help="Add a thematic break") 135 | def dots(n, gr, br): 136 | if gr: 137 | click.echo('Hello world!') 138 | click.echo('.' * n) 139 | if br: 140 | click.echo('-' * n) 141 | 142 | .. click:run:: 143 | 144 | invoke(dots, args=['--help']) 145 | 146 | 147 | Click's Wrapping Behavior 148 | ---------------------------- 149 | 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. In the example notice how the second grouping of three lines is rewrapped into a single paragraph. 150 | 151 | .. click:example:: 152 | 153 | @click.command() 154 | def cli(): 155 | """ 156 | This is a very long paragraph and as you 157 | can see wrapped very early in the source text 158 | but will be rewrapped to the terminal width in 159 | the final output. 160 | 161 | This is 162 | a paragraph 163 | that is compacted. 164 | """ 165 | 166 | .. click:run:: 167 | 168 | invoke(cli, args=['--help']) 169 | 170 | Escaping Click's Wrapping 171 | --------------------------- 172 | Sometimes Click's wrapping can be a problem, such as when showing code examples where newlines 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. 173 | 174 | Example: 175 | 176 | .. click:example:: 177 | 178 | @click.command() 179 | def cli(): 180 | """First paragraph. 181 | 182 | \b 183 | This is 184 | a paragraph 185 | without rewrapping. 186 | 187 | And this is a paragraph 188 | that will be rewrapped again. 189 | """ 190 | 191 | 192 | .. click:run:: 193 | 194 | invoke(cli, args=['--help']) 195 | 196 | To change the rendering maximum width, pass ``max_content_width`` when calling the command. 197 | 198 | .. code-block:: python 199 | 200 | cli(max_content_width=120) 201 | 202 | .. _doc-meta-variables: 203 | 204 | Truncating Help Texts 205 | --------------------- 206 | 207 | 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. 208 | 209 | Example: 210 | 211 | .. click:example:: 212 | 213 | @click.command() 214 | def cli(): 215 | """First paragraph. 216 | \f 217 | 218 | Words to not be included. 219 | """ 220 | 221 | .. click:run:: 222 | 223 | invoke(cli, args=['--help']) 224 | 225 | 226 | Placeholder / Meta Variable 227 | ----------------------------- 228 | 229 | The default placeholder variable (`meta variable `_) 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. 230 | 231 | .. click:example:: 232 | 233 | # This controls entry on the usage line. 234 | @click.command(options_metavar='[[options]]') 235 | @click.option('--count', default=1, help='number of greetings', 236 | metavar='') 237 | @click.argument('name', metavar='') 238 | def hello(name: str, count: int) -> None: 239 | """This script prints 'hello ' a total of times.""" 240 | for x in range(count): 241 | click.echo(f"Hello {name}!") 242 | 243 | Example: 244 | 245 | .. click:run:: 246 | 247 | invoke(hello, args=['--help']) 248 | 249 | Help Parameter Customization 250 | ---------------------------- 251 | Help parameters are automatically added by Click for any command. The default is ``--help`` but can be override 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. 252 | 253 | This example changes the default parameters to ``-h`` and ``--help`` 254 | instead of just ``--help``: 255 | 256 | .. click:example:: 257 | 258 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 259 | 260 | @click.command(context_settings=CONTEXT_SETTINGS) 261 | def cli(): 262 | pass 263 | 264 | 265 | .. click:run:: 266 | 267 | invoke(cli, ['-h']) 268 | -------------------------------------------------------------------------------- /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 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: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/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 = command.make_context('command-name', ['args', 'go', 'here']) 37 | with ctx: 38 | result = 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 | 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/index.rst: -------------------------------------------------------------------------------- 1 | .. rst-class:: hide-header 2 | 3 | Welcome to Click 4 | ================ 5 | 6 | .. image:: _static/click-logo.png 7 | :align: center 8 | :scale: 50% 9 | :target: https://palletsprojects.com/p/click/ 10 | 11 | Click is a Python package for creating beautiful command line interfaces 12 | in a composable way with as little code as necessary. It's the "Command 13 | Line Interface Creation Kit". It's highly configurable but comes with 14 | sensible defaults out of the box. 15 | 16 | It aims to make the process of writing command line tools quick and fun 17 | while also preventing any frustration caused by the inability to implement 18 | an intended CLI API. 19 | 20 | Click in three points: 21 | 22 | - arbitrary nesting of commands 23 | - automatic help page generation 24 | - supports lazy loading of subcommands at runtime 25 | 26 | What does it look like? Here is an example of a simple Click program: 27 | 28 | .. click:example:: 29 | 30 | import click 31 | 32 | @click.command() 33 | @click.option('--count', default=1, help='Number of greetings.') 34 | @click.option('--name', prompt='Your name', 35 | help='The person to greet.') 36 | def hello(count, name): 37 | """Simple program that greets NAME for a total of COUNT times.""" 38 | for x in range(count): 39 | click.echo(f"Hello {name}!") 40 | 41 | if __name__ == '__main__': 42 | hello() 43 | 44 | And what it looks like when run: 45 | 46 | .. click:run:: 47 | 48 | invoke(hello, ['--count=3'], prog_name='python hello.py', input='John\n') 49 | 50 | It automatically generates nicely formatted help pages: 51 | 52 | .. click:run:: 53 | 54 | invoke(hello, ['--help'], prog_name='python hello.py') 55 | 56 | You can get the library directly from PyPI:: 57 | 58 | pip install click 59 | 60 | Documentation 61 | ------------- 62 | 63 | .. toctree:: 64 | :maxdepth: 2 65 | 66 | faqs 67 | 68 | Tutorials 69 | ^^^^^^^^^^^^^ 70 | .. toctree:: 71 | :maxdepth: 1 72 | 73 | quickstart 74 | virtualenv 75 | 76 | How to Guides 77 | ^^^^^^^^^^^^^ 78 | .. toctree:: 79 | :maxdepth: 1 80 | 81 | entry-points 82 | setuptools 83 | support-multiple-versions 84 | 85 | Conceptual Guides 86 | ^^^^^^^^^^^^^^^^^^^ 87 | .. toctree:: 88 | :maxdepth: 1 89 | 90 | why 91 | click-concepts 92 | 93 | General Reference 94 | ^^^^^^^^^^^^^^^^^^^ 95 | 96 | .. toctree:: 97 | :maxdepth: 1 98 | 99 | parameters 100 | parameter-types 101 | options 102 | option-decorators 103 | arguments 104 | commands-and-groups 105 | commands 106 | documentation 107 | prompts 108 | handling-files 109 | advanced 110 | complex 111 | extending-click 112 | testing 113 | utils 114 | shell-completion 115 | exceptions 116 | unicode-support 117 | wincmd 118 | 119 | API Reference 120 | ^^^^^^^^^^^^^^^ 121 | 122 | .. toctree:: 123 | :maxdepth: 2 124 | 125 | api 126 | 127 | About Project 128 | ------------------- 129 | 130 | * This documentation is structured according to `Diataxis `_ 131 | 132 | * `Version Policy `_ 133 | 134 | * `Contributing `_ 135 | 136 | * `Donate `_ 137 | 138 | .. toctree:: 139 | :maxdepth: 1 140 | 141 | contrib 142 | license 143 | changes 144 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | BSD-3-Clause License 2 | ==================== 3 | 4 | .. literalinclude:: ../LICENSE.txt 5 | :language: text 6 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=Jinja 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /docs/parameters.rst: -------------------------------------------------------------------------------- 1 | .. _parameters: 2 | 3 | Parameters 4 | ========== 5 | 6 | .. currentmodule:: click 7 | 8 | Click supports only two principle types of parameters for scripts (by design): options and arguments. 9 | 10 | Options 11 | ---------------- 12 | 13 | * Are optional. 14 | * Recommended to use for everything except subcommands, urls, or files. 15 | * Can take a fixed number of arguments. The default is 1. They may be specified multiple times using :ref:`multiple-options`. 16 | * Are fully documented by the help page. 17 | * Have automatic prompting for missing input. 18 | * Can act as flags (boolean or otherwise). 19 | * Can be pulled from environment variables. 20 | 21 | Arguments 22 | ---------------- 23 | 24 | * Are optional with in reason, but not entirely so. 25 | * Recommended to use for subcommands, urls, or files. 26 | * Can take an arbitrary number of arguments. 27 | * Are not fully documented by the help page since they may be too specific to be automatically documented. For more see :ref:`documenting-arguments`. 28 | * Can be pulled from environment variables but only explicitly named ones. For more see :ref:`environment-variables`. 29 | 30 | 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. 31 | 32 | .. _parameter_names: 33 | 34 | Parameter Names 35 | --------------- 36 | 37 | Parameters (options and arguments) have a name that will be used as 38 | the Python argument name when calling the decorated function with 39 | values. 40 | 41 | .. click:example:: 42 | 43 | @click.command() 44 | @click.argument('filename') 45 | @click.option('-t', '--times', type=int) 46 | def multi_echo(filename, times): 47 | """Print value filename multiple times.""" 48 | for x in range(times): 49 | click.echo(filename) 50 | 51 | In the above 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`. 52 | The option's names are ``-t`` and ``--times``. More names are available for options and are covered in :ref:`options`. 53 | 54 | And what it looks like when run: 55 | 56 | .. click:run:: 57 | 58 | invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo') 59 | -------------------------------------------------------------------------------- /docs/prompts.rst: -------------------------------------------------------------------------------- 1 | User Input Prompts 2 | ================== 3 | 4 | .. currentmodule:: click 5 | 6 | Click supports prompts in two different places. The first is automated 7 | prompts when the parameter handling happens, and the second is to ask for 8 | prompts at a later point independently. 9 | 10 | This can be accomplished with the :func:`prompt` function, which asks for 11 | valid input according to a type, or the :func:`confirm` function, which asks 12 | for confirmation (yes/no). 13 | 14 | .. contents:: 15 | :depth: 2 16 | :local: 17 | 18 | .. _option-prompting: 19 | 20 | Option Prompts 21 | -------------- 22 | 23 | Option prompts are integrated into the option interface. Internally, it 24 | automatically calls either :func:`prompt` or :func:`confirm` as necessary. 25 | 26 | In some cases, you want parameters that can be provided from the command line, 27 | but if not provided, ask for user input instead. This can be implemented with 28 | Click by defining a prompt string. 29 | 30 | Example: 31 | 32 | .. click:example:: 33 | 34 | @click.command() 35 | @click.option('--name', prompt=True) 36 | def hello(name): 37 | click.echo(f"Hello {name}!") 38 | 39 | And what it looks like: 40 | 41 | .. click:run:: 42 | 43 | invoke(hello, args=['--name=John']) 44 | invoke(hello, input=['John']) 45 | 46 | If you are not happy with the default prompt string, you can ask for 47 | a different one: 48 | 49 | .. click:example:: 50 | 51 | @click.command() 52 | @click.option('--name', prompt='Your name please') 53 | def hello(name): 54 | click.echo(f"Hello {name}!") 55 | 56 | What it looks like: 57 | 58 | .. click:run:: 59 | 60 | invoke(hello, input=['John']) 61 | 62 | It is advised that prompt not be used in conjunction with the multiple 63 | flag set to True. Instead, prompt in the function interactively. 64 | 65 | By default, the user will be prompted for an input if one was not passed 66 | through the command line. To turn this behavior off, see 67 | :ref:`optional-value`. 68 | 69 | Input Prompts 70 | ------------- 71 | 72 | To manually ask for user input, you can use the :func:`prompt` function. 73 | By default, it accepts any Unicode string, but you can ask for any other 74 | type. For instance, you can ask for a valid integer:: 75 | 76 | value = click.prompt('Please enter a valid integer', type=int) 77 | 78 | Additionally, the type will be determined automatically if a default value is 79 | provided. For instance, the following will only accept floats:: 80 | 81 | value = click.prompt('Please enter a number', default=42.0) 82 | 83 | Optional Prompts 84 | ------------------ 85 | If the option has ``prompt`` enabled, then setting 86 | ``prompt_required=False`` tells Click to only show the prompt if the 87 | option's flag is given, instead of if the option is not provided at all. 88 | 89 | .. click:example:: 90 | 91 | @click.command() 92 | @click.option('--name', prompt=True, prompt_required=False, default="Default") 93 | def hello(name): 94 | click.echo(f"Hello {name}!") 95 | 96 | .. click:run:: 97 | 98 | invoke(hello) 99 | invoke(hello, args=["--name", "Value"]) 100 | invoke(hello, args=["--name"], input="Prompt") 101 | 102 | If ``required=True``, then the option will still prompt if it is not 103 | given, but it will also prompt if only the flag is given. 104 | 105 | Confirmation Prompts 106 | -------------------- 107 | 108 | To ask if a user wants to continue with an action, the :func:`confirm` 109 | function comes in handy. By default, it returns the result of the prompt 110 | as a boolean value:: 111 | 112 | if click.confirm('Do you want to continue?'): 113 | click.echo('Well done!') 114 | 115 | There is also the option to make the function automatically abort the 116 | execution of the program if it does not return ``True``:: 117 | 118 | click.confirm('Do you want to continue?', abort=True) 119 | 120 | Dynamic Defaults for Prompts 121 | ---------------------------- 122 | 123 | The ``auto_envvar_prefix`` and ``default_map`` options for the context 124 | allow the program to read option values from the environment or a 125 | configuration file. However, this overrides the prompting mechanism, so 126 | that the user does not get the option to change the value interactively. 127 | 128 | If you want to let the user configure the default value, but still be 129 | prompted if the option isn't specified on the command line, you can do so 130 | by supplying a callable as the default value. For example, to get a default 131 | from the environment: 132 | 133 | .. code-block:: python 134 | 135 | import os 136 | 137 | @click.command() 138 | @click.option( 139 | "--username", prompt=True, 140 | default=lambda: os.environ.get("USER", "") 141 | ) 142 | def hello(username): 143 | click.echo(f"Hello, {username}!") 144 | 145 | To describe what the default value will be, set it in ``show_default``. 146 | 147 | .. click:example:: 148 | 149 | import os 150 | 151 | @click.command() 152 | @click.option( 153 | "--username", prompt=True, 154 | default=lambda: os.environ.get("USER", ""), 155 | show_default="current user" 156 | ) 157 | def hello(username): 158 | click.echo(f"Hello, {username}!") 159 | 160 | .. click:run:: 161 | 162 | invoke(hello, args=["--help"]) 163 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | .. currentmodule:: click 5 | 6 | Install 7 | ---------------------- 8 | Install from PyPI:: 9 | 10 | pip install click 11 | 12 | Installing into a virtual environment is highly recommended. We suggest :ref:`virtualenv-heading`. 13 | 14 | Examples 15 | ----------------------- 16 | 17 | Some standalone examples of Click applications are packaged with Click. They are available in the `examples folder `_ of the repo. 18 | 19 | * `inout `_ : A very simple example of an application that can read from files and write to files and also accept input from stdin or write to stdout. 20 | * `validation `_ : A simple example of an application that performs custom validation of parameters in different ways. 21 | * `naval `_ : Port of the `docopt `_ naval example. 22 | * `colors `_ : A simple example that colorizes text. Uses colorama on Windows. 23 | * `aliases `_ : An advanced example that implements :ref:`aliases`. 24 | * `imagepipe `_ : A complex example that implements some :ref:`command-pipelines` . It chains together image processing instructions. Requires pillow. 25 | * `repo `_ : An advanced example that implements a Git-/Mercurial-like command line interface. 26 | * `complex `_ : A very advanced example that implements loading subcommands dynamically from a plugin folder. 27 | * `termui `_ : A simple example that showcases terminal UI helpers provided by click. 28 | 29 | Basic Concepts - Creating a Command 30 | ----------------------------------- 31 | 32 | Click is based on declaring commands through decorators. Internally, there 33 | is a non-decorator interface for advanced use cases, but it's discouraged 34 | for high-level usage. 35 | 36 | A function becomes a Click command line tool by decorating it through 37 | :func:`click.command`. At its simplest, just decorating a function 38 | with this decorator will make it into a callable script: 39 | 40 | .. click:example:: 41 | 42 | import click 43 | 44 | @click.command() 45 | def hello(): 46 | click.echo('Hello World!') 47 | 48 | What's happening is that the decorator converts the function into a 49 | :class:`Command` which then can be invoked:: 50 | 51 | if __name__ == '__main__': 52 | hello() 53 | 54 | And what it looks like: 55 | 56 | .. click:run:: 57 | 58 | invoke(hello, args=[], prog_name='python hello.py') 59 | 60 | And the corresponding help page: 61 | 62 | .. click:run:: 63 | 64 | invoke(hello, args=['--help'], prog_name='python hello.py') 65 | 66 | Echoing 67 | ------- 68 | 69 | Why does this example use :func:`echo` instead of the regular 70 | :func:`print` function? The answer to this question is that Click 71 | attempts to support different environments consistently and to be very 72 | robust even when the environment is misconfigured. Click wants to be 73 | functional at least on a basic level even if everything is completely 74 | broken. 75 | 76 | What this means is that the :func:`echo` function applies some error 77 | correction in case the terminal is misconfigured instead of dying with a 78 | :exc:`UnicodeError`. 79 | 80 | The echo function also supports color and other styles in output. It 81 | will automatically remove styles if the output stream is a file. On 82 | Windows, colorama is automatically installed and used. See 83 | :ref:`ansi-colors`. 84 | 85 | If you don't need this, you can also use the `print()` construct / 86 | function. 87 | 88 | Nesting Commands 89 | ---------------- 90 | 91 | Commands can be attached to other commands of type :class:`Group`. This 92 | allows arbitrary nesting of scripts. As an example here is a script that 93 | implements two commands for managing databases: 94 | 95 | .. click:example:: 96 | 97 | @click.group() 98 | def cli(): 99 | pass 100 | 101 | @click.command() 102 | def initdb(): 103 | click.echo('Initialized the database') 104 | 105 | @click.command() 106 | def dropdb(): 107 | click.echo('Dropped the database') 108 | 109 | cli.add_command(initdb) 110 | cli.add_command(dropdb) 111 | 112 | As you can see, the :func:`group` decorator works like the :func:`command` 113 | decorator, but creates a :class:`Group` object instead which can be given 114 | multiple subcommands that can be attached with :meth:`Group.add_command`. 115 | 116 | For simple scripts, it's also possible to automatically attach and create a 117 | command by using the :meth:`Group.command` decorator instead. The above 118 | script can instead be written like this: 119 | 120 | .. click:example:: 121 | 122 | @click.group() 123 | def cli(): 124 | pass 125 | 126 | @cli.command() 127 | def initdb(): 128 | click.echo('Initialized the database') 129 | 130 | @cli.command() 131 | def dropdb(): 132 | click.echo('Dropped the database') 133 | 134 | You would then invoke the :class:`Group` in your entry points or other invocations:: 135 | 136 | if __name__ == '__main__': 137 | cli() 138 | 139 | 140 | Registering Commands Later 141 | -------------------------- 142 | 143 | Instead of using the ``@group.command()`` decorator, commands can be 144 | decorated with the plain ``@click.command()`` decorator and registered 145 | with a group later with ``group.add_command()``. This could be used to 146 | split commands into multiple Python modules. 147 | 148 | .. code-block:: python 149 | 150 | @click.command() 151 | def greet(): 152 | click.echo("Hello, World!") 153 | 154 | .. code-block:: python 155 | 156 | @click.group() 157 | def group(): 158 | pass 159 | 160 | group.add_command(greet) 161 | 162 | 163 | Adding Parameters 164 | ----------------- 165 | 166 | To add parameters, use the :func:`option` and :func:`argument` decorators: 167 | 168 | .. click:example:: 169 | 170 | @click.command() 171 | @click.option('--count', default=1, help='number of greetings') 172 | @click.argument('name') 173 | def hello(count, name): 174 | for x in range(count): 175 | click.echo(f"Hello {name}!") 176 | 177 | What it looks like: 178 | 179 | .. click:run:: 180 | 181 | invoke(hello, args=['--help'], prog_name='python hello.py') 182 | 183 | Switching to Entry Points 184 | ------------------------- 185 | 186 | In the code you wrote so far there is a block at the end of the file which 187 | looks like this: ``if __name__ == '__main__':``. This is traditionally 188 | how a standalone Python file looks like. With Click you can continue 189 | doing that, but a better way is to package your app with an entry point. 190 | 191 | There are two main (and many more) reasons for this: 192 | 193 | The first one is that installers automatically generate executable 194 | wrappers for Windows so your command line utilities work on Windows too. 195 | 196 | The second reason is that entry point scripts work with virtualenv on Unix 197 | without the virtualenv having to be activated. This is a very useful 198 | concept which allows you to bundle your scripts with all requirements into 199 | a virtualenv. 200 | 201 | Click is perfectly equipped to work with that and in fact the rest of the 202 | documentation will assume that you are writing applications as distributed 203 | packages. 204 | 205 | Look at the :doc:`entry-points` chapter before reading the rest as the examples 206 | assume that you will be using entry points. 207 | -------------------------------------------------------------------------------- /docs/setuptools.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | Setuptools Integration 4 | ====================== 5 | 6 | Moved to :doc:`entry-points`. 7 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /docs/unicode-support.rst: -------------------------------------------------------------------------------- 1 | Unicode Support 2 | =============== 3 | 4 | .. currentmodule:: click 5 | 6 | Click has to take extra care to support Unicode text in different 7 | environments. 8 | 9 | * The command line in Unix is traditionally bytes, not Unicode. While 10 | there are encoding hints, there are some situations where this can 11 | break. The most common one is SSH connections to machines with 12 | different locales. 13 | 14 | Misconfigured environments can cause a wide range of Unicode 15 | problems due to the lack of support for roundtripping surrogate 16 | escapes. This will not be fixed in Click itself! 17 | 18 | * Standard input and output is opened in text mode by default. Click 19 | has to reopen the stream in binary mode in certain situations. 20 | Because there is no standard way to do this, it might not always 21 | work. Primarily this can become a problem when testing command-line 22 | applications. 23 | 24 | This is not supported:: 25 | 26 | sys.stdin = io.StringIO('Input here') 27 | sys.stdout = io.StringIO() 28 | 29 | Instead you need to do this:: 30 | 31 | input = 'Input here' 32 | in_stream = io.BytesIO(input.encode('utf-8')) 33 | sys.stdin = io.TextIOWrapper(in_stream, encoding='utf-8') 34 | out_stream = io.BytesIO() 35 | sys.stdout = io.TextIOWrapper(out_stream, encoding='utf-8') 36 | 37 | Remember in that case, you need to use ``out_stream.getvalue()`` 38 | and not ``sys.stdout.getvalue()`` if you want to access the buffer 39 | contents as the wrapper will not forward that method. 40 | 41 | * ``sys.stdin``, ``sys.stdout`` and ``sys.stderr`` are by default 42 | text-based. When Click needs a binary stream, it attempts to 43 | discover the underlying binary stream. 44 | 45 | * ``sys.argv`` is always text. This means that the native type for 46 | input values to the types in Click is Unicode, not bytes. 47 | 48 | This causes problems if the terminal is incorrectly set and Python 49 | does not figure out the encoding. In that case, the Unicode string 50 | will contain error bytes encoded as surrogate escapes. 51 | 52 | * When dealing with files, Click will always use the Unicode file 53 | system API by using the operating system's reported or guessed 54 | filesystem encoding. Surrogates are supported for filenames, so it 55 | should be possible to open files through the :class:`File` type even 56 | if the environment is misconfigured. 57 | 58 | 59 | Surrogate Handling 60 | ------------------ 61 | 62 | Click does all the Unicode handling in the standard library and is 63 | subject to its behavior. Unicode requires extra care. The reason for 64 | this is that the encoding detection is done in the interpreter, and on 65 | Linux and certain other operating systems, its encoding handling is 66 | problematic. 67 | 68 | The biggest source of frustration is that Click scripts invoked by init 69 | systems, deployment tools, or cron jobs will refuse to work unless a 70 | Unicode locale is exported. 71 | 72 | If Click encounters such an environment it will prevent further 73 | execution to force you to set a locale. This is done because Click 74 | cannot know about the state of the system once it's invoked and restore 75 | the values before Python's Unicode handling kicked in. 76 | 77 | If you see something like this error:: 78 | 79 | Traceback (most recent call last): 80 | ... 81 | RuntimeError: Click will abort further execution because Python was 82 | configured to use ASCII as encoding for the environment. Consult 83 | https://click.palletsprojects.com/unicode-support/ for mitigation 84 | steps. 85 | 86 | You are dealing with an environment where Python thinks you are 87 | restricted to ASCII data. The solution to these problems is different 88 | depending on which locale your computer is running in. 89 | 90 | For instance, if you have a German Linux machine, you can fix the 91 | problem by exporting the locale to ``de_DE.utf-8``:: 92 | 93 | export LC_ALL=de_DE.utf-8 94 | export LANG=de_DE.utf-8 95 | 96 | If you are on a US machine, ``en_US.utf-8`` is the encoding of choice. 97 | On some newer Linux systems, you could also try ``C.UTF-8`` as the 98 | locale:: 99 | 100 | export LC_ALL=C.UTF-8 101 | export LANG=C.UTF-8 102 | 103 | On some systems it was reported that ``UTF-8`` has to be written as 104 | ``UTF8`` and vice versa. To see which locales are supported you can 105 | invoke ``locale -a``. 106 | 107 | You need to export the values before you invoke your Python script. 108 | 109 | In Python 3.7 and later you will no longer get a ``RuntimeError`` in 110 | many cases thanks to :pep:`538` and :pep:`540`, which changed the 111 | default assumption in unconfigured environments. This doesn't change the 112 | general issue that your locale may be misconfigured. 113 | -------------------------------------------------------------------------------- /docs/virtualenv.rst: -------------------------------------------------------------------------------- 1 | .. _virtualenv-heading: 2 | 3 | Virtualenv 4 | ========================= 5 | 6 | Why Use Virtualenv? 7 | ------------------------- 8 | 9 | You should use `Virtualenv `_ because: 10 | 11 | * It allows you to install multiple versions of the same dependency. 12 | 13 | * If you have an operating system version of Python, it prevents you from changing its dependencies and potentially messing up your os. 14 | 15 | How to Use Virtualenv 16 | ----------------------------- 17 | 18 | Create your project folder, then a virtualenv within it:: 19 | 20 | $ mkdir myproject 21 | $ cd myproject 22 | $ python3 -m venv .venv 23 | 24 | Now, whenever you want to work on a project, you only have to activate the 25 | corresponding environment. 26 | 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 | $ pip install click 49 | 50 | And if you want to stop using the virtualenv, use the following command:: 51 | 52 | $ deactivate 53 | 54 | After doing this, the prompt of your shell should be as familiar as before. 55 | -------------------------------------------------------------------------------- /docs/why.rst: -------------------------------------------------------------------------------- 1 | Why Click? 2 | ========== 3 | 4 | There are so many libraries out there for writing command line utilities; 5 | why does Click exist? 6 | 7 | This question is easy to answer: because there is not a single command 8 | line utility for Python out there which ticks the following boxes: 9 | 10 | * Is lazily composable without restrictions. 11 | * Supports implementation of Unix/POSIX command line conventions. 12 | * Supports loading values from environment variables out of the box. 13 | * Support for prompting of custom values. 14 | * Is fully nestable and composable. 15 | * Supports file handling out of the box. 16 | * Comes with useful common helpers (getting terminal dimensions, 17 | ANSI colors, fetching direct keyboard input, screen clearing, 18 | finding config paths, launching apps and editors, etc.). 19 | 20 | There are many alternatives to Click; the obvious ones are ``optparse`` 21 | and ``argparse`` from the standard library. Have a look to see if something 22 | else resonates with you. 23 | 24 | Click actually implements its own parsing of arguments and does not use 25 | ``optparse`` or ``argparse`` following the ``optparse`` parsing behavior. 26 | The reason it's not based on ``argparse`` is that ``argparse`` does not 27 | allow proper nesting of commands by design and has some deficiencies when 28 | it comes to POSIX compliant argument handling. 29 | 30 | Click is designed to be fun and customizable but not overly flexible. 31 | For instance, the customizability of help pages is constrained. This 32 | constraint is intentional because Click promises multiple Click instances 33 | will continue to function as intended when strung together. 34 | 35 | Too much customizability would break this promise. 36 | 37 | Click was written to support the `Flask `_ 38 | microframework ecosystem because no tool could provide it with the 39 | functionality it needed. 40 | 41 | To get an understanding of what Click is all about, I strongly recommend 42 | looking at the :ref:`complex-guide` chapter. 43 | 44 | Why not Argparse? 45 | ----------------- 46 | 47 | Click is internally based on ``optparse`` instead of ``argparse``. This 48 | is an implementation detail that a user does not have to be concerned 49 | with. Click is not based on ``argparse`` because it has some behaviors that 50 | make handling arbitrary command line interfaces hard: 51 | 52 | * ``argparse`` has built-in behavior to guess if something is an 53 | argument or an option. This becomes a problem when dealing with 54 | incomplete command lines; the behaviour becomes unpredictable 55 | without full knowledge of a command line. This goes against Click's 56 | ambitions of dispatching to subparsers. 57 | * ``argparse`` does not support disabling interspersed arguments. Without 58 | this feature, it's not possible to safely implement Click's nested 59 | parsing. 60 | 61 | Why not Docopt etc.? 62 | -------------------- 63 | 64 | Docopt, and many tools like it, are cool in how they work, but very few of 65 | these tools deal with nesting of commands and composability in a way like 66 | Click. To the best of the developer's knowledge, Click is the first 67 | Python library that aims to create a level of composability of applications 68 | that goes beyond what the system itself supports. 69 | 70 | Docopt, for instance, acts by parsing your help pages and then parsing 71 | according to those rules. The side effect of this is that docopt is quite 72 | rigid in how it handles the command line interface. The upside of docopt 73 | is that it gives you strong control over your help page; the downside is 74 | that due to this it cannot rewrap your output for the current terminal 75 | width, and it makes translations hard. On top of that, docopt is restricted 76 | to basic parsing. It does not handle argument dispatching and callback 77 | invocation or types. This means there is a lot of code that needs to be 78 | written in addition to the basic help page to handle the parsing results. 79 | 80 | Most of all, however, it makes composability hard. While docopt does 81 | support dispatching to subcommands, it, for instance, does not directly 82 | support any kind of automatic subcommand enumeration based on what's 83 | available or it does not enforce subcommands to work in a consistent way. 84 | 85 | This is fine, but it's different from how Click wants to work. Click aims 86 | to support fully composable command line user interfaces by doing the 87 | following: 88 | 89 | - Click does not just parse, it also dispatches to the appropriate code. 90 | - Click has a strong concept of an invocation context that allows 91 | subcommands to respond to data from the parent command. 92 | - Click has strong information available for all parameters and commands, 93 | so it can generate unified help pages for the full CLI and 94 | assist the user in converting the input data as necessary. 95 | - Click has a strong understanding of what types are, and it can give the user 96 | consistent error messages if something goes wrong. A subcommand 97 | written by a different developer will not suddenly die with a 98 | different error message because it's manually handled. 99 | - Click has enough meta information available for its whole program 100 | to evolve over time and improve the user experience without 101 | forcing developers to adjust their programs. For instance, if Click 102 | decides to change how help pages are formatted, all Click programs 103 | will automatically benefit from this. 104 | 105 | The aim of Click is to make composable systems. Whereas, the aim of docopt 106 | is to build the most beautiful and hand-crafted command line interfaces. 107 | These two goals conflict with one another in subtle ways. Click 108 | actively prevents people from implementing certain patterns in order to 109 | achieve unified command line interfaces. For instance, as a developer, you 110 | are given very little choice in formatting your help pages. 111 | 112 | 113 | Why Hardcoded Behaviors? 114 | ------------------------ 115 | 116 | The other question is why Click goes away from optparse and hardcodes 117 | certain behaviors instead of staying configurable. There are multiple 118 | reasons for this. The biggest one is that too much configurability makes 119 | it hard to achieve a consistent command line experience. 120 | 121 | The best example for this is optparse's ``callback`` functionality for 122 | accepting an arbitrary number of arguments. Due to syntactical ambiguities 123 | on the command line, there is no way to implement fully variadic arguments. 124 | There are always tradeoffs that need to be made and in case of 125 | ``argparse`` these tradeoffs have been critical enough, that a system like 126 | Click cannot even be implemented on top of it. 127 | 128 | In this particular case, Click attempts to stay with a handful of accepted 129 | paradigms for building command line interfaces that can be well documented 130 | and tested. 131 | 132 | 133 | Why No Auto Correction? 134 | ----------------------- 135 | 136 | The question came up why Click does not auto correct parameters given that 137 | even optparse and ``argparse`` support automatic expansion of long arguments. 138 | The reason for this is that it's a liability for backwards compatibility. 139 | If people start relying on automatically modified parameters and someone 140 | adds a new parameter in the future, the script might stop working. These 141 | kinds of problems are hard to find, so Click does not attempt to be magical 142 | about this. 143 | 144 | This sort of behavior however can be implemented on a higher level to 145 | support things such as explicit aliases. For more information see 146 | :ref:`aliases`. 147 | -------------------------------------------------------------------------------- /docs/wincmd.rst: -------------------------------------------------------------------------------- 1 | Windows Console Notes 2 | ===================== 3 | 4 | .. versionadded:: 6.0 5 | 6 | Click emulates output streams on Windows to support unicode to the 7 | Windows console through separate APIs and we perform different decoding of 8 | parameters. 9 | 10 | Here is a brief overview of how this works and what it means to you. 11 | 12 | Unicode Arguments 13 | ----------------- 14 | 15 | Click internally is generally based on the concept that any argument can 16 | come in as either byte string or unicode string and conversion is 17 | performed to the type expected value as late as possible. This has some 18 | advantages as it allows us to accept the data in the most appropriate form 19 | for the operating system and Python version. 20 | 21 | This caused some problems on Windows where initially the wrong encoding 22 | was used and garbage ended up in your input data. We not only fixed the 23 | encoding part, but we also now extract unicode parameters from `sys.argv`. 24 | 25 | There is also another limitation with this: if `sys.argv` was modified 26 | prior to invoking a click handler, we have to fall back to the regular 27 | byte input in which case not all unicode values are available but only a 28 | subset of the codepage used for parameters. 29 | 30 | Unicode Output and Input 31 | ------------------------ 32 | 33 | Unicode output and input on Windows is implemented through the concept of 34 | a dispatching text stream. What this means is that when click first needs 35 | a text output (or input) stream on windows it goes through a few checks to 36 | figure out of a windows console is connected or not. If no Windows 37 | console is present then the text output stream is returned as such and the 38 | encoding for that stream is set to ``utf-8`` like on all platforms. 39 | 40 | However if a console is connected the stream will instead be emulated and 41 | use the cmd.exe unicode APIs to output text information. In this case the 42 | stream will also use ``utf-16-le`` as internal encoding. However there is 43 | some hackery going on that the underlying raw IO buffer is still bypassing 44 | the unicode APIs and byte output through an indirection is still possible. 45 | 46 | * This unicode support is limited to ``click.echo``, ``click.prompt`` as 47 | well as ``click.get_text_stream``. 48 | * Depending on if unicode values or byte strings are passed the control 49 | flow goes completely different places internally which can have some 50 | odd artifacts if data partially ends up being buffered. Click 51 | attempts to protect against that by manually always flushing but if 52 | you are mixing and matching different string types to ``stdout`` or 53 | ``stderr`` you will need to manually flush. 54 | * The raw output stream is set to binary mode, which is a global 55 | operation on Windows, so ``print`` calls will be affected. Prefer 56 | ``click.echo`` over ``print``. 57 | * On Windows 7 and below, there is a limitation where at most 64k 58 | characters can be written in one call in binary mode. In this 59 | situation, ``sys.stdout`` and ``sys.stderr`` are replaced with 60 | wrappers that work around the limitation. 61 | 62 | Another important thing to note is that the Windows console's default 63 | fonts do not support a lot of characters which means that you are mostly 64 | limited to international letters but no emojis or special characters. 65 | -------------------------------------------------------------------------------- /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/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/aliases/aliases.ini: -------------------------------------------------------------------------------- 1 | [aliases] 2 | ci=commit 3 | -------------------------------------------------------------------------------- /examples/aliases/aliases.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | 4 | import 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /examples/colors/colors.py: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /examples/completion/completion.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | from click.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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /examples/complex/complex/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/examples/complex/complex/__init__.py -------------------------------------------------------------------------------- /examples/complex/complex/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import 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 | -------------------------------------------------------------------------------- /examples/complex/complex/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/examples/complex/complex/commands/__init__.py -------------------------------------------------------------------------------- /examples/complex/complex/commands/cmd_init.py: -------------------------------------------------------------------------------- 1 | from complex.cli import pass_environment 2 | 3 | import 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 | -------------------------------------------------------------------------------- /examples/complex/complex/commands/cmd_status.py: -------------------------------------------------------------------------------- 1 | from complex.cli import pass_environment 2 | 3 | import 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 | -------------------------------------------------------------------------------- /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/imagepipe/.gitignore: -------------------------------------------------------------------------------- 1 | processed-* 2 | -------------------------------------------------------------------------------- /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/imagepipe/example01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/examples/imagepipe/example01.jpg -------------------------------------------------------------------------------- /examples/imagepipe/example02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/examples/imagepipe/example02.jpg -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/inout/inout.py: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /examples/naval/naval.py: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/repo/repo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import posixpath 3 | import sys 4 | 5 | import 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 | -------------------------------------------------------------------------------- /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/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/termui/termui.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import time 4 | 5 | import 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/validation/validation.py: -------------------------------------------------------------------------------- 1 | from urllib import parse as urlparse 2 | 3 | import 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 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "click" 3 | version = "8.2.1" 4 | description = "Composable command line interface toolkit" 5 | readme = "README.md" 6 | license = "BSD-3-Clause" 7 | license-files = ["LICENSE.txt"] 8 | maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] 9 | classifiers = [ 10 | "Development Status :: 5 - Production/Stable", 11 | "Intended Audience :: Developers", 12 | "Operating System :: OS Independent", 13 | "Programming Language :: Python", 14 | "Typing :: Typed", 15 | ] 16 | requires-python = ">=3.10" 17 | dependencies = [ 18 | "colorama; platform_system == 'Windows'", 19 | ] 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 | "pytest", 53 | ] 54 | typing = [ 55 | "mypy", 56 | "pyright", 57 | "pytest", 58 | ] 59 | 60 | [build-system] 61 | requires = ["flit_core<4"] 62 | build-backend = "flit_core.buildapi" 63 | 64 | [tool.flit.module] 65 | name = "click" 66 | 67 | [tool.flit.sdist] 68 | include = [ 69 | "docs/", 70 | "tests/", 71 | "CHANGES.rst", 72 | "uv.lock" 73 | ] 74 | exclude = [ 75 | "docs/_build/", 76 | ] 77 | 78 | [tool.uv] 79 | default-groups = ["dev", "pre-commit", "tests", "typing"] 80 | 81 | [tool.pytest.ini_options] 82 | testpaths = ["tests"] 83 | filterwarnings = [ 84 | "error", 85 | ] 86 | 87 | [tool.coverage.run] 88 | branch = true 89 | source = ["click", "tests"] 90 | 91 | [tool.coverage.paths] 92 | source = ["src", "*/site-packages"] 93 | 94 | [tool.coverage.report] 95 | exclude_also = [ 96 | "if t.TYPE_CHECKING", 97 | "raise NotImplementedError", 98 | ": \\.{3}", 99 | ] 100 | 101 | [tool.mypy] 102 | python_version = "3.10" 103 | files = ["src", "tests/typing"] 104 | show_error_codes = true 105 | pretty = true 106 | strict = true 107 | 108 | [[tool.mypy.overrides]] 109 | module = [ 110 | "colorama.*", 111 | ] 112 | ignore_missing_imports = true 113 | 114 | [tool.pyright] 115 | pythonVersion = "3.10" 116 | include = ["src", "tests/typing"] 117 | typeCheckingMode = "basic" 118 | 119 | [tool.ruff] 120 | extend-exclude = ["examples/"] 121 | src = ["src"] 122 | fix = true 123 | show-fixes = true 124 | output-format = "full" 125 | 126 | [tool.ruff.lint] 127 | select = [ 128 | "B", # flake8-bugbear 129 | "E", # pycodestyle error 130 | "F", # pyflakes 131 | "I", # isort 132 | "UP", # pyupgrade 133 | "W", # pycodestyle warning 134 | ] 135 | ignore = [ 136 | "UP038", # keep isinstance tuple 137 | ] 138 | 139 | [tool.ruff.lint.isort] 140 | force-single-line = true 141 | order-by-type = false 142 | 143 | [tool.gha-update] 144 | tag-only = [ 145 | "slsa-framework/slsa-github-generator", 146 | ] 147 | 148 | [tool.tox] 149 | env_list = [ 150 | "py3.13", "py3.12", "py3.11", "py3.10", 151 | "pypy3.11", 152 | "style", 153 | "typing", 154 | "docs", 155 | ] 156 | 157 | [tool.tox.env_run_base] 158 | description = "pytest on latest dependency versions" 159 | runner = "uv-venv-lock-runner" 160 | package = "wheel" 161 | wheel_build_env = ".pkg" 162 | constrain_package_deps = true 163 | use_frozen_constraints = true 164 | dependency_groups = ["tests"] 165 | commands = [[ 166 | "pytest", "-v", "--tb=short", "--basetemp={env_tmp_dir}", 167 | {replace = "posargs", default = [], extend = true}, 168 | ]] 169 | 170 | [tool.tox.env.style] 171 | description = "run all pre-commit hooks on all files" 172 | dependency_groups = ["pre-commit"] 173 | skip_install = true 174 | commands = [["pre-commit", "run", "--all-files"]] 175 | 176 | [tool.tox.env.typing] 177 | description = "run static type checkers" 178 | dependency_groups = ["typing"] 179 | commands = [ 180 | ["mypy"], 181 | ] 182 | 183 | [tool.tox.env.docs] 184 | description = "build docs" 185 | dependency_groups = ["docs"] 186 | commands = [["sphinx-build", "-E", "-W", "-b", "dirhtml", "docs", "docs/_build/dirhtml"]] 187 | 188 | [tool.tox.env.docs-auto] 189 | description = "continuously rebuild docs and start a local server" 190 | dependency_groups = ["docs", "docs-auto"] 191 | commands = [["sphinx-autobuild", "-W", "-b", "dirhtml", "--watch", "src", "docs", "docs/_build/dirhtml"]] 192 | 193 | [tool.tox.env.update-actions] 194 | description = "update GitHub Actions pins" 195 | labels = ["update"] 196 | dependency_groups = ["gha-update"] 197 | skip_install = true 198 | commands = [["gha-update"]] 199 | 200 | [tool.tox.env.update-pre_commit] 201 | description = "update pre-commit pins" 202 | labels = ["update"] 203 | dependency_groups = ["pre-commit"] 204 | skip_install = true 205 | commands = [["pre-commit", "autoupdate", "--freeze", "-j4"]] 206 | 207 | [tool.tox.env.update-requirements] 208 | description = "update uv lock" 209 | labels = ["update"] 210 | dependency_groups = [] 211 | no_default_groups = true 212 | skip_install = true 213 | commands = [["uv", "lock", {replace = "posargs", default = ["-U"], extend = true}]] 214 | -------------------------------------------------------------------------------- /src/click/__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 | -------------------------------------------------------------------------------- /src/click/_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 | -------------------------------------------------------------------------------- /src/click/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 | -------------------------------------------------------------------------------- /src/click/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pallets/click/b63239d657aab7c4e04667a62e7177f617b37230/src/click/py.typed -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from click.testing import CliRunner 4 | 5 | 6 | @pytest.fixture(scope="function") 7 | def runner(request): 8 | return CliRunner() 9 | -------------------------------------------------------------------------------- /tests/test_chain.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | import 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 | # last two lines are '' and 'Aborted!' 167 | assert result.output.splitlines()[:-2] == 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 | -------------------------------------------------------------------------------- /tests/test_command_decorators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import 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 | -------------------------------------------------------------------------------- /tests/test_compat.py: -------------------------------------------------------------------------------- 1 | from click._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 | -------------------------------------------------------------------------------- /tests/test_custom_classes.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | 4 | def test_command_context_class(): 5 | """A command with a custom ``context_class`` should produce a 6 | context using that type. 7 | """ 8 | 9 | class CustomContext(click.Context): 10 | pass 11 | 12 | class CustomCommand(click.Command): 13 | context_class = CustomContext 14 | 15 | command = CustomCommand("test") 16 | context = command.make_context("test", []) 17 | assert isinstance(context, CustomContext) 18 | 19 | 20 | def test_context_invoke_type(runner): 21 | """A command invoked from a custom context should have a new 22 | context with the same type. 23 | """ 24 | 25 | class CustomContext(click.Context): 26 | pass 27 | 28 | class CustomCommand(click.Command): 29 | context_class = CustomContext 30 | 31 | @click.command() 32 | @click.argument("first_id", type=int) 33 | @click.pass_context 34 | def second(ctx, first_id): 35 | assert isinstance(ctx, CustomContext) 36 | assert id(ctx) != first_id 37 | 38 | @click.command(cls=CustomCommand) 39 | @click.pass_context 40 | def first(ctx): 41 | assert isinstance(ctx, CustomContext) 42 | ctx.invoke(second, first_id=id(ctx)) 43 | 44 | assert not runner.invoke(first).exception 45 | 46 | 47 | def test_context_formatter_class(): 48 | """A context with a custom ``formatter_class`` should format help 49 | using that type. 50 | """ 51 | 52 | class CustomFormatter(click.HelpFormatter): 53 | def write_heading(self, heading): 54 | heading = click.style(heading, fg="yellow") 55 | return super().write_heading(heading) 56 | 57 | class CustomContext(click.Context): 58 | formatter_class = CustomFormatter 59 | 60 | context = CustomContext( 61 | click.Command("test", params=[click.Option(["--value"])]), color=True 62 | ) 63 | assert "\x1b[33mOptions\x1b[0m:" in context.get_help() 64 | 65 | 66 | def test_group_command_class(runner): 67 | """A group with a custom ``command_class`` should create subcommands 68 | of that type by default. 69 | """ 70 | 71 | class CustomCommand(click.Command): 72 | pass 73 | 74 | class CustomGroup(click.Group): 75 | command_class = CustomCommand 76 | 77 | group = CustomGroup() 78 | subcommand = group.command()(lambda: None) 79 | assert type(subcommand) is CustomCommand 80 | subcommand = group.command(cls=click.Command)(lambda: None) 81 | assert type(subcommand) is click.Command 82 | 83 | 84 | def test_group_group_class(runner): 85 | """A group with a custom ``group_class`` should create subgroups 86 | of that type by default. 87 | """ 88 | 89 | class CustomSubGroup(click.Group): 90 | pass 91 | 92 | class CustomGroup(click.Group): 93 | group_class = CustomSubGroup 94 | 95 | group = CustomGroup() 96 | subgroup = group.group()(lambda: None) 97 | assert type(subgroup) is CustomSubGroup 98 | subgroup = group.command(cls=click.Group)(lambda: None) 99 | assert type(subgroup) is click.Group 100 | 101 | 102 | def test_group_group_class_self(runner): 103 | """A group with ``group_class = type`` should create subgroups of 104 | the same type as itself. 105 | """ 106 | 107 | class CustomGroup(click.Group): 108 | group_class = type 109 | 110 | group = CustomGroup() 111 | subgroup = group.group()(lambda: None) 112 | assert type(subgroup) is CustomGroup 113 | -------------------------------------------------------------------------------- /tests/test_defaults.py: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | import json 2 | import subprocess 3 | import sys 4 | 5 | from click._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 globals['__name__'].startswith('click') and level == 0: 18 | found_imports.add(module) 19 | return rv 20 | builtins.__import__ = tracking_import 21 | 22 | import click 23 | rv = list(found_imports) 24 | import json 25 | click.echo(json.dumps(rv)) 26 | """ 27 | 28 | ALLOWED_IMPORTS = { 29 | "__future__", 30 | "weakref", 31 | "os", 32 | "struct", 33 | "collections", 34 | "collections.abc", 35 | "sys", 36 | "contextlib", 37 | "functools", 38 | "stat", 39 | "re", 40 | "codecs", 41 | "inspect", 42 | "itertools", 43 | "io", 44 | "threading", 45 | "errno", 46 | "fcntl", 47 | "datetime", 48 | "enum", 49 | "typing", 50 | "types", 51 | "gettext", 52 | "shutil", 53 | } 54 | 55 | if WIN: 56 | ALLOWED_IMPORTS.update(["ctypes", "ctypes.wintypes", "msvcrt", "time"]) 57 | 58 | 59 | def test_light_imports(): 60 | c = subprocess.Popen( 61 | [sys.executable, "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE 62 | ) 63 | rv = c.communicate(IMPORT_TEST)[0] 64 | rv = rv.decode("utf-8") 65 | imported = json.loads(rv) 66 | 67 | for module in imported: 68 | if module == "click" or module.startswith("click."): 69 | continue 70 | assert module in ALLOWED_IMPORTS 71 | -------------------------------------------------------------------------------- /tests/test_info_dict.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import click.types 4 | 5 | # Common (obj, expect) pairs used to construct multiple tests. 6 | STRING_PARAM_TYPE = (click.STRING, {"param_type": "String", "name": "text"}) 7 | INT_PARAM_TYPE = (click.INT, {"param_type": "Int", "name": "integer"}) 8 | BOOL_PARAM_TYPE = (click.BOOL, {"param_type": "Bool", "name": "boolean"}) 9 | HELP_OPTION = ( 10 | None, 11 | { 12 | "name": "help", 13 | "param_type_name": "option", 14 | "opts": ["--help"], 15 | "secondary_opts": [], 16 | "type": BOOL_PARAM_TYPE[1], 17 | "required": False, 18 | "nargs": 1, 19 | "multiple": False, 20 | "default": False, 21 | "envvar": None, 22 | "help": "Show this message and exit.", 23 | "prompt": None, 24 | "is_flag": True, 25 | "flag_value": True, 26 | "count": False, 27 | "hidden": False, 28 | }, 29 | ) 30 | NAME_ARGUMENT = ( 31 | click.Argument(["name"]), 32 | { 33 | "name": "name", 34 | "param_type_name": "argument", 35 | "opts": ["name"], 36 | "secondary_opts": [], 37 | "type": STRING_PARAM_TYPE[1], 38 | "required": True, 39 | "nargs": 1, 40 | "multiple": False, 41 | "default": None, 42 | "envvar": None, 43 | }, 44 | ) 45 | NUMBER_OPTION = ( 46 | click.Option(["-c", "--count", "number"], default=1), 47 | { 48 | "name": "number", 49 | "param_type_name": "option", 50 | "opts": ["-c", "--count"], 51 | "secondary_opts": [], 52 | "type": INT_PARAM_TYPE[1], 53 | "required": False, 54 | "nargs": 1, 55 | "multiple": False, 56 | "default": 1, 57 | "envvar": None, 58 | "help": None, 59 | "prompt": None, 60 | "is_flag": False, 61 | "flag_value": None, 62 | "count": False, 63 | "hidden": False, 64 | }, 65 | ) 66 | HELLO_COMMAND = ( 67 | click.Command("hello", params=[NUMBER_OPTION[0]]), 68 | { 69 | "name": "hello", 70 | "params": [NUMBER_OPTION[1], HELP_OPTION[1]], 71 | "help": None, 72 | "epilog": None, 73 | "short_help": None, 74 | "hidden": False, 75 | "deprecated": False, 76 | }, 77 | ) 78 | HELLO_GROUP = ( 79 | click.Group("cli", [HELLO_COMMAND[0]]), 80 | { 81 | "name": "cli", 82 | "params": [HELP_OPTION[1]], 83 | "help": None, 84 | "epilog": None, 85 | "short_help": None, 86 | "hidden": False, 87 | "deprecated": False, 88 | "commands": {"hello": HELLO_COMMAND[1]}, 89 | "chain": False, 90 | }, 91 | ) 92 | 93 | 94 | @pytest.mark.parametrize( 95 | ("obj", "expect"), 96 | [ 97 | pytest.param( 98 | click.types.FuncParamType(range), 99 | {"param_type": "Func", "name": "range", "func": range}, 100 | id="Func ParamType", 101 | ), 102 | pytest.param( 103 | click.UNPROCESSED, 104 | {"param_type": "Unprocessed", "name": "text"}, 105 | id="UNPROCESSED ParamType", 106 | ), 107 | pytest.param(*STRING_PARAM_TYPE, id="STRING ParamType"), 108 | pytest.param( 109 | click.Choice(("a", "b")), 110 | { 111 | "param_type": "Choice", 112 | "name": "choice", 113 | "choices": ("a", "b"), 114 | "case_sensitive": True, 115 | }, 116 | id="Choice ParamType", 117 | ), 118 | pytest.param( 119 | click.DateTime(["%Y-%m-%d"]), 120 | {"param_type": "DateTime", "name": "datetime", "formats": ["%Y-%m-%d"]}, 121 | id="DateTime ParamType", 122 | ), 123 | pytest.param(*INT_PARAM_TYPE, id="INT ParamType"), 124 | pytest.param( 125 | click.IntRange(0, 10, clamp=True), 126 | { 127 | "param_type": "IntRange", 128 | "name": "integer range", 129 | "min": 0, 130 | "max": 10, 131 | "min_open": False, 132 | "max_open": False, 133 | "clamp": True, 134 | }, 135 | id="IntRange ParamType", 136 | ), 137 | pytest.param( 138 | click.FLOAT, {"param_type": "Float", "name": "float"}, id="FLOAT ParamType" 139 | ), 140 | pytest.param( 141 | click.FloatRange(-0.5, 0.5), 142 | { 143 | "param_type": "FloatRange", 144 | "name": "float range", 145 | "min": -0.5, 146 | "max": 0.5, 147 | "min_open": False, 148 | "max_open": False, 149 | "clamp": False, 150 | }, 151 | id="FloatRange ParamType", 152 | ), 153 | pytest.param(*BOOL_PARAM_TYPE, id="Bool ParamType"), 154 | pytest.param( 155 | click.UUID, {"param_type": "UUID", "name": "uuid"}, id="UUID ParamType" 156 | ), 157 | pytest.param( 158 | click.File(), 159 | {"param_type": "File", "name": "filename", "mode": "r", "encoding": None}, 160 | id="File ParamType", 161 | ), 162 | pytest.param( 163 | click.Path(), 164 | { 165 | "param_type": "Path", 166 | "name": "path", 167 | "exists": False, 168 | "file_okay": True, 169 | "dir_okay": True, 170 | "writable": False, 171 | "readable": True, 172 | "allow_dash": False, 173 | }, 174 | id="Path ParamType", 175 | ), 176 | pytest.param( 177 | click.Tuple((click.STRING, click.INT)), 178 | { 179 | "param_type": "Tuple", 180 | "name": "", 181 | "types": [STRING_PARAM_TYPE[1], INT_PARAM_TYPE[1]], 182 | }, 183 | id="Tuple ParamType", 184 | ), 185 | pytest.param(*NUMBER_OPTION, id="Option"), 186 | pytest.param( 187 | click.Option(["--cache/--no-cache", "-c/-u"]), 188 | { 189 | "name": "cache", 190 | "param_type_name": "option", 191 | "opts": ["--cache", "-c"], 192 | "secondary_opts": ["--no-cache", "-u"], 193 | "type": BOOL_PARAM_TYPE[1], 194 | "required": False, 195 | "nargs": 1, 196 | "multiple": False, 197 | "default": False, 198 | "envvar": None, 199 | "help": None, 200 | "prompt": None, 201 | "is_flag": True, 202 | "flag_value": True, 203 | "count": False, 204 | "hidden": False, 205 | }, 206 | id="Flag Option", 207 | ), 208 | pytest.param(*NAME_ARGUMENT, id="Argument"), 209 | ], 210 | ) 211 | def test_parameter(obj, expect): 212 | out = obj.to_info_dict() 213 | assert out == expect 214 | 215 | 216 | @pytest.mark.parametrize( 217 | ("obj", "expect"), 218 | [ 219 | pytest.param(*HELLO_COMMAND, id="Command"), 220 | pytest.param(*HELLO_GROUP, id="Group"), 221 | pytest.param( 222 | click.Group( 223 | "base", 224 | [click.Command("test", params=[NAME_ARGUMENT[0]]), HELLO_GROUP[0]], 225 | ), 226 | { 227 | "name": "base", 228 | "params": [HELP_OPTION[1]], 229 | "help": None, 230 | "epilog": None, 231 | "short_help": None, 232 | "hidden": False, 233 | "deprecated": False, 234 | "commands": { 235 | "cli": HELLO_GROUP[1], 236 | "test": { 237 | "name": "test", 238 | "params": [NAME_ARGUMENT[1], HELP_OPTION[1]], 239 | "help": None, 240 | "epilog": None, 241 | "short_help": None, 242 | "hidden": False, 243 | "deprecated": False, 244 | }, 245 | }, 246 | "chain": False, 247 | }, 248 | id="Nested Group", 249 | ), 250 | ], 251 | ) 252 | def test_command(obj, expect): 253 | ctx = click.Context(obj) 254 | out = obj.to_info_dict(ctx) 255 | assert out == expect 256 | 257 | 258 | def test_context(): 259 | ctx = click.Context(HELLO_COMMAND[0]) 260 | out = ctx.to_info_dict() 261 | assert out == { 262 | "command": HELLO_COMMAND[1], 263 | "info_name": None, 264 | "allow_extra_args": False, 265 | "allow_interspersed_args": True, 266 | "ignore_unknown_options": False, 267 | "auto_envvar_prefix": None, 268 | } 269 | 270 | 271 | def test_paramtype_no_name(): 272 | class TestType(click.ParamType): 273 | pass 274 | 275 | assert TestType().to_info_dict()["name"] == "TestType" 276 | -------------------------------------------------------------------------------- /tests/test_normalization.py: -------------------------------------------------------------------------------- 1 | import 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 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import click 4 | from click.parser import _OptionParser 5 | from click.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 | -------------------------------------------------------------------------------- /tests/test_types.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import pathlib 3 | import platform 4 | import tempfile 5 | 6 | import pytest 7 | 8 | import click 9 | from click 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 | -------------------------------------------------------------------------------- /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 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 | 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 = 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 | -------------------------------------------------------------------------------- /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 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 | -------------------------------------------------------------------------------- /tests/typing/typing_group_kw_options.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import assert_type 2 | 3 | import click 4 | 5 | 6 | @click.group(context_settings={}) 7 | def hello() -> None: 8 | pass 9 | 10 | 11 | assert_type(hello, click.Group) 12 | -------------------------------------------------------------------------------- /tests/typing/typing_help_option.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import assert_type 2 | 3 | import 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_options.py: -------------------------------------------------------------------------------- 1 | """From https://click.palletsprojects.com/en/stable/quickstart/#adding-parameters""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import 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 | -------------------------------------------------------------------------------- /tests/typing/typing_password_option.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | from typing_extensions import assert_type 4 | 5 | import 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/typing/typing_progressbar.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing_extensions import assert_type 4 | 5 | from click import progressbar 6 | from click._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 | -------------------------------------------------------------------------------- /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 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_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 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 | --------------------------------------------------------------------------------