├── .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 |
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 |
--------------------------------------------------------------------------------