├── .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 │ ├── publish.yaml │ └── tests.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── CHANGES.rst ├── CONTRIBUTING.rst ├── LICENSE.txt ├── Makefile ├── README.md ├── artwork └── logo.svg ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── rules ├── source │ └── format └── watch ├── docs ├── Makefile ├── _static │ ├── click-icon.png │ ├── click-logo-sidebar.png │ └── click-logo.png ├── advanced.rst ├── api.rst ├── arguments.rst ├── changes.rst ├── commands.rst ├── complex.rst ├── conf.py ├── contrib.rst ├── documentation.rst ├── exceptions.rst ├── handling-files.rst ├── index.rst ├── license.rst ├── make.bat ├── options.rst ├── parameters.rst ├── prompts.rst ├── quickstart.rst ├── setuptools.rst ├── shell-completion.rst ├── testing.rst ├── unicode-support.rst ├── upgrading.rst ├── utils.rst ├── virtualenv.rst ├── why.rst └── wincmd.rst ├── examples ├── README ├── aliases │ ├── README │ ├── aliases.ini │ ├── aliases.py │ └── setup.py ├── colors │ ├── README │ ├── colors.py │ └── setup.py ├── completion │ ├── README │ ├── completion.py │ └── setup.py ├── complex │ ├── README │ ├── complex │ │ ├── __init__.py │ │ ├── cli.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── cmd_init.py │ │ │ └── cmd_status.py │ └── setup.py ├── imagepipe │ ├── .gitignore │ ├── README │ ├── example01.jpg │ ├── example02.jpg │ ├── imagepipe.py │ └── setup.py ├── inout │ ├── README │ ├── inout.py │ └── setup.py ├── naval │ ├── README │ ├── naval.py │ └── setup.py ├── repo │ ├── README │ ├── repo.py │ └── setup.py ├── termui │ ├── README │ ├── setup.py │ └── termui.py └── validation │ ├── README │ ├── setup.py │ └── validation.py ├── pyproject.toml ├── requirements ├── build.in ├── build.txt ├── dev.in ├── dev.txt ├── docs.in ├── docs.txt ├── tests.in ├── tests.txt ├── tests37.txt ├── typing.in └── typing.txt ├── src └── asyncclick │ ├── __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_simple_example.py │ └── typing_version_option.py └── tox.ini /.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 | concurrency: 14 | group: lock 15 | jobs: 16 | lock: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 20 | with: 21 | issue-inactive-days: 14 22 | pr-inactive-days: 14 23 | discussion-inactive-days: 14 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | outputs: 10 | hash: ${{ steps.hash.outputs.hash }} 11 | steps: 12 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 13 | - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 14 | with: 15 | python-version: '3.x' 16 | cache: pip 17 | cache-dependency-path: requirements*/*.txt 18 | - run: pip install -r requirements/build.txt 19 | # Use the commit date instead of the current date during the build. 20 | - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV 21 | - run: python -m build 22 | # Generate hashes used for provenance. 23 | - name: generate hash 24 | id: hash 25 | run: cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT 26 | - uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 27 | with: 28 | path: ./dist 29 | provenance: 30 | needs: [build] 31 | permissions: 32 | actions: read 33 | id-token: write 34 | contents: write 35 | # Can't pin with hash due to how this workflow works. 36 | uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 37 | with: 38 | base64-subjects: ${{ needs.build.outputs.hash }} 39 | create-release: 40 | # Upload the sdist, wheels, and provenance to a GitHub release. They remain 41 | # available as build artifacts for a while as well. 42 | needs: [provenance] 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: write 46 | steps: 47 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 48 | - name: create release 49 | run: > 50 | gh release create --draft --repo ${{ github.repository }} 51 | ${{ github.ref_name }} 52 | *.intoto.jsonl/* artifact/* 53 | env: 54 | GH_TOKEN: ${{ github.token }} 55 | publish-pypi: 56 | needs: [provenance] 57 | # Wait for approval before attempting to upload to PyPI. This allows reviewing the 58 | # files in the draft release. 59 | environment: 60 | name: publish 61 | url: https://pypi.org/project/click/${{ github.ref_name }} 62 | runs-on: ubuntu-latest 63 | permissions: 64 | id-token: write 65 | steps: 66 | - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 67 | - uses: pypa/gh-action-pypi-publish@15c56dba361d8335944d31a2ecd17d700fc7bcbc # v1.12.2 68 | with: 69 | packages-dir: artifact/ 70 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: [main, stable] 5 | paths-ignore: ['docs/**', '*.md', '*.rst'] 6 | pull_request: 7 | paths-ignore: [ 'docs/**', '*.md', '*.rst' ] 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 | - {python: '3.12'} 18 | - {name: Windows, python: '3.12', os: windows-latest} 19 | - {name: Mac, python: '3.12', os: macos-latest} 20 | - {python: '3.11'} 21 | - {python: '3.10'} 22 | - {python: '3.9'} 23 | - {name: PyPy, python: 'pypy-3.10', tox: pypy310} 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 27 | with: 28 | python-version: ${{ matrix.python }} 29 | allow-prereleases: true 30 | cache: pip 31 | cache-dependency-path: requirements*/*.txt 32 | - run: pip install tox 33 | - run: tox run -e ${{ matrix.tox || format('py{0}', matrix.python) }} 34 | typing: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 38 | - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 39 | with: 40 | python-version: '3.x' 41 | cache: pip 42 | cache-dependency-path: requirements*/*.txt 43 | - name: cache mypy 44 | uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2 45 | with: 46 | path: ./.mypy_cache 47 | key: mypy|${{ hashFiles('pyproject.toml') }} 48 | - run: pip install tox 49 | - run: tox run -e typing 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/*.egg-info/ 2 | /.hypothesis/ 3 | /build/ 4 | /.pybuild/ 5 | .cache/ 6 | .idea/ 7 | .vscode/ 8 | .venv*/ 9 | venv*/ 10 | __pycache__/ 11 | /dist/ 12 | /.pytest_cache/ 13 | dist/ 14 | .coverage* 15 | htmlcov/ 16 | .tox/ 17 | docs/_build/ 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.8.1 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v5.0.0 9 | hooks: 10 | - id: check-merge-conflict 11 | - id: debug-statements 12 | - id: fix-byte-order-marker 13 | - id: trailing-whitespace 14 | - id: end-of-file-fixer 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: '3.12' 6 | python: 7 | install: 8 | - requirements: requirements/docs.txt 9 | - method: pip 10 | path: . 11 | sphinx: 12 | builder: dirhtml 13 | fail_on_warning: true 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | How to contribute to Click 2 | ========================== 3 | 4 | Thank you for considering contributing to Click! 5 | 6 | 7 | Support questions 8 | ----------------- 9 | 10 | Please don't use the issue tracker for this. The issue tracker is a tool 11 | to address bugs and feature requests in Click itself. Use one of the 12 | following resources for questions about using Click or issues with your 13 | own code: 14 | 15 | - The ``#get-help`` channel on our Discord chat: 16 | https://discord.gg/pallets 17 | - The mailing list flask@python.org for long term discussion or larger 18 | issues. 19 | - Ask on `Stack Overflow`_. Search with Google first using: 20 | ``site:stackoverflow.com python click {search term, exception message, etc.}`` 21 | 22 | .. _Stack Overflow: https://stackoverflow.com/questions/tagged/python-click?tab=Frequent 23 | 24 | 25 | Reporting issues 26 | ---------------- 27 | 28 | Include the following information in your post: 29 | 30 | - Describe what you expected to happen. 31 | - If possible, include a `minimal reproducible example`_ to help us 32 | identify the issue. This also helps check that the issue is not with 33 | your own code. 34 | - Describe what actually happened. Include the full traceback if there 35 | was an exception. 36 | - List your Python and Click versions. If possible, check if this 37 | issue is already fixed in the latest releases or the latest code in 38 | the repository. 39 | 40 | .. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example 41 | 42 | 43 | Submitting patches 44 | ------------------ 45 | 46 | If there is not an open issue for what you want to submit, prefer 47 | opening one for discussion before working on a PR. You can work on any 48 | issue that doesn't have an open PR linked to it or a maintainer assigned 49 | to it. These show up in the sidebar. No need to ask if you can work on 50 | an issue that interests you. 51 | 52 | Include the following in your patch: 53 | 54 | - Use `Black`_ to format your code. This and other tools will run 55 | automatically if you install `pre-commit`_ using the instructions 56 | below. 57 | - Include tests if your patch adds or changes code. Make sure the test 58 | fails without your patch. 59 | - Update any relevant docs pages and docstrings. Docs pages and 60 | docstrings should be wrapped at 72 characters. 61 | - Add an entry in ``CHANGES.rst``. Use the same style as other 62 | entries. Also include ``.. versionchanged::`` inline changelogs in 63 | relevant docstrings. 64 | 65 | .. _Black: https://black.readthedocs.io 66 | .. _pre-commit: https://pre-commit.com 67 | 68 | 69 | First time setup 70 | ~~~~~~~~~~~~~~~~ 71 | 72 | - Download and install the `latest version of git`_. 73 | - Configure git with your `username`_ and `email`_. 74 | 75 | .. code-block:: text 76 | 77 | $ git config --global user.name 'your name' 78 | $ git config --global user.email 'your email' 79 | 80 | - Make sure you have a `GitHub account`_. 81 | - Fork Click to your GitHub account by clicking the `Fork`_ button. 82 | - `Clone`_ the main repository locally. 83 | 84 | .. code-block:: text 85 | 86 | $ git clone https://github.com/pallets/click 87 | $ cd click 88 | 89 | - Add your fork as a remote to push your work to. Replace 90 | ``{username}`` with your username. This names the remote "fork", the 91 | default Pallets remote is "origin". 92 | 93 | .. code-block:: text 94 | 95 | $ git remote add fork https://github.com/{username}/click 96 | 97 | - Create a virtualenv. 98 | 99 | .. code-block:: text 100 | 101 | $ python3 -m venv env 102 | $ . env/bin/activate 103 | 104 | On Windows, activating is different. 105 | 106 | .. code-block:: text 107 | 108 | > env\Scripts\activate 109 | 110 | - Upgrade pip and setuptools. 111 | 112 | .. code-block:: text 113 | 114 | $ python -m pip install --upgrade pip setuptools 115 | 116 | - Install the development dependencies, then install Click in 117 | editable mode. 118 | 119 | .. code-block:: text 120 | 121 | $ pip install -r requirements/dev.txt && pip install -e . 122 | 123 | - Install the pre-commit hooks. 124 | 125 | .. code-block:: text 126 | 127 | $ pre-commit install 128 | 129 | .. _latest version of git: https://git-scm.com/downloads 130 | .. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git 131 | .. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address 132 | .. _GitHub account: https://github.com/join 133 | .. _Fork: https://github.com/pallets/click/fork 134 | .. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork 135 | 136 | 137 | Start coding 138 | ~~~~~~~~~~~~ 139 | 140 | - Create a branch to identify the issue you would like to work on. If 141 | you're submitting a bug or documentation fix, branch off of the 142 | latest ".x" branch. 143 | 144 | .. code-block:: text 145 | 146 | $ git fetch origin 147 | $ git checkout -b your-branch-name origin/8.1.x 148 | 149 | If you're submitting a feature addition or change, branch off of the 150 | "main" branch. 151 | 152 | .. code-block:: text 153 | 154 | $ git fetch origin 155 | $ git checkout -b your-branch-name origin/main 156 | 157 | - Using your favorite editor, make your changes, 158 | `committing as you go`_. 159 | - Include tests that cover any code changes you make. Make sure the 160 | test fails without your patch. Run the tests as described below. 161 | - Push your commits to your fork on GitHub and 162 | `create a pull request`_. Link to the issue being addressed with 163 | ``fixes #123`` in the pull request. 164 | 165 | .. code-block:: text 166 | 167 | $ git push --set-upstream fork your-branch-name 168 | 169 | .. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes 170 | .. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request 171 | 172 | 173 | Running the tests 174 | ~~~~~~~~~~~~~~~~~ 175 | 176 | Run the basic test suite with pytest. 177 | 178 | .. code-block:: text 179 | 180 | $ pytest 181 | 182 | This runs the tests for the current environment, which is usually 183 | sufficient. CI will run the full suite when you submit your pull 184 | request. You can run the full test suite with tox if you don't want to 185 | wait. 186 | 187 | .. code-block:: text 188 | 189 | $ tox 190 | 191 | 192 | Running test coverage 193 | ~~~~~~~~~~~~~~~~~~~~~ 194 | 195 | Generating a report of lines that do not have test coverage can indicate 196 | where to start contributing. Run ``pytest`` using ``coverage`` and 197 | generate a report. 198 | 199 | .. code-block:: text 200 | 201 | $ pip install coverage 202 | $ coverage run -m pytest 203 | $ coverage html 204 | 205 | Open ``htmlcov/index.html`` in your browser to explore the report. 206 | 207 | Read more about `coverage `__. 208 | 209 | 210 | Building the docs 211 | ~~~~~~~~~~~~~~~~~ 212 | 213 | Build the docs in the ``docs`` directory using Sphinx. 214 | 215 | .. code-block:: text 216 | 217 | $ cd docs 218 | $ make html 219 | 220 | Open ``_build/html/index.html`` in your browser to view the docs. 221 | 222 | Read more about `Sphinx `__. 223 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | PACKAGE=asyncclick 4 | ifneq ($(wildcard /usr/share/sourcemgr/make/py),) 5 | include /usr/share/sourcemgr/make/py 6 | # available via http://github.com/smurfix/sourcemgr 7 | 8 | else 9 | %: 10 | @echo "Please use 'python setup.py'." 11 | @exit 1 12 | endif 13 | 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # $ asyncclick_ 2 | 3 | Asyncclick is a fork of Click (described below) that works with trio or asyncio. 4 | 5 | AsyncClick allows you to seamlessly use async command and subcommand handlers. 6 | 7 | 8 | # $ click_ 9 | 10 | Click is a Python package for creating beautiful command line interfaces 11 | in a composable way with as little code as necessary. It's the "Command 12 | Line Interface Creation Kit". It's highly configurable but comes with 13 | sensible defaults out of the box. 14 | 15 | It aims to make the process of writing command line tools quick and fun 16 | while also preventing any frustration caused by the inability to 17 | implement an intended CLI API. 18 | 19 | Click in three points: 20 | 21 | - Arbitrary nesting of commands 22 | - Automatic help page generation 23 | - Supports lazy loading of subcommands at runtime 24 | 25 | 26 | ## A Simple Example 27 | 28 | ```python 29 | import asyncclick as click 30 | import anyio 31 | 32 | @click.command() 33 | @click.option("--count", default=1, help="Number of greetings.") 34 | @click.option("--name", prompt="Your name", help="The person to greet.") 35 | async def hello(count, name): 36 | """Simple program that greets NAME for a total of COUNT times.""" 37 | for _ in range(count): 38 | click.echo(f"Hello, {name}!") 39 | await anyio.sleep(0.2) 40 | 41 | if __name__ == '__main__': 42 | hello() 43 | # alternately: anyio.run(hello.main) 44 | ``` 45 | 46 | ``` 47 | $ python hello.py --count=3 48 | Your name: Click 49 | Hello, Click! 50 | Hello, Click! 51 | Hello, Click! 52 | ``` 53 | 54 | 55 | ## Donate 56 | 57 | The Pallets organization develops and supports Click and other popular 58 | packages. In order to grow the community of contributors and users, and 59 | allow the maintainers to devote more time to the projects, [please 60 | donate today][]. 61 | 62 | [please donate today]: https://palletsprojects.com/donate 63 | 64 | The AsyncClick fork is maintained by Matthias Urlichs . 65 | It's not a lot of work, so if you'd like to motivate me, donate to the 66 | charity of your choice and tell me that you've done so. ;-) 67 | -------------------------------------------------------------------------------- /artwork/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 64 | 69 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | /files 2 | /*.log 3 | /*.debhelper 4 | /debhelper-build-stamp 5 | /*.substvars 6 | /python3-asyncclick 7 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | asyncclick (1:8.1.8-1) unstable; urgency=medium 2 | 3 | * new Upstream 4 | 5 | -- Matthias Urlichs Mon, 06 Jan 2025 10:42:05 +0100 6 | 7 | asyncclick (1:8.1.7-2) unstable; urgency=medium 8 | 9 | * Fixups 10 | 11 | -- Matthias Urlichs Sun, 10 Mar 2024 10:28:31 +0100 12 | 13 | asyncclick (1:8.1.3-2) unstable; urgency=medium 14 | 15 | * new Upstream 16 | 17 | -- Matthias Urlichs Tue, 07 Jun 2022 18:53:38 +0200 18 | 19 | asyncclick (1:8.0.3-6) unstable; urgency=medium 20 | 21 | * fix test 22 | 23 | -- Matthias Urlichs Sat, 15 Jan 2022 22:22:34 +0100 24 | 25 | asyncclick (1:8.0.3-5) unstable; urgency=medium 26 | 27 | * enter_context => with_resource 28 | 29 | -- Matthias Urlichs Sat, 15 Jan 2022 22:21:09 +0100 30 | 31 | asyncclick (1:8.0.3-4) unstable; urgency=medium 32 | 33 | * missed anyio markers 34 | 35 | -- Matthias Urlichs Wed, 05 Jan 2022 11:44:55 +0100 36 | 37 | asyncclick (1:8.0.3-3) unstable; urgency=medium 38 | 39 | * missed a comflict marker 40 | 41 | -- Matthias Urlichs Wed, 05 Jan 2022 11:43:07 +0100 42 | 43 | asyncclick (1:8.0.3-2) unstable; urgency=medium 44 | 45 | * Fixed PYTHONPATH for tests 46 | 47 | -- Matthias Urlichs Wed, 05 Jan 2022 11:42:14 +0100 48 | 49 | asyncclick (1:8.0.3-1) unstable; urgency=medium 50 | 51 | * Merge 8.0.3 52 | 53 | -- Matthias Urlichs Wed, 05 Jan 2022 11:33:57 +0100 54 | 55 | asyncclick (1:7.0.90-1) unstable; urgency=medium 56 | 57 | * Merge current 7.1 58 | 59 | -- Matthias Urlichs Thu, 13 Feb 2020 12:27:50 +0100 60 | 61 | asyncclick (1:7.0.4-2) unstable; urgency=medium 62 | 63 | * move to anyio 64 | 65 | -- Matthias Urlichs Fri, 21 Jun 2019 09:36:52 +0200 66 | 67 | trio-click (1:7.0.3-1) unstable; urgency=medium 68 | 69 | * Removed debug output mistakenly left in the code 70 | 71 | -- Matthias Urlichs Sun, 27 Jan 2019 14:30:39 +0100 72 | 73 | trio-click (1:7.0.2-1) unstable; urgency=medium 74 | 75 | * Mistakenly imported the "real" click 76 | 77 | -- Matthias Urlichs Mon, 07 Jan 2019 19:18:44 +0100 78 | 79 | trio-click (1:7.0.1-3) unstable; urgency=medium 80 | 81 | * Fix 3.6 compatibility 82 | 83 | -- Matthias Urlichs Wed, 02 Jan 2019 15:56:11 +0100 84 | 85 | trio-click (1:7.0.1-2) unstable; urgency=medium 86 | 87 | * Add an async context manager 88 | 89 | -- Matthias Urlichs Wed, 02 Jan 2019 15:49:40 +0100 90 | 91 | trio-click (1:7.0.1-1) unstable; urgency=medium 92 | 93 | * Merge to 7.0 94 | * Switched to AnyIO (instead of directly using Trio). 95 | 96 | -- Matthias Urlichs Thu, 20 Dec 2018 14:44:04 +0100 97 | 98 | trio-click (1:7.0~dev5-1) unstable; urgency=medium 99 | 100 | * fix version 101 | 102 | -- Matthias Urlichs Tue, 12 Jun 2018 05:41:03 +0200 103 | 104 | trio-click (1:7.0~dev4-1) unstable; urgency=medium 105 | 106 | * Updated README 107 | 108 | -- Matthias Urlichs Fri, 08 Jun 2018 12:09:19 +0200 109 | 110 | trio-click (1:7.0~dev3-1) unstable; urgency=medium 111 | 112 | * Merge to current Upstream. 113 | 114 | -- Matthias Urlichs Mon, 28 May 2018 03:38:15 +0200 115 | 116 | trio-click (7.0+trio-dev3-1) unstable; urgency=medium 117 | 118 | * Merge to current Upstream. 119 | 120 | -- Matthias Urlichs Mon, 28 May 2018 03:38:15 +0200 121 | 122 | trio-click (7.0+trio-dev2-1) unstable; urgency=medium 123 | 124 | * Merge to current Upstream. 125 | 126 | -- Matthias Urlichs Wed, 11 Apr 2018 06:19:22 +0200 127 | 128 | trio-click (7.0+trio-dev1-1) unstable; urgency=medium 129 | 130 | * Updated version number to something trackable 131 | 132 | -- Matthias Urlichs Sun, 28 Jan 2018 09:59:28 +0100 133 | 134 | trio-click (7.0~dev0-1) smurf; urgency=low 135 | 136 | * source package automatically created by stdeb 0.8.5 137 | 138 | -- Matthias Urlichs Wed, 24 Jan 2018 22:45:37 +0100 139 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 13 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: asyncclick 2 | Maintainer: Matthias Urlichs 3 | Section: python 4 | Priority: optional 5 | Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), 6 | python3-anyio, 7 | python3-trio, 8 | python3-pytest, 9 | python3-pytest-runner, 10 | Standards-Version: 3.9.6 11 | Homepage: http://github.com/pallets/click 12 | 13 | Package: python3-asyncclick 14 | Architecture: all 15 | Depends: ${misc:Depends}, ${python3:Depends}, 16 | python3-anyio, 17 | Recommends: python3-trio 18 | Description: A simple wrapper around optparse for powerful command line u 19 | 20 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # This file was automatically generated by stdeb 0.8.5 at 4 | # Wed, 24 Jan 2018 22:45:36 +0100 5 | export PYBUILD_NAME=asyncclick 6 | %: 7 | dh $@ --with python3 --buildsystem=pybuild 8 | override_dh_auto_test: 9 | env PYTHONPATH=src python3 -mpytest -sxv tests/ 10 | 11 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | # please also check http://pypi.debian.net/asyncclick/watch 2 | version=3 3 | opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ 4 | http://pypi.debian.net/asyncclick/asyncclick-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) -------------------------------------------------------------------------------- /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/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/docs/_static/click-icon.png -------------------------------------------------------------------------------- /docs/_static/click-logo-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/docs/_static/click-logo-sidebar.png -------------------------------------------------------------------------------- /docs/_static/click-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/docs/_static/click-logo.png -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. module:: asyncclick 5 | 6 | This part of the documentation lists the full API reference of all public 7 | classes and functions. 8 | 9 | Decorators 10 | ---------- 11 | 12 | .. autofunction:: command 13 | 14 | .. autofunction:: group 15 | 16 | .. autofunction:: argument 17 | 18 | .. autofunction:: option 19 | 20 | .. autofunction:: password_option 21 | 22 | .. autofunction:: confirmation_option 23 | 24 | .. autofunction:: version_option 25 | 26 | .. autofunction:: help_option 27 | 28 | .. autofunction:: pass_context 29 | 30 | .. autofunction:: pass_obj 31 | 32 | .. autofunction:: make_pass_decorator 33 | 34 | .. autofunction:: asyncclick.decorators.pass_meta_key 35 | 36 | 37 | Utilities 38 | --------- 39 | 40 | .. autofunction:: echo 41 | 42 | .. autofunction:: echo_via_pager 43 | 44 | .. autofunction:: prompt 45 | 46 | .. autofunction:: confirm 47 | 48 | .. autofunction:: progressbar 49 | 50 | .. autofunction:: clear 51 | 52 | .. autofunction:: style 53 | 54 | .. autofunction:: unstyle 55 | 56 | .. autofunction:: secho 57 | 58 | .. autofunction:: edit 59 | 60 | .. autofunction:: launch 61 | 62 | .. autofunction:: getchar 63 | 64 | .. autofunction:: pause 65 | 66 | .. autofunction:: get_binary_stream 67 | 68 | .. autofunction:: get_text_stream 69 | 70 | .. autofunction:: open_file 71 | 72 | .. autofunction:: get_app_dir 73 | 74 | .. autofunction:: format_filename 75 | 76 | Commands 77 | -------- 78 | 79 | .. autoclass:: BaseCommand 80 | :members: 81 | 82 | .. autoclass:: Command 83 | :members: 84 | 85 | .. autoclass:: MultiCommand 86 | :members: 87 | 88 | .. autoclass:: Group 89 | :members: 90 | 91 | .. autoclass:: CommandCollection 92 | :members: 93 | 94 | Parameters 95 | ---------- 96 | 97 | .. autoclass:: Parameter 98 | :members: 99 | 100 | .. autoclass:: Option 101 | 102 | .. autoclass:: Argument 103 | 104 | Context 105 | ------- 106 | 107 | .. autoclass:: Context 108 | :members: 109 | 110 | .. autofunction:: get_current_context 111 | 112 | .. autoclass:: asyncclick.core.ParameterSource 113 | :members: 114 | :member-order: bysource 115 | 116 | 117 | Types 118 | ----- 119 | 120 | .. autodata:: STRING 121 | 122 | .. autodata:: INT 123 | 124 | .. autodata:: FLOAT 125 | 126 | .. autodata:: BOOL 127 | 128 | .. autodata:: UUID 129 | 130 | .. autodata:: UNPROCESSED 131 | 132 | .. autoclass:: File 133 | 134 | .. autoclass:: Path 135 | 136 | .. autoclass:: Choice 137 | 138 | .. autoclass:: IntRange 139 | 140 | .. autoclass:: FloatRange 141 | 142 | .. autoclass:: DateTime 143 | 144 | .. autoclass:: Tuple 145 | 146 | .. autoclass:: ParamType 147 | :members: 148 | 149 | Exceptions 150 | ---------- 151 | 152 | .. autoexception:: ClickException 153 | 154 | .. autoexception:: Abort 155 | 156 | .. autoexception:: UsageError 157 | 158 | .. autoexception:: BadParameter 159 | 160 | .. autoexception:: FileError 161 | 162 | .. autoexception:: NoSuchOption 163 | 164 | .. autoexception:: BadOptionUsage 165 | 166 | .. autoexception:: BadArgumentUsage 167 | 168 | Formatting 169 | ---------- 170 | 171 | .. autoclass:: HelpFormatter 172 | :members: 173 | 174 | .. autofunction:: wrap_text 175 | 176 | Parsing 177 | ------- 178 | 179 | .. autoclass:: OptionParser 180 | :members: 181 | 182 | 183 | Shell Completion 184 | ---------------- 185 | 186 | See :doc:`/shell-completion` for information about enabling and 187 | customizing Click's shell completion system. 188 | 189 | .. currentmodule:: asyncclick.shell_completion 190 | 191 | .. autoclass:: CompletionItem 192 | 193 | .. autoclass:: ShellComplete 194 | :members: 195 | :member-order: bysource 196 | 197 | .. autofunction:: add_completion_class 198 | 199 | 200 | Testing 201 | ------- 202 | 203 | .. currentmodule:: asyncclick.testing 204 | 205 | .. autoclass:: CliRunner 206 | :members: 207 | 208 | .. autoclass:: Result 209 | :members: 210 | -------------------------------------------------------------------------------- /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/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 | default_role = "code" 14 | extensions = [ 15 | "sphinx.ext.autodoc", 16 | "sphinx.ext.extlinks", 17 | "sphinx.ext.intersphinx", 18 | "sphinx_tabs.tabs", 19 | "sphinxcontrib.log_cabinet", 20 | "pallets_sphinx_themes", 21 | ] 22 | autodoc_member_order = "bysource" 23 | autodoc_typehints = "description" 24 | autodoc_preserve_defaults = True 25 | extlinks = { 26 | "issue": ("https://github.com/pallets/click/issues/%s", "#%s"), 27 | "pr": ("https://github.com/pallets/click/pull/%s", "#%s"), 28 | } 29 | intersphinx_mapping = { 30 | "python": ("https://docs.python.org/3/", None), 31 | } 32 | 33 | # HTML ----------------------------------------------------------------- 34 | 35 | html_theme = "click" 36 | html_theme_options = {"index_sidebar_logo": False} 37 | html_context = { 38 | "project_links": [ 39 | ProjectLink("Donate", "https://palletsprojects.com/donate"), 40 | ProjectLink("PyPI Releases", "https://pypi.org/project/click/"), 41 | ProjectLink("Source Code", "https://github.com/pallets/click/"), 42 | ProjectLink("Issue Tracker", "https://github.com/pallets/click/issues/"), 43 | ProjectLink("Chat", "https://discord.gg/pallets"), 44 | ] 45 | } 46 | html_sidebars = { 47 | "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"], 48 | "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"], 49 | } 50 | singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]} 51 | html_static_path = ["_static"] 52 | html_favicon = "_static/click-icon.png" 53 | html_logo = "_static/click-logo-sidebar.png" 54 | html_title = f"Click Documentation ({version})" 55 | html_show_sourcelink = False 56 | -------------------------------------------------------------------------------- /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 | Documenting Scripts 2 | =================== 3 | 4 | .. currentmodule:: click 5 | 6 | Click makes it very easy to document your command line tools. First of 7 | all, it automatically generates help pages for you. While these are 8 | currently not customizable in terms of their layout, all of the text 9 | can be changed. 10 | 11 | Help Texts 12 | ---------- 13 | 14 | Commands and options accept help arguments. In the case of commands, the 15 | docstring of the function is automatically used if provided. 16 | 17 | Simple example: 18 | 19 | .. click:example:: 20 | 21 | @click.command() 22 | @click.option('--count', default=1, help='number of greetings') 23 | @click.argument('name') 24 | def hello(count, name): 25 | """This script prints hello NAME COUNT times.""" 26 | for x in range(count): 27 | click.echo(f"Hello {name}!") 28 | 29 | And what it looks like: 30 | 31 | .. click:run:: 32 | 33 | invoke(hello, args=['--help']) 34 | 35 | 36 | .. _documenting-arguments: 37 | 38 | Documenting Arguments 39 | ---------------------- 40 | 41 | :func:`click.argument` does not take a ``help`` parameter. This is to 42 | follow the general convention of Unix tools of using arguments for only 43 | the most necessary things, and to document them in the command help text 44 | by referring to them by name. 45 | 46 | You might prefer to reference the argument in the description: 47 | 48 | .. click:example:: 49 | 50 | @click.command() 51 | @click.argument('filename') 52 | def touch(filename): 53 | """Print FILENAME.""" 54 | click.echo(filename) 55 | 56 | And what it looks like: 57 | 58 | .. click:run:: 59 | 60 | invoke(touch, args=['--help']) 61 | 62 | Or you might prefer to explicitly provide a description of the argument: 63 | 64 | .. click:example:: 65 | 66 | @click.command() 67 | @click.argument('filename') 68 | def touch(filename): 69 | """Print FILENAME. 70 | 71 | FILENAME is the name of the file to check. 72 | """ 73 | click.echo(filename) 74 | 75 | And what it looks like: 76 | 77 | .. click:run:: 78 | 79 | invoke(touch, args=['--help']) 80 | 81 | For more examples, see the examples in :doc:`/arguments`. 82 | 83 | 84 | Preventing Rewrapping 85 | --------------------- 86 | 87 | The default behavior of Click is to rewrap text based on the width of the 88 | terminal, to a maximum 80 characters. In some circumstances, this can become 89 | a problem. The main issue is when showing code examples, where newlines are 90 | significant. 91 | 92 | Rewrapping can be disabled on a per-paragraph basis by adding a line with 93 | solely the ``\b`` escape marker in it. This line will be removed from the 94 | help text and rewrapping will be disabled. 95 | 96 | Example: 97 | 98 | .. click:example:: 99 | 100 | @click.command() 101 | def cli(): 102 | """First paragraph. 103 | 104 | This is a very long second paragraph and as you 105 | can see wrapped very early in the source text 106 | but will be rewrapped to the terminal width in 107 | the final output. 108 | 109 | \b 110 | This is 111 | a paragraph 112 | without rewrapping. 113 | 114 | And this is a paragraph 115 | that will be rewrapped again. 116 | """ 117 | 118 | And what it looks like: 119 | 120 | .. click:run:: 121 | 122 | invoke(cli, args=['--help']) 123 | 124 | To change the maximum width, pass ``max_content_width`` when calling the command. 125 | 126 | .. code-block:: python 127 | 128 | cli(max_content_width=120) 129 | 130 | 131 | .. _doc-meta-variables: 132 | 133 | Truncating Help Texts 134 | --------------------- 135 | 136 | Click gets command help text from function docstrings. However if you 137 | already use docstrings to document function arguments you may not want 138 | to see :param: and :return: lines in your help text. 139 | 140 | You can use the ``\f`` escape marker to have Click truncate the help text 141 | after the marker. 142 | 143 | Example: 144 | 145 | .. click:example:: 146 | 147 | @click.command() 148 | @click.pass_context 149 | def cli(ctx): 150 | """First paragraph. 151 | 152 | This is a very long second 153 | paragraph and not correctly 154 | wrapped but it will be rewrapped. 155 | \f 156 | 157 | :param click.core.Context ctx: Click context. 158 | """ 159 | 160 | And what it looks like: 161 | 162 | .. click:run:: 163 | 164 | invoke(cli, args=['--help']) 165 | 166 | 167 | Meta Variables 168 | -------------- 169 | 170 | Options and parameters accept a ``metavar`` argument that can change the 171 | meta variable in the help page. The default version is the parameter name 172 | in uppercase with underscores, but can be annotated differently if 173 | desired. This can be customized at all levels: 174 | 175 | .. click:example:: 176 | 177 | @click.command(options_metavar='') 178 | @click.option('--count', default=1, help='number of greetings', 179 | metavar='') 180 | @click.argument('name', metavar='') 181 | def hello(count, name): 182 | """This script prints hello times.""" 183 | for x in range(count): 184 | click.echo(f"Hello {name}!") 185 | 186 | Example: 187 | 188 | .. click:run:: 189 | 190 | invoke(hello, args=['--help']) 191 | 192 | 193 | Command Short Help 194 | ------------------ 195 | 196 | For commands, a short help snippet is generated. By default, it's the first 197 | sentence of the help message of the command, unless it's too long. This can 198 | also be overridden: 199 | 200 | .. click:example:: 201 | 202 | @click.group() 203 | def cli(): 204 | """A simple command line tool.""" 205 | 206 | @cli.command('init', short_help='init the repo') 207 | def init(): 208 | """Initializes the repository.""" 209 | 210 | @cli.command('delete', short_help='delete the repo') 211 | def delete(): 212 | """Deletes the repository.""" 213 | 214 | And what it looks like: 215 | 216 | .. click:run:: 217 | 218 | invoke(cli, prog_name='repo.py') 219 | 220 | Command Epilog Help 221 | ------------------- 222 | 223 | The help epilog is like the help string but it's printed at the end of the help 224 | page after everything else. Useful for showing example command usages or 225 | referencing additional help resources. 226 | 227 | .. click:example:: 228 | 229 | @click.command(epilog='Check out our docs at https://click.palletsprojects.com/ for more details') 230 | def init(): 231 | """Initializes the repository.""" 232 | 233 | And what it looks like: 234 | 235 | .. click:run:: 236 | 237 | invoke(init, prog_name='repo.py', args=['--help']) 238 | 239 | Help Parameter Customization 240 | ---------------------------- 241 | 242 | .. versionadded:: 2.0 243 | 244 | The help parameter is implemented in Click in a very special manner. 245 | Unlike regular parameters it's automatically added by Click for any 246 | command and it performs automatic conflict resolution. By default it's 247 | called ``--help``, but this can be changed. If a command itself implements 248 | a parameter with the same name, the default help parameter stops accepting 249 | it. There is a context setting that can be used to override the names of 250 | the help parameters called :attr:`~Context.help_option_names`. 251 | 252 | This example changes the default parameters to ``-h`` and ``--help`` 253 | instead of just ``--help``: 254 | 255 | .. click:example:: 256 | 257 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) 258 | 259 | @click.command(context_settings=CONTEXT_SETTINGS) 260 | def cli(): 261 | pass 262 | 263 | And what it looks like: 264 | 265 | .. click:run:: 266 | 267 | invoke(cli, ['-h']) 268 | -------------------------------------------------------------------------------- /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:`BaseCommand.main`. In 14 | there it handles all subclasses of :exc:`ClickException` as well as the 15 | standard :exc:`EOFError` and :exc:`KeyboardInterrupt` exceptions. The 16 | latter are internally translated into an :exc:`Abort`. 17 | 18 | The logic applied is the following: 19 | 20 | 1. If an :exc:`EOFError` or :exc:`KeyboardInterrupt` happens, reraise it 21 | as :exc:`Abort`. 22 | 2. If a :exc:`ClickException` is raised, invoke the 23 | :meth:`ClickException.show` method on it to display it and then exit 24 | the program with :attr:`ClickException.exit_code`. 25 | 3. If an :exc:`Abort` exception is raised print the string ``Aborted!`` 26 | to standard error and exit the program with exit code ``1``. 27 | 4. If it goes through well, exit the program with exit code ``0``. 28 | 29 | What if I don't want that? 30 | -------------------------- 31 | 32 | Generally you always have the option to invoke the :meth:`invoke` method 33 | yourself. For instance if you have a :class:`Command` you can invoke it 34 | manually like this:: 35 | 36 | ctx = await command.make_context('command-name', ['args', 'go', 'here']) 37 | with ctx: 38 | result = await command.invoke(ctx) 39 | 40 | In this case exceptions will not be handled at all and bubbled up as you 41 | would expect. 42 | 43 | Starting with Click 3.0 you can also use the :meth:`Command.main` method 44 | but disable the standalone mode which will do two things: disable 45 | exception handling and disable the implicit :meth:`sys.exit` at the end. 46 | 47 | So you can do something like this:: 48 | 49 | await command.main(['command-name', 'args', 'go', 'here'], 50 | standalone_mode=False) 51 | 52 | Which Exceptions Exist? 53 | ----------------------- 54 | 55 | Click has two exception bases: :exc:`ClickException` which is raised for 56 | all exceptions that Click wants to signal to the user and :exc:`Abort` 57 | which is used to instruct Click to abort the execution. 58 | 59 | A :exc:`ClickException` has a :meth:`~ClickException.show` method which 60 | can render an error message to stderr or the given file object. If you 61 | want to use the exception yourself for doing something check the API docs 62 | about what else they provide. 63 | 64 | The following common subclasses exist: 65 | 66 | * :exc:`UsageError` to inform the user that something went wrong. 67 | * :exc:`BadParameter` to inform the user that something went wrong with 68 | a specific parameter. These are often handled internally in Click and 69 | augmented with extra information if possible. For instance if those 70 | are raised from a callback Click will automatically augment it with 71 | the parameter name if possible. 72 | * :exc:`FileError` this is an error that is raised by the 73 | :exc:`FileType` if Click encounters issues opening the file. 74 | -------------------------------------------------------------------------------- /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 | Welcome to the AsyncClick Documentation 2 | ================================== 3 | .. rst-class:: hide-header 4 | 5 | .. image:: _static/click-logo.png 6 | :align: center 7 | :scale: 50% 8 | :target: https://palletsprojects.com/p/click/ 9 | 10 | AsyncClick is a fork of Click that works well with (some) async 11 | frameworks. Supported: asyncio, trio, and curio. 12 | 13 | Click, in turn, is a Python package for creating beautiful command line interfaces 14 | in a composable way with as little code as necessary. It's the "Command 15 | Line Interface Creation Kit". It's highly configurable but comes with 16 | sensible defaults out of the box. 17 | 18 | It aims to make the process of writing command line tools quick and fun 19 | while also preventing any frustration caused by the inability to implement 20 | an intended CLI API. 21 | 22 | Click in three points: 23 | 24 | - arbitrary nesting of commands 25 | - automatic help page generation 26 | - supports lazy loading of subcommands at runtime 27 | 28 | What does it look like? Here is an example of a simple Click program: 29 | 30 | .. click:example:: 31 | 32 | import asyncclick as click 33 | import anyio 34 | 35 | @click.command() 36 | @click.option('--count', default=1, help='Number of greetings.') 37 | @click.option('--name', prompt='Your name', 38 | help='The person to greet.') 39 | async def hello(count, name): 40 | """Simple program that greets NAME for a total of COUNT times.""" 41 | for x in range(count): 42 | if x: await anyio.sleep(0.1) 43 | click.echo(f"Hello {name}!") 44 | 45 | if __name__ == '__main__': 46 | hello() 47 | 48 | And what it looks like when run: 49 | 50 | .. click:run:: 51 | 52 | invoke(hello, ['--count=3'], prog_name='python hello.py', input='John\n') 53 | 54 | It automatically generates nicely formatted help pages: 55 | 56 | .. click:run:: 57 | 58 | invoke(hello, ['--help'], prog_name='python hello.py') 59 | 60 | You can get the library directly from PyPI:: 61 | 62 | pip install asyncclick 63 | 64 | Documentation 65 | ------------- 66 | 67 | This part of the documentation guides you through all of the library's 68 | usage patterns. 69 | 70 | .. toctree:: 71 | :maxdepth: 2 72 | 73 | why 74 | quickstart 75 | virtualenv 76 | setuptools 77 | parameters 78 | options 79 | arguments 80 | commands 81 | prompts 82 | handling-files 83 | documentation 84 | complex 85 | advanced 86 | testing 87 | utils 88 | shell-completion 89 | exceptions 90 | unicode-support 91 | wincmd 92 | 93 | API Reference 94 | ------------- 95 | 96 | If you are looking for information on a specific function, class, or 97 | method, this part of the documentation is for you. 98 | 99 | .. toctree:: 100 | :maxdepth: 2 101 | 102 | api 103 | 104 | Miscellaneous Pages 105 | ------------------- 106 | 107 | .. toctree:: 108 | :maxdepth: 2 109 | 110 | contrib 111 | upgrading 112 | license 113 | changes 114 | -------------------------------------------------------------------------------- /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/parameters.rst: -------------------------------------------------------------------------------- 1 | Parameters 2 | ========== 3 | 4 | .. currentmodule:: click 5 | 6 | Click supports only two types of parameters for scripts (by design): options and arguments. 7 | 8 | Options 9 | ---------------- 10 | 11 | * Are optional. 12 | * Recommended to use for everything except subcommands, urls, or files. 13 | * Can take a fixed number of arguments. The default is 1. They may be specified multiple times using :ref:`multiple-options`. 14 | * Are fully documented by the help page. 15 | * Have automatic prompting for missing input. 16 | * Can act as flags (boolean or otherwise). 17 | * Can be pulled from environment variables. 18 | 19 | Arguments 20 | ---------------- 21 | 22 | * Are optional with in reason, but not entirely so. 23 | * Recommended to use for subcommands, urls, or files. 24 | * Can take an arbitrary number of arguments. 25 | * Are not fully documented by the help page since they may be too specific to be automatically documented. For more see :ref:`documenting-arguments`. 26 | * Can be pulled from environment variables but only explicitly named ones. For more see :ref:`environment-variables`. 27 | 28 | .. _parameter_names: 29 | 30 | Parameter Names 31 | --------------- 32 | 33 | Parameters (options and arguments) have a name that will be used as 34 | the Python argument name when calling the decorated function with 35 | values. 36 | 37 | .. click:example:: 38 | 39 | @click.command() 40 | @click.argument('filename') 41 | @click.option('-t', '--times', type=int) 42 | def multi_echo(filename, times): 43 | """Print value filename multiple times.""" 44 | for x in range(times): 45 | click.echo(filename) 46 | 47 | 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`. 48 | The option's names are ``-t`` and ``--times``. More names are available for options and are covered in :ref:`options`. 49 | 50 | And what it looks like when run: 51 | 52 | .. click:run:: 53 | 54 | invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo') 55 | 56 | .. _parameter-types: 57 | 58 | Parameter Types 59 | --------------- 60 | 61 | The supported parameter types are: 62 | 63 | ``str`` / :data:`click.STRING`: 64 | The default parameter type which indicates unicode strings. 65 | 66 | ``int`` / :data:`click.INT`: 67 | A parameter that only accepts integers. 68 | 69 | ``float`` / :data:`click.FLOAT`: 70 | A parameter that only accepts floating point values. 71 | 72 | ``bool`` / :data:`click.BOOL`: 73 | A parameter that accepts boolean values. This is automatically used 74 | for boolean flags. The string values "1", "true", "t", "yes", "y", 75 | and "on" convert to ``True``. "0", "false", "f", "no", "n", and 76 | "off" convert to ``False``. 77 | 78 | :data:`click.UUID`: 79 | A parameter that accepts UUID values. This is not automatically 80 | guessed but represented as :class:`uuid.UUID`. 81 | 82 | .. autoclass:: File 83 | :noindex: 84 | 85 | .. autoclass:: Path 86 | :noindex: 87 | 88 | .. autoclass:: Choice 89 | :noindex: 90 | 91 | .. autoclass:: IntRange 92 | :noindex: 93 | 94 | .. autoclass:: FloatRange 95 | :noindex: 96 | 97 | .. autoclass:: DateTime 98 | :noindex: 99 | 100 | How to Implement Custom Types 101 | ------------------------------- 102 | 103 | 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. 104 | 105 | The following code implements an integer type that accepts hex and octal 106 | numbers in addition to normal integers, and converts them into regular 107 | integers. 108 | 109 | .. code-block:: python 110 | 111 | import asyncclick as click 112 | 113 | class BasedIntParamType(click.ParamType): 114 | name = "integer" 115 | 116 | def convert(self, value, param, ctx): 117 | if isinstance(value, int): 118 | return value 119 | 120 | try: 121 | if value[:2].lower() == "0x": 122 | return int(value[2:], 16) 123 | elif value[:1] == "0": 124 | return int(value, 8) 125 | return int(value, 10) 126 | except ValueError: 127 | self.fail(f"{value!r} is not a valid integer", param, ctx) 128 | 129 | BASED_INT = BasedIntParamType() 130 | 131 | The :attr:`~ParamType.name` attribute is optional and is used for 132 | documentation. Call :meth:`~ParamType.fail` if conversion fails. The 133 | ``param`` and ``ctx`` arguments may be ``None`` in some cases such as 134 | prompts. 135 | 136 | Values from user input or the command line will be strings, but default 137 | values and Python arguments may already be the correct type. The custom 138 | type should check at the top if the value is already valid and pass it 139 | through to support those cases. 140 | -------------------------------------------------------------------------------- /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 | Option Prompts 15 | -------------- 16 | 17 | Option prompts are integrated into the option interface. See 18 | :ref:`option-prompting` for more information. Internally, it 19 | automatically calls either :func:`prompt` or :func:`confirm` as necessary. 20 | 21 | Input Prompts 22 | ------------- 23 | 24 | To manually ask for user input, you can use the :func:`prompt` function. 25 | By default, it accepts any Unicode string, but you can ask for any other 26 | type. For instance, you can ask for a valid integer:: 27 | 28 | value = click.prompt('Please enter a valid integer', type=int) 29 | 30 | Additionally, the type will be determined automatically if a default value is 31 | provided. For instance, the following will only accept floats:: 32 | 33 | value = click.prompt('Please enter a number', default=42.0) 34 | 35 | Confirmation Prompts 36 | -------------------- 37 | 38 | To ask if a user wants to continue with an action, the :func:`confirm` 39 | function comes in handy. By default, it returns the result of the prompt 40 | as a boolean value:: 41 | 42 | if click.confirm('Do you want to continue?'): 43 | click.echo('Well done!') 44 | 45 | There is also the option to make the function automatically abort the 46 | execution of the program if it does not return ``True``:: 47 | 48 | click.confirm('Do you want to continue?', abort=True) 49 | -------------------------------------------------------------------------------- /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:`multi-command-chaining` . 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 asyncclick as 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 setuptools entry points or 135 | other invocations:: 136 | 137 | if __name__ == '__main__': 138 | cli() 139 | 140 | 141 | Registering Commands Later 142 | -------------------------- 143 | 144 | Instead of using the ``@group.command()`` decorator, commands can be 145 | decorated with the plain ``@click.command()`` decorator and registered 146 | with a group later with ``group.add_command()``. This could be used to 147 | split commands into multiple Python modules. 148 | 149 | .. code-block:: python 150 | 151 | @click.command() 152 | def greet(): 153 | click.echo("Hello, World!") 154 | 155 | .. code-block:: python 156 | 157 | @click.group() 158 | def group(): 159 | pass 160 | 161 | group.add_command(greet) 162 | 163 | 164 | Adding Parameters 165 | ----------------- 166 | 167 | To add parameters, use the :func:`option` and :func:`argument` decorators: 168 | 169 | .. click:example:: 170 | 171 | @click.command() 172 | @click.option('--count', default=1, help='number of greetings') 173 | @click.argument('name') 174 | def hello(count, name): 175 | for x in range(count): 176 | click.echo(f"Hello {name}!") 177 | 178 | What it looks like: 179 | 180 | .. click:run:: 181 | 182 | invoke(hello, args=['--help'], prog_name='python hello.py') 183 | 184 | .. _switching-to-setuptools: 185 | 186 | Switching to Setuptools 187 | ----------------------- 188 | 189 | In the code you wrote so far there is a block at the end of the file which 190 | looks like this: ``if __name__ == '__main__':``. This is traditionally 191 | how a standalone Python file looks like. With Click you can continue 192 | doing that, but there are better ways through setuptools. 193 | 194 | There are two main (and many more) reasons for this: 195 | 196 | The first one is that setuptools automatically generates executable 197 | wrappers for Windows so your command line utilities work on Windows too. 198 | 199 | The second reason is that setuptools scripts work with virtualenv on Unix 200 | without the virtualenv having to be activated. This is a very useful 201 | concept which allows you to bundle your scripts with all requirements into 202 | a virtualenv. 203 | 204 | Click is perfectly equipped to work with that and in fact the rest of the 205 | documentation will assume that you are writing applications through 206 | setuptools. 207 | 208 | I strongly recommend to have a look at the :ref:`setuptools-integration` 209 | chapter before reading the rest as the examples assume that you will 210 | be using setuptools. 211 | -------------------------------------------------------------------------------- /docs/setuptools.rst: -------------------------------------------------------------------------------- 1 | .. _setuptools-integration: 2 | 3 | Setuptools Integration 4 | ====================== 5 | 6 | When writing command line utilities, it's recommended to write them as 7 | modules that are distributed with setuptools instead of using Unix 8 | shebangs. 9 | 10 | Why would you want to do that? There are a bunch of reasons: 11 | 12 | 1. One of the problems with the traditional approach is that the first 13 | module the Python interpreter loads has an incorrect name. This might 14 | sound like a small issue but it has quite significant implications. 15 | 16 | The first module is not called by its actual name, but the 17 | interpreter renames it to ``__main__``. While that is a perfectly 18 | valid name it means that if another piece of code wants to import from 19 | that module it will trigger the import a second time under its real 20 | name and all of a sudden your code is imported twice. 21 | 22 | 2. Not on all platforms are things that easy to execute. On Linux and OS 23 | X you can add a comment to the beginning of the file (``#!/usr/bin/env 24 | python``) and your script works like an executable (assuming it has 25 | the executable bit set). This however does not work on Windows. 26 | While on Windows you can associate interpreters with file extensions 27 | (like having everything ending in ``.py`` execute through the Python 28 | interpreter) you will then run into issues if you want to use the 29 | script in a virtualenv. 30 | 31 | In fact running a script in a virtualenv is an issue with OS X and 32 | Linux as well. With the traditional approach you need to have the 33 | whole virtualenv activated so that the correct Python interpreter is 34 | used. Not very user friendly. 35 | 36 | 3. The main trick only works if the script is a Python module. If your 37 | application grows too large and you want to start using a package you 38 | will run into issues. 39 | 40 | Introduction 41 | ------------ 42 | 43 | To bundle your script with setuptools, all you need is the script in a 44 | Python package and a ``setup.py`` file. 45 | 46 | Imagine this directory structure: 47 | 48 | .. code-block:: text 49 | 50 | yourscript.py 51 | setup.py 52 | 53 | Contents of ``yourscript.py``: 54 | 55 | .. click:example:: 56 | 57 | import asyncclick as click 58 | 59 | @click.command() 60 | def cli(): 61 | """Example script.""" 62 | click.echo('Hello World!') 63 | 64 | Contents of ``setup.py``: 65 | 66 | .. code-block:: python 67 | 68 | from setuptools import setup 69 | 70 | setup( 71 | name='yourscript', 72 | version='0.1.0', 73 | py_modules=['yourscript'], 74 | install_requires=[ 75 | 'Click', 76 | ], 77 | entry_points={ 78 | 'console_scripts': [ 79 | 'yourscript = yourscript:cli', 80 | ], 81 | }, 82 | ) 83 | 84 | The magic is in the ``entry_points`` parameter. Read the full 85 | `entry_points `_ 86 | specification for more details. Below ``console_scripts``, each 87 | line identifies one console script. The first part before the 88 | equals sign (``=``) is the name of the script that should be 89 | generated, the second part is the import path followed by a colon 90 | (``:``) with the Click command. 91 | 92 | That's it. 93 | 94 | Testing The Script 95 | ------------------ 96 | 97 | To test the script, you can make a new virtualenv and then install your 98 | package: 99 | 100 | .. code-block:: console 101 | 102 | $ python3 -m venv .venv 103 | $ . .venv/bin/activate 104 | $ pip install --editable . 105 | 106 | Afterwards, your command should be available: 107 | 108 | .. click:run:: 109 | 110 | invoke(cli, prog_name='yourscript') 111 | 112 | Scripts in Packages 113 | ------------------- 114 | 115 | If your script is growing and you want to switch over to your script being 116 | contained in a Python package the changes necessary are minimal. Let's 117 | assume your directory structure changed to this: 118 | 119 | .. code-block:: text 120 | 121 | project/ 122 | yourpackage/ 123 | __init__.py 124 | main.py 125 | utils.py 126 | scripts/ 127 | __init__.py 128 | yourscript.py 129 | setup.py 130 | 131 | In this case instead of using ``py_modules`` in your ``setup.py`` file you 132 | can use ``packages`` and the automatic package finding support of 133 | setuptools. In addition to that it's also recommended to include other 134 | package data. 135 | 136 | These would be the modified contents of ``setup.py``: 137 | 138 | .. code-block:: python 139 | 140 | from setuptools import setup, find_packages 141 | 142 | setup( 143 | name='yourpackage', 144 | version='0.1.0', 145 | packages=find_packages(), 146 | include_package_data=True, 147 | install_requires=[ 148 | 'Click', 149 | ], 150 | entry_points={ 151 | 'console_scripts': [ 152 | 'yourscript = yourpackage.scripts.yourscript:cli', 153 | ], 154 | }, 155 | ) 156 | -------------------------------------------------------------------------------- /docs/testing.rst: -------------------------------------------------------------------------------- 1 | Testing Click Applications 2 | ========================== 3 | 4 | .. currentmodule:: click.testing 5 | 6 | For basic testing, Click provides the :mod:`click.testing` module which 7 | provides test functionality that helps you invoke command line 8 | applications and check their behavior. 9 | 10 | These tools should really only be used for testing as they change 11 | the entire interpreter state for simplicity and are not in any way 12 | thread-safe! 13 | 14 | Basic Testing 15 | ------------- 16 | 17 | The basic functionality for testing Click applications is the 18 | :class:`CliRunner` which can invoke commands as command line scripts. The 19 | :meth:`CliRunner.invoke` method runs the command line script in isolation 20 | and captures the output as both bytes and binary data. 21 | 22 | Note that :meth:`CliRunner.invoke` is asynchronous. The :func:`runner` 23 | fixture, which most Click tests use, contains a synchronous :attr:`invoke` 24 | for your convenience. 25 | 26 | The return value is a :class:`Result` object, which has the captured output 27 | data, exit code, and optional exception attached: 28 | 29 | .. code-block:: python 30 | :caption: hello.py 31 | 32 | import asyncclick as click 33 | 34 | @click.command() 35 | @click.argument('name') 36 | async def hello(name): 37 | click.echo(f'Hello {name}!') 38 | 39 | .. code-block:: python 40 | :caption: test_hello.py 41 | 42 | from asyncclick.testing import CliRunner 43 | from hello import hello 44 | 45 | @pytest.mark.anyio 46 | async def test_hello_world(): 47 | runner = CliRunner() 48 | result = await runner.invoke(hello, ['Peter']) 49 | assert result.exit_code == 0 50 | assert result.output == 'Hello Peter!\n' 51 | 52 | For subcommand testing, a subcommand name must be specified in the `args` parameter of :meth:`CliRunner.invoke` method: 53 | 54 | .. code-block:: python 55 | :caption: sync.py 56 | 57 | import asyncclick as click 58 | 59 | @click.group() 60 | @click.option('--debug/--no-debug', default=False) 61 | async def cli(debug): 62 | click.echo(f"Debug mode is {'on' if debug else 'off'}") 63 | 64 | @cli.command() 65 | async def sync(): 66 | click.echo('Syncing') 67 | 68 | .. code-block:: python 69 | :caption: test_sync.py 70 | 71 | from asyncclick.testing import CliRunner 72 | from sync import cli 73 | 74 | @pytest.mark.anyio 75 | async def test_sync(): 76 | runner = CliRunner() 77 | result = await runner.invoke(cli, ['--debug', 'sync']) 78 | assert result.exit_code == 0 79 | assert 'Debug mode is on' in result.output 80 | assert 'Syncing' in result.output 81 | 82 | Additional keyword arguments passed to ``.invoke()`` will be used to construct the initial Context object. 83 | For example, if you want to run your tests against a fixed terminal width you can use the following:: 84 | 85 | runner = CliRunner() 86 | result = await runner.invoke(cli, ['--debug', 'sync'], terminal_width=60) 87 | 88 | File System Isolation 89 | --------------------- 90 | 91 | For basic command line tools with file system operations, the 92 | :meth:`CliRunner.isolated_filesystem` method is useful for setting the 93 | current working directory to a new, empty folder. 94 | 95 | .. code-block:: python 96 | :caption: cat.py 97 | 98 | import asyncclick as click 99 | 100 | @click.command() 101 | @click.argument('f', type=click.File()) 102 | async def cat(f): 103 | click.echo(f.read()) 104 | 105 | .. code-block:: python 106 | :caption: test_cat.py 107 | 108 | from asyncclick.testing import CliRunner 109 | from cat import cat 110 | 111 | @pytest.mark.anyio 112 | async def test_cat(): 113 | runner = CliRunner() 114 | with runner.isolated_filesystem(): 115 | with open('hello.txt', 'w') as f: 116 | f.write('Hello World!') 117 | 118 | result = await runner.invoke(cat, ['hello.txt']) 119 | assert result.exit_code == 0 120 | assert result.output == 'Hello World!\n' 121 | 122 | Pass ``temp_dir`` to control where the temporary directory is created. 123 | The directory will not be removed by Click in this case. This is useful 124 | to integrate with a framework like Pytest that manages temporary files. 125 | 126 | .. code-block:: python 127 | 128 | def test_keep_dir(tmp_path): 129 | runner = CliRunner() 130 | 131 | with runner.isolated_filesystem(temp_dir=tmp_path) as td: 132 | ... 133 | 134 | 135 | Input Streams 136 | ------------- 137 | 138 | The test wrapper can also be used to provide input data for the input 139 | stream (stdin). This is very useful for testing prompts, for instance: 140 | 141 | .. code-block:: python 142 | :caption: prompt.py 143 | 144 | import asyncclick as click 145 | 146 | @click.command() 147 | @click.option('--foo', prompt=True) 148 | async def prompt(foo): 149 | click.echo(f"foo={foo}") 150 | 151 | .. code-block:: python 152 | :caption: test_prompt.py 153 | 154 | from asyncclick.testing import CliRunner 155 | from prompt import prompt 156 | 157 | def test_prompts(): 158 | runner = CliRunner() 159 | result = await runner.invoke(prompt, input='wau wau\n') 160 | assert not result.exception 161 | assert result.output == 'Foo: wau wau\nfoo=wau wau\n' 162 | 163 | Note that prompts will be emulated so that they write the input data to 164 | the output stream as well. If hidden input is expected then this 165 | obviously does not happen. 166 | -------------------------------------------------------------------------------- /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/upgrading.rst: -------------------------------------------------------------------------------- 1 | Upgrading To Newer Releases 2 | =========================== 3 | 4 | Click attempts the highest level of backwards compatibility but sometimes 5 | this is not entirely possible. In case we need to break backwards 6 | compatibility this document gives you information about how to upgrade or 7 | handle backwards compatibility properly. 8 | 9 | .. _upgrade-to-anyio: 10 | 11 | Upgrading to asyncclick 12 | ----------------------- 13 | 14 | The anyio-compatible version of Click is mostly backwards compatible. 15 | 16 | Several methods, most notably :meth:`BaseCommand.main` and 17 | :meth:`Context.invoke`, are now asynchronous. 18 | The :meth:`BaseCommand.__call__` alias invokes the main entry point via 19 | `anyio.run`. If you already have an async main program, simply use 20 | ``await cmd.main()`` instead of ``cmd()``. 21 | 22 | Commands and callbacks may be asynchronous; Click auto-``await``s them. 23 | 24 | Support for Python 2.x was dropped. 25 | 26 | .. _upgrade-to-7.0: 27 | 28 | Upgrading to 7.0 29 | ---------------- 30 | 31 | Commands that take their name from the decorated function now replace 32 | underscores with dashes. For example, the Python function ``run_server`` 33 | will get the command name ``run-server`` now. There are a few options 34 | to address this: 35 | 36 | - To continue with the new behavior, pin your dependency to 37 | ``Click>=7`` and update any documentation to use dashes. 38 | - To keep existing behavior, add an explicit command name with 39 | underscores, like ``@click.command("run_server")``. 40 | - To try a name with dashes if the name with underscores was not 41 | found, pass a ``token_normalize_func`` to the context: 42 | 43 | .. code-block:: python 44 | 45 | def normalize(name): 46 | return name.replace("_", "-") 47 | 48 | @click.group(context_settings={"token_normalize_func": normalize}) 49 | def group(): 50 | ... 51 | 52 | @group.command() 53 | def run_server(): 54 | ... 55 | 56 | 57 | .. _upgrade-to-3.2: 58 | 59 | Upgrading to 3.2 60 | ---------------- 61 | 62 | Click 3.2 had to perform two changes to multi commands which were 63 | triggered by a change between Click 2 and Click 3 that had bigger 64 | consequences than anticipated. 65 | 66 | Context Invokes 67 | ``````````````` 68 | 69 | Click 3.2 contains a fix for the :meth:`Context.invoke` function when used 70 | with other commands. The original intention of this function was to 71 | invoke the other command as as if it came from the command line when it 72 | was passed a context object instead of a function. This use was only 73 | documented in a single place in the documentation before and there was no 74 | proper explanation for the method in the API documentation. 75 | 76 | The core issue is that before 3.2 this call worked against intentions:: 77 | 78 | ctx.invoke(other_command, 'arg1', 'arg2') 79 | 80 | This was never intended to work as it does not allow Click to operate on 81 | the parameters. Given that this pattern was never documented and ill 82 | intended the decision was made to change this behavior in a bugfix release 83 | before it spreads by accident and developers depend on it. 84 | 85 | The correct invocation for the above command is the following:: 86 | 87 | ctx.invoke(other_command, name_of_arg1='arg1', name_of_arg2='arg2') 88 | 89 | This also allowed us to fix the issue that defaults were not handled 90 | properly by this function. 91 | 92 | Multicommand Chaining API 93 | ````````````````````````` 94 | 95 | Click 3 introduced multicommand chaining. This required a change in how 96 | Click internally dispatches. Unfortunately this change was not correctly 97 | implemented and it appeared that it was possible to provide an API that 98 | can inform the super command about all the subcommands that will be 99 | invoked. 100 | 101 | This assumption however does not work with one of the API guarantees that 102 | have been given in the past. As such this functionality has been removed 103 | in 3.2 as it was already broken. Instead the accidentally broken 104 | functionality of the :attr:`Context.invoked_subcommand` attribute was 105 | restored. 106 | 107 | If you do require the know which exact commands will be invoked there are 108 | different ways to cope with this. The first one is to let the subcommands 109 | all return functions and then to invoke the functions in a 110 | :meth:`Context.result_callback`. 111 | 112 | 113 | .. _upgrade-to-2.0: 114 | 115 | Upgrading to 2.0 116 | ---------------- 117 | 118 | Click 2.0 has one breaking change which is the signature for parameter 119 | callbacks. Before 2.0, the callback was invoked with ``(ctx, value)`` 120 | whereas now it's ``(ctx, param, value)``. This change was necessary as it 121 | otherwise made reusing callbacks too complicated. 122 | 123 | To ease the transition Click will still accept old callbacks. Starting 124 | with Click 3.0 it will start to issue a warning to stderr to encourage you 125 | to upgrade. 126 | 127 | In case you want to support both Click 1.0 and Click 2.0, you can make a 128 | simple decorator that adjusts the signatures:: 129 | 130 | import asyncclick as click 131 | from functools import update_wrapper 132 | 133 | def compatcallback(f): 134 | # Click 1.0 does not have a version string stored, so we need to 135 | # use getattr here to be safe. 136 | if getattr(click, '__version__', '0.0') >= '2.0': 137 | return f 138 | return update_wrapper(lambda ctx, value: f(ctx, None, value), f) 139 | 140 | With that helper you can then write something like this:: 141 | 142 | @compatcallback 143 | def callback(ctx, param, value): 144 | return value.upper() 145 | 146 | Note that because Click 1.0 did not pass a parameter, the `param` argument 147 | here would be `None`, so a compatibility callback could not use that 148 | argument. 149 | -------------------------------------------------------------------------------- /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 | 7 | This is done this way so that scripts also properly work 8 | on Windows and in virtualenvs without accidentally executing 9 | through the wrong interpreter. 10 | 11 | For more information about this see the documentation: 12 | https://click.palletsprojects.com/setuptools/ 13 | -------------------------------------------------------------------------------- /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 asyncclick as click 5 | 6 | 7 | class Config: 8 | """The config in this example only holds aliases.""" 9 | 10 | def __init__(self): 11 | self.path = os.getcwd() 12 | self.aliases = {} 13 | 14 | def add_alias(self, alias, cmd): 15 | self.aliases.update({alias: cmd}) 16 | 17 | def read_config(self, filename): 18 | parser = configparser.RawConfigParser() 19 | parser.read([filename]) 20 | try: 21 | self.aliases.update(parser.items("aliases")) 22 | except configparser.NoSectionError: 23 | pass 24 | 25 | def write_config(self, filename): 26 | parser = configparser.RawConfigParser() 27 | parser.add_section("aliases") 28 | for key, value in self.aliases.items(): 29 | parser.set("aliases", key, value) 30 | with open(filename, "wb") as file: 31 | parser.write(file) 32 | 33 | 34 | pass_config = click.make_pass_decorator(Config, ensure=True) 35 | 36 | 37 | class AliasedGroup(click.Group): 38 | """This subclass of a group supports looking up aliases in a config 39 | file and with a bit of magic. 40 | """ 41 | 42 | def get_command(self, ctx, cmd_name): 43 | # Step one: bulitin commands as normal 44 | rv = click.Group.get_command(self, ctx, cmd_name) 45 | if rv is not None: 46 | return rv 47 | 48 | # Step two: find the config object and ensure it's there. This 49 | # will create the config object is missing. 50 | cfg = ctx.ensure_object(Config) 51 | 52 | # Step three: look up an explicit command alias in the config 53 | if cmd_name in cfg.aliases: 54 | actual_cmd = cfg.aliases[cmd_name] 55 | return click.Group.get_command(self, ctx, actual_cmd) 56 | 57 | # Alternative option: if we did not find an explicit alias we 58 | # allow automatic abbreviation of the command. "status" for 59 | # instance will match "st". We only allow that however if 60 | # there is only one command. 61 | matches = [ 62 | x for x in self.list_commands(ctx) if x.lower().startswith(cmd_name.lower()) 63 | ] 64 | if not matches: 65 | return None 66 | elif len(matches) == 1: 67 | return click.Group.get_command(self, ctx, matches[0]) 68 | ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 69 | 70 | def resolve_command(self, ctx, args): 71 | # always return the command's name, not the alias 72 | _, cmd, args = super().resolve_command(ctx, args) 73 | return cmd.name, cmd, args 74 | 75 | 76 | def read_config(ctx, param, value): 77 | """Callback that is used whenever --config is passed. We use this to 78 | always load the correct config. This means that the config is loaded 79 | even if the group itself never executes so our aliases stay always 80 | available. 81 | """ 82 | cfg = ctx.ensure_object(Config) 83 | if value is None: 84 | value = os.path.join(os.path.dirname(__file__), "aliases.ini") 85 | cfg.read_config(value) 86 | return value 87 | 88 | 89 | @click.command(cls=AliasedGroup) 90 | @click.option( 91 | "--config", 92 | type=click.Path(exists=True, dir_okay=False), 93 | callback=read_config, 94 | expose_value=False, 95 | help="The config file to use instead of the default.", 96 | ) 97 | def cli(): 98 | """An example application that supports aliases.""" 99 | 100 | 101 | @cli.command() 102 | def push(): 103 | """Pushes changes.""" 104 | click.echo("Push") 105 | 106 | 107 | @cli.command() 108 | def pull(): 109 | """Pulls changes.""" 110 | click.echo("Pull") 111 | 112 | 113 | @cli.command() 114 | def clone(): 115 | """Clones a repository.""" 116 | click.echo("Clone") 117 | 118 | 119 | @cli.command() 120 | def commit(): 121 | """Commits pending changes.""" 122 | click.echo("Commit") 123 | 124 | 125 | @cli.command() 126 | @pass_config 127 | def status(config): 128 | """Shows the status.""" 129 | click.echo(f"Status for {config.path}") 130 | 131 | 132 | @cli.command() 133 | @pass_config 134 | @click.argument("alias_", metavar="ALIAS", type=click.STRING) 135 | @click.argument("cmd", type=click.STRING) 136 | @click.option( 137 | "--config_file", type=click.Path(exists=True, dir_okay=False), default="aliases.ini" 138 | ) 139 | def alias(config, alias_, cmd, config_file): 140 | """Adds an alias to the specified configuration file.""" 141 | config.add_alias(alias_, cmd) 142 | config.write_config(config_file) 143 | click.echo(f"Added '{alias_}' as alias for '{cmd}'") 144 | -------------------------------------------------------------------------------- /examples/aliases/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-aliases", 5 | version="1.0", 6 | py_modules=["aliases"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | aliases=aliases:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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 asyncclick as click 2 | 3 | 4 | all_colors = ( 5 | "black", 6 | "red", 7 | "green", 8 | "yellow", 9 | "blue", 10 | "magenta", 11 | "cyan", 12 | "white", 13 | "bright_black", 14 | "bright_red", 15 | "bright_green", 16 | "bright_yellow", 17 | "bright_blue", 18 | "bright_magenta", 19 | "bright_cyan", 20 | "bright_white", 21 | ) 22 | 23 | 24 | @click.command() 25 | def cli(): 26 | """This script prints some colors. It will also automatically remove 27 | all ANSI styles if data is piped into a file. 28 | 29 | Give it a try! 30 | """ 31 | for color in all_colors: 32 | click.echo(click.style(f"I am colored {color}", fg=color)) 33 | for color in all_colors: 34 | click.echo(click.style(f"I am colored {color} and bold", fg=color, bold=True)) 35 | for color in all_colors: 36 | click.echo(click.style(f"I am reverse colored {color}", fg=color, reverse=True)) 37 | 38 | click.echo(click.style("I am blinking", blink=True)) 39 | click.echo(click.style("I am underlined", underline=True)) 40 | -------------------------------------------------------------------------------- /examples/colors/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-colors", 5 | version="1.0", 6 | py_modules=["colors"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | colors=colors:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/completion/completion.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import asyncclick as click 4 | from asyncclick.shell_completion import CompletionItem 5 | 6 | 7 | @click.group() 8 | def cli(): 9 | pass 10 | 11 | 12 | @cli.command() 13 | @click.option("--dir", type=click.Path(file_okay=False)) 14 | def ls(dir): 15 | click.echo("\n".join(os.listdir(dir))) 16 | 17 | 18 | def get_env_vars(ctx, param, incomplete): 19 | # Returning a list of values is a shortcut to returning a list of 20 | # CompletionItem(value). 21 | return [k for k in os.environ if incomplete in k] 22 | 23 | 24 | @cli.command(help="A command to print environment variables") 25 | @click.argument("envvar", shell_complete=get_env_vars) 26 | def show_env(envvar): 27 | click.echo(f"Environment variable: {envvar}") 28 | click.echo(f"Value: {os.environ[envvar]}") 29 | 30 | 31 | @cli.group(help="A group that holds a subcommand") 32 | def group(): 33 | pass 34 | 35 | 36 | def list_users(ctx, param, incomplete): 37 | # You can generate completions with help strings by returning a list 38 | # of CompletionItem. You can match on whatever you want, including 39 | # the help. 40 | items = [("bob", "butcher"), ("alice", "baker"), ("jerry", "candlestick maker")] 41 | out = [] 42 | 43 | for value, help in items: 44 | if incomplete in value or incomplete in help: 45 | out.append(CompletionItem(value, help=help)) 46 | 47 | return out 48 | 49 | 50 | @group.command(help="Choose a user") 51 | @click.argument("user", shell_complete=list_users) 52 | def select_user(user): 53 | click.echo(f"Chosen user is {user}") 54 | 55 | 56 | cli.add_command(group) 57 | -------------------------------------------------------------------------------- /examples/completion/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-completion", 5 | version="1.0", 6 | py_modules=["completion"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | completion=completion:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/examples/complex/complex/__init__.py -------------------------------------------------------------------------------- /examples/complex/complex/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import asyncclick as click 5 | 6 | 7 | CONTEXT_SETTINGS = dict(auto_envvar_prefix="COMPLEX") 8 | 9 | 10 | class Environment: 11 | def __init__(self): 12 | self.verbose = False 13 | self.home = os.getcwd() 14 | 15 | def log(self, msg, *args): 16 | """Logs a message to stderr.""" 17 | if args: 18 | msg %= args 19 | click.echo(msg, file=sys.stderr) 20 | 21 | def vlog(self, msg, *args): 22 | """Logs a message to stderr only if verbose is enabled.""" 23 | if self.verbose: 24 | self.log(msg, *args) 25 | 26 | 27 | pass_environment = click.make_pass_decorator(Environment, ensure=True) 28 | cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), "commands")) 29 | 30 | 31 | class ComplexCLI(click.MultiCommand): 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/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/examples/complex/complex/commands/__init__.py -------------------------------------------------------------------------------- /examples/complex/complex/commands/cmd_init.py: -------------------------------------------------------------------------------- 1 | from complex.cli import pass_environment 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.command("init", short_help="Initializes a repo.") 7 | @click.argument("path", required=False, type=click.Path(resolve_path=True)) 8 | @pass_environment 9 | def cli(ctx, path): 10 | """Initializes a repository.""" 11 | if path is None: 12 | path = ctx.home 13 | ctx.log(f"Initialized the repository in {click.format_filename(path)}") 14 | -------------------------------------------------------------------------------- /examples/complex/complex/commands/cmd_status.py: -------------------------------------------------------------------------------- 1 | from complex.cli import pass_environment 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.command("status", short_help="Shows file changes.") 7 | @pass_environment 8 | def cli(ctx): 9 | """Shows file changes in the current working directory.""" 10 | ctx.log("Changed files: none") 11 | ctx.vlog("bla bla bla, debug info") 12 | -------------------------------------------------------------------------------- /examples/complex/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-complex", 5 | version="1.0", 6 | packages=["complex", "complex.commands"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | complex=complex.cli:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /examples/imagepipe/.gitignore: -------------------------------------------------------------------------------- 1 | processed-* 2 | -------------------------------------------------------------------------------- /examples/imagepipe/README: -------------------------------------------------------------------------------- 1 | $ imagepipe_ 2 | 3 | imagepipe is an example application that implements some 4 | multi 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/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/examples/imagepipe/example01.jpg -------------------------------------------------------------------------------- /examples/imagepipe/example02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/examples/imagepipe/example02.jpg -------------------------------------------------------------------------------- /examples/imagepipe/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-imagepipe", 5 | version="1.0", 6 | py_modules=["imagepipe"], 7 | include_package_data=True, 8 | install_requires=["click", "pillow"], 9 | entry_points=""" 10 | [console_scripts] 11 | imagepipe=imagepipe:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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 asyncclick as click 2 | 3 | 4 | @click.command() 5 | @click.argument("input", type=click.File("rb"), nargs=-1) 6 | @click.argument("output", type=click.File("wb")) 7 | def cli(input, output): 8 | """This script works similar to the Unix `cat` command but it writes 9 | into a specific file (which could be the standard output as denoted by 10 | the ``-`` sign). 11 | 12 | \b 13 | Copy stdin to stdout: 14 | inout - - 15 | 16 | \b 17 | Copy foo.txt and bar.txt to stdout: 18 | inout foo.txt bar.txt - 19 | 20 | \b 21 | Write stdin into the file foo.txt 22 | inout - foo.txt 23 | """ 24 | for f in input: 25 | while True: 26 | chunk = f.read(1024) 27 | if not chunk: 28 | break 29 | output.write(chunk) 30 | output.flush() 31 | -------------------------------------------------------------------------------- /examples/inout/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-inout", 5 | version="0.1", 6 | py_modules=["inout"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | inout=inout:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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 asyncclick as click 2 | 3 | 4 | @click.group() 5 | @click.version_option() 6 | def cli(): 7 | """Naval Fate. 8 | 9 | This is the docopt example adopted to Click but with some actual 10 | commands implemented and not just the empty parsing which really 11 | is not all that interesting. 12 | """ 13 | 14 | 15 | @cli.group() 16 | def ship(): 17 | """Manages ships.""" 18 | 19 | 20 | @ship.command("new") 21 | @click.argument("name") 22 | def ship_new(name): 23 | """Creates a new ship.""" 24 | click.echo(f"Created ship {name}") 25 | 26 | 27 | @ship.command("move") 28 | @click.argument("ship") 29 | @click.argument("x", type=float) 30 | @click.argument("y", type=float) 31 | @click.option("--speed", metavar="KN", default=10, help="Speed in knots.") 32 | def ship_move(ship, x, y, speed): 33 | """Moves SHIP to the new location X,Y.""" 34 | click.echo(f"Moving ship {ship} to {x},{y} with speed {speed}") 35 | 36 | 37 | @ship.command("shoot") 38 | @click.argument("ship") 39 | @click.argument("x", type=float) 40 | @click.argument("y", type=float) 41 | def ship_shoot(ship, x, y): 42 | """Makes SHIP fire to X,Y.""" 43 | click.echo(f"Ship {ship} fires to {x},{y}") 44 | 45 | 46 | @cli.group("mine") 47 | def mine(): 48 | """Manages mines.""" 49 | 50 | 51 | @mine.command("set") 52 | @click.argument("x", type=float) 53 | @click.argument("y", type=float) 54 | @click.option( 55 | "ty", 56 | "--moored", 57 | flag_value="moored", 58 | default=True, 59 | help="Moored (anchored) mine. Default.", 60 | ) 61 | @click.option("ty", "--drifting", flag_value="drifting", help="Drifting mine.") 62 | def mine_set(x, y, ty): 63 | """Sets a mine at a specific coordinate.""" 64 | click.echo(f"Set {ty} mine at {x},{y}") 65 | 66 | 67 | @mine.command("remove") 68 | @click.argument("x", type=float) 69 | @click.argument("y", type=float) 70 | def mine_remove(x, y): 71 | """Removes a mine at a specific coordinate.""" 72 | click.echo(f"Removed mine at {x},{y}") 73 | -------------------------------------------------------------------------------- /examples/naval/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-naval", 5 | version="2.0", 6 | py_modules=["naval"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | naval=naval:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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/repo.py: -------------------------------------------------------------------------------- 1 | import os 2 | import posixpath 3 | import sys 4 | 5 | import asyncclick as click 6 | 7 | 8 | class Repo: 9 | def __init__(self, home): 10 | self.home = home 11 | self.config = {} 12 | self.verbose = False 13 | 14 | def set_config(self, key, value): 15 | self.config[key] = value 16 | if self.verbose: 17 | click.echo(f" config[{key}] = {value}", file=sys.stderr) 18 | 19 | def __repr__(self): 20 | return f"" 21 | 22 | 23 | pass_repo = click.make_pass_decorator(Repo) 24 | 25 | 26 | @click.group() 27 | @click.option( 28 | "--repo-home", 29 | envvar="REPO_HOME", 30 | default=".repo", 31 | metavar="PATH", 32 | help="Changes the repository folder location.", 33 | ) 34 | @click.option( 35 | "--config", 36 | nargs=2, 37 | multiple=True, 38 | metavar="KEY VALUE", 39 | help="Overrides a config key/value pair.", 40 | ) 41 | @click.option("--verbose", "-v", is_flag=True, help="Enables verbose mode.") 42 | @click.version_option("1.0") 43 | @click.pass_context 44 | def cli(ctx, repo_home, config, verbose): 45 | """Repo is a command line tool that showcases how to build complex 46 | command line interfaces with Click. 47 | 48 | This tool is supposed to look like a distributed version control 49 | system to show how something like this can be structured. 50 | """ 51 | # Create a repo object and remember it as as the context object. From 52 | # this point onwards other commands can refer to it by using the 53 | # @pass_repo decorator. 54 | ctx.obj = Repo(os.path.abspath(repo_home)) 55 | ctx.obj.verbose = verbose 56 | for key, value in config: 57 | ctx.obj.set_config(key, value) 58 | 59 | 60 | @cli.command() 61 | @click.argument("src") 62 | @click.argument("dest", required=False) 63 | @click.option( 64 | "--shallow/--deep", 65 | default=False, 66 | help="Makes a checkout shallow or deep. Deep by default.", 67 | ) 68 | @click.option( 69 | "--rev", "-r", default="HEAD", help="Clone a specific revision instead of HEAD." 70 | ) 71 | @pass_repo 72 | def clone(repo, src, dest, shallow, rev): 73 | """Clones a repository. 74 | 75 | This will clone the repository at SRC into the folder DEST. If DEST 76 | is not provided this will automatically use the last path component 77 | of SRC and create that folder. 78 | """ 79 | if dest is None: 80 | dest = posixpath.split(src)[-1] or "." 81 | click.echo(f"Cloning repo {src} to {os.path.basename(dest)}") 82 | repo.home = dest 83 | if shallow: 84 | click.echo("Making shallow checkout") 85 | click.echo(f"Checking out revision {rev}") 86 | 87 | 88 | @cli.command() 89 | @click.confirmation_option() 90 | @pass_repo 91 | def delete(repo): 92 | """Deletes a repository. 93 | 94 | This will throw away the current repository. 95 | """ 96 | click.echo(f"Destroying repo {repo.home}") 97 | click.echo("Deleted!") 98 | 99 | 100 | @cli.command() 101 | @click.option("--username", prompt=True, help="The developer's shown username.") 102 | @click.option("--email", prompt="E-Mail", help="The developer's email address") 103 | @click.password_option(help="The login password.") 104 | @pass_repo 105 | def setuser(repo, username, email, password): 106 | """Sets the user credentials. 107 | 108 | This will override the current user config. 109 | """ 110 | repo.set_config("username", username) 111 | repo.set_config("email", email) 112 | repo.set_config("password", "*" * len(password)) 113 | click.echo("Changed credentials.") 114 | 115 | 116 | @cli.command() 117 | @click.option( 118 | "--message", 119 | "-m", 120 | multiple=True, 121 | help="The commit message. If provided multiple times each" 122 | " argument gets converted into a new line.", 123 | ) 124 | @click.argument("files", nargs=-1, type=click.Path()) 125 | @pass_repo 126 | def commit(repo, files, message): 127 | """Commits outstanding changes. 128 | 129 | Commit changes to the given files into the repository. You will need to 130 | "repo push" to push up your changes to other repositories. 131 | 132 | If a list of files is omitted, all changes reported by "repo status" 133 | will be committed. 134 | """ 135 | if not message: 136 | marker = "# Files to be committed:" 137 | hint = ["", "", marker, "#"] 138 | for file in files: 139 | hint.append(f"# U {file}") 140 | message = click.edit("\n".join(hint)) 141 | if message is None: 142 | click.echo("Aborted!") 143 | return 144 | msg = message.split(marker)[0].rstrip() 145 | if not msg: 146 | click.echo("Aborted! Empty commit message") 147 | return 148 | else: 149 | msg = "\n".join(message) 150 | click.echo(f"Files to be committed: {files}") 151 | click.echo(f"Commit message:\n{msg}") 152 | 153 | 154 | @cli.command(short_help="Copies files.") 155 | @click.option( 156 | "--force", is_flag=True, help="forcibly copy over an existing managed file" 157 | ) 158 | @click.argument("src", nargs=-1, type=click.Path()) 159 | @click.argument("dst", type=click.Path()) 160 | @pass_repo 161 | def copy(repo, src, dst, force): 162 | """Copies one or multiple files to a new location. This copies all 163 | files from SRC to DST. 164 | """ 165 | for fn in src: 166 | click.echo(f"Copy from {fn} -> {dst}") 167 | -------------------------------------------------------------------------------- /examples/repo/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-repo", 5 | version="0.1", 6 | py_modules=["repo"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | repo=repo:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /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/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-termui", 5 | version="1.0", 6 | py_modules=["termui"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | termui=termui:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /examples/termui/termui.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import time 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.group() 9 | def cli(): 10 | """This script showcases different terminal UI helpers in Click.""" 11 | pass 12 | 13 | 14 | @cli.command() 15 | def colordemo(): 16 | """Demonstrates ANSI color support.""" 17 | for color in "red", "green", "blue": 18 | click.echo(click.style(f"I am colored {color}", fg=color)) 19 | click.echo(click.style(f"I am background colored {color}", bg=color)) 20 | 21 | 22 | @cli.command() 23 | def pager(): 24 | """Demonstrates using the pager.""" 25 | lines = [] 26 | for x in range(200): 27 | lines.append(f"{click.style(str(x), fg='green')}. Hello World!") 28 | click.echo_via_pager("\n".join(lines)) 29 | 30 | 31 | @cli.command() 32 | @click.option( 33 | "--count", 34 | default=8000, 35 | type=click.IntRange(1, 100000), 36 | help="The number of items to process.", 37 | ) 38 | def progress(count): 39 | """Demonstrates the progress bar.""" 40 | items = range(count) 41 | 42 | def process_slowly(item): 43 | time.sleep(0.002 * random.random()) 44 | 45 | def filter(items): 46 | for item in items: 47 | if random.random() > 0.3: 48 | yield item 49 | 50 | with click.progressbar( 51 | items, label="Processing accounts", fill_char=click.style("#", fg="green") 52 | ) as bar: 53 | for item in bar: 54 | process_slowly(item) 55 | 56 | def show_item(item): 57 | if item is not None: 58 | return f"Item #{item}" 59 | 60 | with click.progressbar( 61 | filter(items), 62 | label="Committing transaction", 63 | fill_char=click.style("#", fg="yellow"), 64 | item_show_func=show_item, 65 | ) as bar: 66 | for item in bar: 67 | process_slowly(item) 68 | 69 | with click.progressbar( 70 | length=count, 71 | label="Counting", 72 | bar_template="%(label)s %(bar)s | %(info)s", 73 | fill_char=click.style("█", fg="cyan"), 74 | empty_char=" ", 75 | ) as bar: 76 | for item in bar: 77 | process_slowly(item) 78 | 79 | with click.progressbar( 80 | length=count, 81 | width=0, 82 | show_percent=False, 83 | show_eta=False, 84 | fill_char=click.style("#", fg="magenta"), 85 | ) as bar: 86 | for item in bar: 87 | process_slowly(item) 88 | 89 | # 'Non-linear progress bar' 90 | steps = [math.exp(x * 1.0 / 20) - 1 for x in range(20)] 91 | count = int(sum(steps)) 92 | with click.progressbar( 93 | length=count, 94 | show_percent=False, 95 | label="Slowing progress bar", 96 | fill_char=click.style("█", fg="green"), 97 | ) as bar: 98 | for item in steps: 99 | time.sleep(item) 100 | bar.update(item) 101 | 102 | 103 | @cli.command() 104 | @click.argument("url") 105 | def open(url): 106 | """Opens a file or URL In the default application.""" 107 | click.launch(url) 108 | 109 | 110 | @cli.command() 111 | @click.argument("url") 112 | def locate(url): 113 | """Opens a file or URL In the default application.""" 114 | click.launch(url, locate=True) 115 | 116 | 117 | @cli.command() 118 | def edit(): 119 | """Opens an editor with some text in it.""" 120 | MARKER = "# Everything below is ignored\n" 121 | message = click.edit(f"\n\n{MARKER}") 122 | if message is not None: 123 | msg = message.split(MARKER, 1)[0].rstrip("\n") 124 | if not msg: 125 | click.echo("Empty message!") 126 | else: 127 | click.echo(f"Message:\n{msg}") 128 | else: 129 | click.echo("You did not enter anything!") 130 | 131 | 132 | @cli.command() 133 | def clear(): 134 | """Clears the entire screen.""" 135 | click.clear() 136 | 137 | 138 | @cli.command() 139 | def pause(): 140 | """Waits for the user to press a button.""" 141 | click.pause() 142 | 143 | 144 | @cli.command() 145 | def menu(): 146 | """Shows a simple menu.""" 147 | menu = "main" 148 | while True: 149 | if menu == "main": 150 | click.echo("Main menu:") 151 | click.echo(" d: debug menu") 152 | click.echo(" q: quit") 153 | char = click.getchar() 154 | if char == "d": 155 | menu = "debug" 156 | elif char == "q": 157 | menu = "quit" 158 | else: 159 | click.echo("Invalid input") 160 | elif menu == "debug": 161 | click.echo("Debug menu") 162 | click.echo(" b: back") 163 | char = click.getchar() 164 | if char == "b": 165 | menu = "main" 166 | else: 167 | click.echo("Invalid input") 168 | elif menu == "quit": 169 | return 170 | -------------------------------------------------------------------------------- /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/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="click-example-validation", 5 | version="1.0", 6 | py_modules=["validation"], 7 | include_package_data=True, 8 | install_requires=["click"], 9 | entry_points=""" 10 | [console_scripts] 11 | validation=validation:cli 12 | """, 13 | ) 14 | -------------------------------------------------------------------------------- /examples/validation/validation.py: -------------------------------------------------------------------------------- 1 | from urllib import parse as urlparse 2 | 3 | import asyncclick as click 4 | 5 | 6 | def validate_count(ctx, param, value): 7 | if value < 0 or value % 2 != 0: 8 | raise click.BadParameter("Should be a positive, even integer.") 9 | return value 10 | 11 | 12 | class URL(click.ParamType): 13 | name = "url" 14 | 15 | def convert(self, value, param, ctx): 16 | if not isinstance(value, tuple): 17 | value = urlparse.urlparse(value) 18 | if value.scheme not in ("http", "https"): 19 | self.fail( 20 | f"invalid URL scheme ({value.scheme}). Only HTTP URLs are allowed", 21 | param, 22 | ctx, 23 | ) 24 | return value 25 | 26 | 27 | @click.command() 28 | @click.option( 29 | "--count", default=2, callback=validate_count, help="A positive even number." 30 | ) 31 | @click.option("--foo", help="A mysterious parameter.") 32 | @click.option("--url", help="A URL", type=URL()) 33 | @click.version_option() 34 | def cli(count, foo, url): 35 | """Validation. 36 | 37 | This example validates parameters in different ways. It does it 38 | through callbacks, through a custom type as well as by validating 39 | manually in the function. 40 | """ 41 | if foo is not None and foo != "wat": 42 | raise click.BadParameter( 43 | 'If a value is provided it needs to be the value "wat".', 44 | param_hint=["--foo"], 45 | ) 46 | click.echo(f"count: {count}") 47 | click.echo(f"foo: {foo}") 48 | click.echo(f"url: {url!r}") 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "asyncclick" 3 | description = "Composable command line interface toolkit, " 4 | readme = "README.md" 5 | license = {file = "LICENSE.txt"} 6 | maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] 7 | classifiers = [ 8 | "Development Status :: 5 - Production/Stable", 9 | "Intended Audience :: Developers", 10 | "License :: OSI Approved :: BSD License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python", 13 | "Typing :: Typed", 14 | ] 15 | requires-python = ">=3.9" 16 | dependencies = [ 17 | "colorama; platform_system == 'Windows'", 18 | "anyio ~= 4.0", 19 | ] 20 | dynamic = ["version"] 21 | 22 | [project.urls] 23 | Donate = "https://palletsprojects.com/donate" 24 | Documentation = "https://click.palletsprojects.com/" 25 | Changes = "https://click.palletsprojects.com/changes/" 26 | Source = "https://github.com/python-trio/asyncclick" 27 | Chat = "https://discord.gg/pallets" 28 | 29 | [build-system] 30 | requires = ["flit_core<4"] 31 | build-backend = "flit_core.buildapi" 32 | 33 | [tool.flit.module] 34 | name = "asyncclick" 35 | 36 | [tool.flit.sdist] 37 | include = [ 38 | "docs/", 39 | "requirements/", 40 | "tests/", 41 | "CHANGES.rst", 42 | "tox.ini", 43 | ] 44 | exclude = [ 45 | "docs/_build/", 46 | ] 47 | 48 | [tool.pytest.ini_options] 49 | testpaths = ["tests"] 50 | filterwarnings = [ 51 | "error", 52 | ] 53 | 54 | [tool.coverage.run] 55 | branch = true 56 | source = ["asyncclick", "tests"] 57 | 58 | [tool.coverage.paths] 59 | source = ["src", "*/site-packages"] 60 | 61 | [tool.mypy] 62 | python_version = "3.8" 63 | files = ["src/asyncclick", "tests/typing"] 64 | show_error_codes = true 65 | pretty = true 66 | strict = true 67 | 68 | [[tool.mypy.overrides]] 69 | module = [ 70 | "colorama.*", 71 | ] 72 | ignore_missing_imports = true 73 | 74 | [tool.pyright] 75 | pythonVersion = "3.8" 76 | include = ["src/asyncclick", "tests/typing"] 77 | typeCheckingMode = "basic" 78 | 79 | [tool.ruff] 80 | extend-exclude = ["examples/"] 81 | src = ["src"] 82 | fix = true 83 | show-fixes = true 84 | output-format = "full" 85 | 86 | [tool.ruff.lint] 87 | select = [ 88 | "B", # flake8-bugbear 89 | "E", # pycodestyle error 90 | "F", # pyflakes 91 | "I", # isort 92 | "UP", # pyupgrade 93 | "W", # pycodestyle warning 94 | ] 95 | 96 | [tool.ruff.lint.isort] 97 | force-single-line = true 98 | order-by-type = false 99 | 100 | [tool.gha-update] 101 | tag-only = [ 102 | "slsa-framework/slsa-github-generator", 103 | ] 104 | -------------------------------------------------------------------------------- /requirements/build.in: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /requirements/build.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/build.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | build==1.2.2.post1 13 | # via -r requirements/build.in 14 | packaging==24.2 15 | # via build 16 | pyproject-hooks==1.2.0 17 | # via build 18 | -------------------------------------------------------------------------------- /requirements/dev.in: -------------------------------------------------------------------------------- 1 | -r docs.in 2 | -r tests.in 3 | -r typing.in 4 | pip-compile-multi 5 | pre-commit 6 | tox 7 | anyio 8 | trio 9 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/dev.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | alabaster==1.0.0 13 | # via sphinx 14 | anyio==4.8.0 15 | # via -r requirements/dev.in 16 | attrs==24.3.0 17 | # via 18 | # outcome 19 | # trio 20 | babel==2.16.0 21 | # via sphinx 22 | build==1.2.2.post1 23 | # via pip-tools 24 | cachetools==5.5.0 25 | # via tox 26 | certifi==2024.8.30 27 | # via requests 28 | cfgv==3.4.0 29 | # via pre-commit 30 | chardet==5.2.0 31 | # via tox 32 | charset-normalizer==3.4.0 33 | # via requests 34 | click==8.1.7 35 | # via 36 | # pip-compile-multi 37 | # pip-tools 38 | colorama==0.4.6 39 | # via tox 40 | distlib==0.3.9 41 | # via virtualenv 42 | docutils==0.21.2 43 | # via 44 | # sphinx 45 | # sphinx-tabs 46 | filelock==3.16.1 47 | # via 48 | # tox 49 | # virtualenv 50 | identify==2.6.3 51 | # via pre-commit 52 | idna==3.10 53 | # via 54 | # anyio 55 | # requests 56 | # trio 57 | imagesize==1.4.1 58 | # via sphinx 59 | iniconfig==2.0.0 60 | # via pytest 61 | jinja2==3.1.4 62 | # via sphinx 63 | markupsafe==3.0.2 64 | # via jinja2 65 | mypy==1.13.0 66 | # via -r /src/asyncclick/requirements/typing.in 67 | mypy-extensions==1.0.0 68 | # via mypy 69 | nodeenv==1.9.1 70 | # via 71 | # pre-commit 72 | # pyright 73 | outcome==1.3.0.post0 74 | # via trio 75 | packaging==24.2 76 | # via 77 | # build 78 | # pallets-sphinx-themes 79 | # pyproject-api 80 | # pytest 81 | # sphinx 82 | # tox 83 | pallets-sphinx-themes==2.3.0 84 | # via -r /src/asyncclick/requirements/docs.in 85 | pip-compile-multi==2.7.1 86 | # via -r requirements/dev.in 87 | pip-tools==7.4.1 88 | # via pip-compile-multi 89 | platformdirs==4.3.6 90 | # via 91 | # tox 92 | # virtualenv 93 | pluggy==1.5.0 94 | # via 95 | # pytest 96 | # tox 97 | pre-commit==4.0.1 98 | # via -r requirements/dev.in 99 | pygments==2.18.0 100 | # via 101 | # sphinx 102 | # sphinx-tabs 103 | pyproject-api==1.8.0 104 | # via tox 105 | pyproject-hooks==1.2.0 106 | # via 107 | # build 108 | # pip-tools 109 | pyright==1.1.390 110 | # via -r /src/asyncclick/requirements/typing.in 111 | pytest==8.3.4 112 | # via -r /src/asyncclick/requirements/tests.in 113 | pyyaml==6.0.2 114 | # via pre-commit 115 | requests==2.32.3 116 | # via sphinx 117 | sniffio==1.3.1 118 | # via 119 | # anyio 120 | # trio 121 | snowballstemmer==2.2.0 122 | # via sphinx 123 | sortedcontainers==2.4.0 124 | # via trio 125 | sphinx==8.1.3 126 | # via 127 | # -r /src/asyncclick/requirements/docs.in 128 | # pallets-sphinx-themes 129 | # sphinx-issues 130 | # sphinx-notfound-page 131 | # sphinx-tabs 132 | # sphinxcontrib-log-cabinet 133 | sphinx-issues==5.0.0 134 | # via -r /src/asyncclick/requirements/docs.in 135 | sphinx-notfound-page==1.0.4 136 | # via pallets-sphinx-themes 137 | sphinx-tabs==3.4.7 138 | # via -r /src/asyncclick/requirements/docs.in 139 | sphinxcontrib-applehelp==2.0.0 140 | # via sphinx 141 | sphinxcontrib-devhelp==2.0.0 142 | # via sphinx 143 | sphinxcontrib-htmlhelp==2.1.0 144 | # via sphinx 145 | sphinxcontrib-jsmath==1.0.1 146 | # via sphinx 147 | sphinxcontrib-log-cabinet==1.0.1 148 | # via -r /src/asyncclick/requirements/docs.in 149 | sphinxcontrib-qthelp==2.0.0 150 | # via sphinx 151 | sphinxcontrib-serializinghtml==2.0.0 152 | # via sphinx 153 | toposort==1.10 154 | # via pip-compile-multi 155 | tox==4.23.2 156 | # via -r requirements/dev.in 157 | trio==0.28.0 158 | # via 159 | # -r /src/asyncclick/requirements/tests.in 160 | # -r requirements/dev.in 161 | typing-extensions==4.12.2 162 | # via 163 | # anyio 164 | # mypy 165 | # pyright 166 | urllib3==2.2.3 167 | # via requests 168 | virtualenv==20.28.0 169 | # via 170 | # pre-commit 171 | # tox 172 | wheel==0.45.1 173 | # via pip-tools 174 | 175 | # The following packages are considered to be unsafe in a requirements file: 176 | # pip 177 | # setuptools 178 | -------------------------------------------------------------------------------- /requirements/docs.in: -------------------------------------------------------------------------------- 1 | Pallets-Sphinx-Themes 2 | Sphinx 3 | sphinx-issues 4 | sphinxcontrib-log-cabinet 5 | sphinx-tabs 6 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/docs.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | alabaster==1.0.0 13 | # via sphinx 14 | babel==2.16.0 15 | # via sphinx 16 | certifi==2024.8.30 17 | # via requests 18 | charset-normalizer==3.4.0 19 | # via requests 20 | docutils==0.21.2 21 | # via 22 | # sphinx 23 | # sphinx-tabs 24 | idna==3.10 25 | # via requests 26 | imagesize==1.4.1 27 | # via sphinx 28 | jinja2==3.1.4 29 | # via sphinx 30 | markupsafe==3.0.2 31 | # via jinja2 32 | packaging==24.2 33 | # via 34 | # pallets-sphinx-themes 35 | # sphinx 36 | pallets-sphinx-themes==2.3.0 37 | # via -r requirements/docs.in 38 | pygments==2.18.0 39 | # via 40 | # sphinx 41 | # sphinx-tabs 42 | requests==2.32.3 43 | # via sphinx 44 | snowballstemmer==2.2.0 45 | # via sphinx 46 | sphinx==8.1.3 47 | # via 48 | # -r requirements/docs.in 49 | # pallets-sphinx-themes 50 | # sphinx-issues 51 | # sphinx-notfound-page 52 | # sphinx-tabs 53 | # sphinxcontrib-log-cabinet 54 | sphinx-issues==5.0.0 55 | # via -r requirements/docs.in 56 | sphinx-notfound-page==1.0.4 57 | # via pallets-sphinx-themes 58 | sphinx-tabs==3.4.7 59 | # via -r requirements/docs.in 60 | sphinxcontrib-applehelp==2.0.0 61 | # via sphinx 62 | sphinxcontrib-devhelp==2.0.0 63 | # via sphinx 64 | sphinxcontrib-htmlhelp==2.1.0 65 | # via sphinx 66 | sphinxcontrib-jsmath==1.0.1 67 | # via sphinx 68 | sphinxcontrib-log-cabinet==1.0.1 69 | # via -r requirements/docs.in 70 | sphinxcontrib-qthelp==2.0.0 71 | # via sphinx 72 | sphinxcontrib-serializinghtml==2.0.0 73 | # via sphinx 74 | urllib3==2.2.3 75 | # via requests 76 | -------------------------------------------------------------------------------- /requirements/tests.in: -------------------------------------------------------------------------------- 1 | pytest 2 | trio 3 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/tests.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | attrs==24.3.0 13 | # via 14 | # outcome 15 | # trio 16 | idna==3.10 17 | # via trio 18 | iniconfig==2.0.0 19 | # via pytest 20 | outcome==1.3.0.post0 21 | # via trio 22 | packaging==24.2 23 | # via pytest 24 | pluggy==1.5.0 25 | # via pytest 26 | pytest==8.3.4 27 | # via -r requirements/tests.in 28 | sniffio==1.3.1 29 | # via trio 30 | sortedcontainers==2.4.0 31 | # via trio 32 | trio==0.28.0 33 | # via -r requirements/tests.in 34 | -------------------------------------------------------------------------------- /requirements/tests37.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.7 3 | # by the following command: 4 | # 5 | # pip-compile --output-file=tests37.txt tests.in 6 | # 7 | exceptiongroup==1.2.2 8 | # via pytest 9 | importlib-metadata==6.7.0 10 | # via 11 | # pluggy 12 | # pytest 13 | iniconfig==2.0.0 14 | # via pytest 15 | packaging==24.0 16 | # via pytest 17 | pluggy==1.2.0 18 | # via pytest 19 | pytest==7.4.4 20 | # via -r tests.in 21 | tomli==2.0.1 22 | # via pytest 23 | typing-extensions==4.7.1 24 | # via importlib-metadata 25 | zipp==3.15.0 26 | # via importlib-metadata 27 | -------------------------------------------------------------------------------- /requirements/typing.in: -------------------------------------------------------------------------------- 1 | mypy 2 | pyright 3 | -------------------------------------------------------------------------------- /requirements/typing.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.12 3 | # by the following command: 4 | # 5 | # pip-compile requirements/typing.in 6 | # 7 | --trusted-host pypi.python.org 8 | --trusted-host pypi.org 9 | --trusted-host files.pythonhosted.org 10 | --trusted-host pypi01vp.office.noris.de 11 | 12 | mypy==1.13.0 13 | # via -r requirements/typing.in 14 | mypy-extensions==1.0.0 15 | # via mypy 16 | nodeenv==1.9.1 17 | # via pyright 18 | pyright==1.1.390 19 | # via -r requirements/typing.in 20 | typing-extensions==4.12.2 21 | # via 22 | # mypy 23 | # pyright 24 | -------------------------------------------------------------------------------- /src/asyncclick/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Click is a simple Python module inspired by the stdlib optparse to make 3 | writing command line scripts fun. Unlike other modules, it's based 4 | around a simple API that does not come with too much magic and is 5 | composable. 6 | """ 7 | 8 | from .core import Argument as Argument 9 | from .core import BaseCommand as BaseCommand 10 | from .core import Command as Command 11 | from .core import CommandCollection as CommandCollection 12 | from .core import Context as Context 13 | from .core import Group as Group 14 | from .core import MultiCommand as MultiCommand 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 HelpOption as HelpOption 23 | from .decorators import make_pass_decorator as make_pass_decorator 24 | from .decorators import option as option 25 | from .decorators import pass_context as pass_context 26 | from .decorators import pass_obj as pass_obj 27 | from .decorators import password_option as password_option 28 | from .decorators import version_option as version_option 29 | from .exceptions import Abort as Abort 30 | from .exceptions import BadArgumentUsage as BadArgumentUsage 31 | from .exceptions import BadOptionUsage as BadOptionUsage 32 | from .exceptions import BadParameter as BadParameter 33 | from .exceptions import ClickException as ClickException 34 | from .exceptions import FileError as FileError 35 | from .exceptions import MissingParameter as MissingParameter 36 | from .exceptions import NoSuchOption as NoSuchOption 37 | from .exceptions import UsageError as UsageError 38 | from .formatting import HelpFormatter as HelpFormatter 39 | from .formatting import wrap_text as wrap_text 40 | from .globals import get_current_context as get_current_context 41 | from .parser import OptionParser as OptionParser 42 | from .termui import clear as clear 43 | from .termui import confirm as confirm 44 | from .termui import echo_via_pager as echo_via_pager 45 | from .termui import edit as edit 46 | from .termui import getchar as getchar 47 | from .termui import launch as launch 48 | from .termui import pause as pause 49 | from .termui import progressbar as progressbar 50 | from .termui import prompt as prompt 51 | from .termui import secho as secho 52 | from .termui import style as style 53 | from .termui import unstyle as unstyle 54 | from .types import BOOL as BOOL 55 | from .types import Choice as Choice 56 | from .types import DateTime as DateTime 57 | from .types import File as File 58 | from .types import FLOAT as FLOAT 59 | from .types import FloatRange as FloatRange 60 | from .types import INT as INT 61 | from .types import IntRange as IntRange 62 | from .types import ParamType as ParamType 63 | from .types import Path as Path 64 | from .types import STRING as STRING 65 | from .types import Tuple as Tuple 66 | from .types import UNPROCESSED as UNPROCESSED 67 | from .types import UUID as UUID 68 | from .utils import echo as echo 69 | from .utils import format_filename as format_filename 70 | from .utils import get_app_dir as get_app_dir 71 | from .utils import get_binary_stream as get_binary_stream 72 | from .utils import get_text_stream as get_text_stream 73 | from .utils import open_file as open_file 74 | 75 | __version__ = "8.1.8.0" 76 | -------------------------------------------------------------------------------- /src/asyncclick/_textwrap.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import typing as t 3 | from contextlib import contextmanager 4 | 5 | 6 | class TextWrapper(textwrap.TextWrapper): 7 | def _handle_long_word( 8 | self, 9 | reversed_chunks: t.List[str], 10 | cur_line: t.List[str], 11 | cur_len: int, 12 | width: int, 13 | ) -> None: 14 | space_left = max(width - cur_len, 1) 15 | 16 | if self.break_long_words: 17 | last = reversed_chunks[-1] 18 | cut = last[:space_left] 19 | res = last[space_left:] 20 | cur_line.append(cut) 21 | reversed_chunks[-1] = res 22 | elif not cur_line: 23 | cur_line.append(reversed_chunks.pop()) 24 | 25 | @contextmanager 26 | def extra_indent(self, indent: str) -> t.Iterator[None]: 27 | old_initial_indent = self.initial_indent 28 | old_subsequent_indent = self.subsequent_indent 29 | self.initial_indent += indent 30 | self.subsequent_indent += indent 31 | 32 | try: 33 | yield 34 | finally: 35 | self.initial_indent = old_initial_indent 36 | self.subsequent_indent = old_subsequent_indent 37 | 38 | def indent_only(self, text: str) -> str: 39 | rv = [] 40 | 41 | for idx, line in enumerate(text.splitlines()): 42 | indent = self.initial_indent 43 | 44 | if idx > 0: 45 | indent = self.subsequent_indent 46 | 47 | rv.append(f"{indent}{line}") 48 | 49 | return "\n".join(rv) 50 | -------------------------------------------------------------------------------- /src/asyncclick/globals.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | from threading import local 3 | 4 | if t.TYPE_CHECKING: 5 | import typing_extensions as te 6 | 7 | from .core import Context 8 | 9 | _local = local() 10 | 11 | 12 | @t.overload 13 | def get_current_context(silent: "te.Literal[False]" = False) -> "Context": ... 14 | 15 | 16 | @t.overload 17 | def get_current_context(silent: bool = ...) -> t.Optional["Context"]: ... 18 | 19 | 20 | def get_current_context(silent: bool = False) -> t.Optional["Context"]: 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: t.Optional[bool] = None) -> t.Optional[bool]: 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/asyncclick/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-trio/asyncclick/385b38be17f7629798887619458cc9d1cbc13518/src/asyncclick/py.typed -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import anyio 3 | from functools import partial 4 | from threading import Thread 5 | 6 | from asyncclick.testing import CliRunner 7 | 8 | class SyncCliRunner(CliRunner): 9 | def invoke(self,*a,_sync=False,**k): 10 | fn = super().invoke 11 | if _sync: 12 | return fn(*a,**k) 13 | 14 | # anyio now protects against nested calls, so we use a thread 15 | result = None 16 | def f(): 17 | nonlocal result,fn 18 | async def r(): 19 | return await fn(*a,**k) 20 | result = anyio.run(r) ## , backend="trio") 21 | t=Thread(target=f, name="TEST") 22 | t.start() 23 | t.join() 24 | return result 25 | 26 | @pytest.fixture(scope="function") 27 | def runner(request): 28 | return SyncCliRunner() 29 | -------------------------------------------------------------------------------- /tests/test_chain.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | import asyncclick as click 6 | 7 | 8 | def debug(): 9 | click.echo( 10 | f"{sys._getframe(1).f_code.co_name}" 11 | f"={'|'.join(click.get_current_context().args)}" 12 | ) 13 | 14 | 15 | def test_basic_chaining(runner): 16 | @click.group(chain=True) 17 | def cli(): 18 | pass 19 | 20 | @cli.command("sdist") 21 | def sdist(): 22 | click.echo("sdist called") 23 | 24 | @cli.command("bdist") 25 | def bdist(): 26 | click.echo("bdist called") 27 | 28 | result = runner.invoke(cli, ["bdist", "sdist", "bdist"]) 29 | assert not result.exception 30 | assert result.output.splitlines() == [ 31 | "bdist called", 32 | "sdist called", 33 | "bdist called", 34 | ] 35 | 36 | 37 | @pytest.mark.parametrize( 38 | ("args", "expect"), 39 | [ 40 | (["--help"], "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."), 41 | (["--help"], "ROOT HELP"), 42 | (["sdist", "--help"], "SDIST HELP"), 43 | (["bdist", "--help"], "BDIST HELP"), 44 | (["bdist", "sdist", "--help"], "SDIST HELP"), 45 | ], 46 | ) 47 | def test_chaining_help(runner, args, expect): 48 | @click.group(chain=True) 49 | def cli(): 50 | """ROOT HELP""" 51 | pass 52 | 53 | @cli.command("sdist") 54 | def sdist(): 55 | """SDIST HELP""" 56 | click.echo("sdist called") 57 | 58 | @cli.command("bdist") 59 | def bdist(): 60 | """BDIST HELP""" 61 | click.echo("bdist called") 62 | 63 | result = runner.invoke(cli, args) 64 | assert not result.exception 65 | assert expect in result.output 66 | 67 | 68 | def test_chaining_with_options(runner): 69 | @click.group(chain=True) 70 | def cli(): 71 | pass 72 | 73 | @cli.command("sdist") 74 | @click.option("--format") 75 | def sdist(format): 76 | click.echo(f"sdist called {format}") 77 | 78 | @cli.command("bdist") 79 | @click.option("--format") 80 | def bdist(format): 81 | click.echo(f"bdist called {format}") 82 | 83 | result = runner.invoke(cli, ["bdist", "--format=1", "sdist", "--format=2"]) 84 | assert not result.exception 85 | assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] 86 | 87 | 88 | @pytest.mark.parametrize(("chain", "expect"), [(False, "1"), (True, "[]")]) 89 | def test_no_command_result_callback(runner, chain, expect): 90 | """When a group has ``invoke_without_command=True``, the result 91 | callback is always invoked. A regular group invokes it with 92 | its return value, a chained group with ``[]``. 93 | """ 94 | 95 | @click.group(invoke_without_command=True, chain=chain) 96 | def cli(): 97 | return 1 98 | 99 | @cli.result_callback() 100 | def process_result(result): 101 | click.echo(result, nl=False) 102 | 103 | result = runner.invoke(cli, []) 104 | assert result.output == expect 105 | 106 | 107 | def test_chaining_with_arguments(runner): 108 | @click.group(chain=True) 109 | def cli(): 110 | pass 111 | 112 | @cli.command("sdist") 113 | @click.argument("format") 114 | def sdist(format): 115 | click.echo(f"sdist called {format}") 116 | 117 | @cli.command("bdist") 118 | @click.argument("format") 119 | def bdist(format): 120 | click.echo(f"bdist called {format}") 121 | 122 | result = runner.invoke(cli, ["bdist", "1", "sdist", "2"]) 123 | assert not result.exception 124 | assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] 125 | 126 | 127 | @pytest.mark.parametrize( 128 | ("args", "input", "expect"), 129 | [ 130 | (["-f", "-"], "foo\nbar", ["foo", "bar"]), 131 | (["-f", "-", "strip"], "foo \n bar", ["foo", "bar"]), 132 | (["-f", "-", "strip", "uppercase"], "foo \n bar", ["FOO", "BAR"]), 133 | ], 134 | ) 135 | def test_pipeline(runner, args, input, expect): 136 | @click.group(chain=True, invoke_without_command=True) 137 | @click.option("-f", type=click.File("r")) 138 | def cli(f): 139 | pass 140 | 141 | @cli.result_callback() 142 | def process_pipeline(processors, f): 143 | iterator = (x.rstrip("\r\n") for x in f) 144 | for processor in processors: 145 | iterator = processor(iterator) 146 | for item in iterator: 147 | click.echo(item) 148 | 149 | @cli.command("uppercase") 150 | def make_uppercase(): 151 | def processor(iterator): 152 | for line in iterator: 153 | yield line.upper() 154 | 155 | return processor 156 | 157 | @cli.command("strip") 158 | def make_strip(): 159 | def processor(iterator): 160 | for line in iterator: 161 | yield line.strip() 162 | 163 | return processor 164 | 165 | result = runner.invoke(cli, args, input=input) 166 | assert not result.exception 167 | assert result.output.splitlines() == expect 168 | 169 | 170 | def test_args_and_chain(runner): 171 | @click.group(chain=True) 172 | def cli(): 173 | debug() 174 | 175 | @cli.command() 176 | def a(): 177 | debug() 178 | 179 | @cli.command() 180 | def b(): 181 | debug() 182 | 183 | @cli.command() 184 | def c(): 185 | debug() 186 | 187 | result = runner.invoke(cli, ["a", "b", "c"]) 188 | assert not result.exception 189 | assert result.output.splitlines() == ["cli=", "a=", "b=", "c="] 190 | 191 | 192 | def test_multicommand_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_multicommand_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 asyncclick as click 4 | 5 | 6 | def test_command_no_parens(runner): 7 | @click.command 8 | def cli(): 9 | click.echo("hello") 10 | 11 | result = runner.invoke(cli) 12 | assert result.exception is None 13 | assert result.output == "hello\n" 14 | 15 | 16 | def test_custom_command_no_parens(runner): 17 | class CustomCommand(click.Command): 18 | pass 19 | 20 | class CustomGroup(click.Group): 21 | command_class = CustomCommand 22 | 23 | @click.group(cls=CustomGroup) 24 | def grp(): 25 | pass 26 | 27 | @grp.command 28 | def cli(): 29 | click.echo("hello custom command class") 30 | 31 | result = runner.invoke(cli) 32 | assert result.exception is None 33 | assert result.output == "hello custom command class\n" 34 | 35 | 36 | def test_group_no_parens(runner): 37 | @click.group 38 | def grp(): 39 | click.echo("grp1") 40 | 41 | @grp.command 42 | def cmd1(): 43 | click.echo("cmd1") 44 | 45 | @grp.group 46 | def grp2(): 47 | click.echo("grp2") 48 | 49 | @grp2.command 50 | def cmd2(): 51 | click.echo("cmd2") 52 | 53 | result = runner.invoke(grp, ["cmd1"]) 54 | assert result.exception is None 55 | assert result.output == "grp1\ncmd1\n" 56 | 57 | result = runner.invoke(grp, ["grp2", "cmd2"]) 58 | assert result.exception is None 59 | assert result.output == "grp1\ngrp2\ncmd2\n" 60 | 61 | 62 | def test_params_argument(runner): 63 | opt = click.Argument(["a"]) 64 | 65 | @click.command(params=[opt]) 66 | @click.argument("b") 67 | def cli(a, b): 68 | click.echo(f"{a} {b}") 69 | 70 | assert cli.params[0].name == "a" 71 | assert cli.params[1].name == "b" 72 | result = runner.invoke(cli, ["1", "2"]) 73 | assert result.output == "1 2\n" 74 | -------------------------------------------------------------------------------- /tests/test_compat.py: -------------------------------------------------------------------------------- 1 | from asyncclick._compat import should_strip_ansi 2 | 3 | 4 | def test_is_jupyter_kernel_output(): 5 | class JupyterKernelFakeStream: 6 | pass 7 | 8 | # implementation detail, aka cheapskate test 9 | JupyterKernelFakeStream.__module__ = "ipykernel.faked" 10 | assert not should_strip_ansi(stream=JupyterKernelFakeStream()) 11 | -------------------------------------------------------------------------------- /tests/test_custom_classes.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | import pytest 4 | 5 | 6 | @pytest.mark.anyio 7 | async def test_command_context_class(): 8 | """A command with a custom ``context_class`` should produce a 9 | context using that type. 10 | """ 11 | 12 | class CustomContext(click.Context): 13 | pass 14 | 15 | class CustomCommand(click.Command): 16 | context_class = CustomContext 17 | 18 | command = CustomCommand("test") 19 | context = await command.make_context("test", []) 20 | assert isinstance(context, CustomContext) 21 | 22 | 23 | def test_context_invoke_type(runner): 24 | """A command invoked from a custom context should have a new 25 | context with the same type. 26 | """ 27 | 28 | class CustomContext(click.Context): 29 | pass 30 | 31 | class CustomCommand(click.Command): 32 | context_class = CustomContext 33 | 34 | @click.command() 35 | @click.argument("first_id", type=int) 36 | @click.pass_context 37 | def second(ctx, first_id): 38 | assert isinstance(ctx, CustomContext) 39 | assert id(ctx) != first_id 40 | 41 | @click.command(cls=CustomCommand) 42 | @click.pass_context 43 | async def first(ctx): 44 | assert isinstance(ctx, CustomContext) 45 | await ctx.invoke(second, first_id=id(ctx)) 46 | 47 | assert not runner.invoke(first).exception 48 | 49 | 50 | def test_context_formatter_class(): 51 | """A context with a custom ``formatter_class`` should format help 52 | using that type. 53 | """ 54 | 55 | class CustomFormatter(click.HelpFormatter): 56 | def write_heading(self, heading): 57 | heading = click.style(heading, fg="yellow") 58 | return super().write_heading(heading) 59 | 60 | class CustomContext(click.Context): 61 | formatter_class = CustomFormatter 62 | 63 | context = CustomContext( 64 | click.Command("test", params=[click.Option(["--value"])]), color=True 65 | ) 66 | assert "\x1b[33mOptions\x1b[0m:" in context.get_help() 67 | 68 | 69 | def test_group_command_class(runner): 70 | """A group with a custom ``command_class`` should create subcommands 71 | of that type by default. 72 | """ 73 | 74 | class CustomCommand(click.Command): 75 | pass 76 | 77 | class CustomGroup(click.Group): 78 | command_class = CustomCommand 79 | 80 | group = CustomGroup() 81 | subcommand = group.command()(lambda: None) 82 | assert type(subcommand) is CustomCommand 83 | subcommand = group.command(cls=click.Command)(lambda: None) 84 | assert type(subcommand) is click.Command 85 | 86 | 87 | def test_group_group_class(runner): 88 | """A group with a custom ``group_class`` should create subgroups 89 | of that type by default. 90 | """ 91 | 92 | class CustomSubGroup(click.Group): 93 | pass 94 | 95 | class CustomGroup(click.Group): 96 | group_class = CustomSubGroup 97 | 98 | group = CustomGroup() 99 | subgroup = group.group()(lambda: None) 100 | assert type(subgroup) is CustomSubGroup 101 | subgroup = group.command(cls=click.Group)(lambda: None) 102 | assert type(subgroup) is click.Group 103 | 104 | 105 | def test_group_group_class_self(runner): 106 | """A group with ``group_class = type`` should create subgroups of 107 | the same type as itself. 108 | """ 109 | 110 | class CustomGroup(click.Group): 111 | group_class = type 112 | 113 | group = CustomGroup() 114 | subgroup = group.group()(lambda: None) 115 | assert type(subgroup) is CustomGroup 116 | -------------------------------------------------------------------------------- /tests/test_defaults.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | 4 | def test_basic_defaults(runner): 5 | @click.command() 6 | @click.option("--foo", default=42, type=click.FLOAT) 7 | def cli(foo): 8 | assert type(foo) is float # noqa E721 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 type(item) is float # noqa E721 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 asyncclick._compat import WIN 6 | 7 | IMPORT_TEST = b"""\ 8 | import builtins 9 | 10 | found_imports = set() 11 | real_import = builtins.__import__ 12 | import sys 13 | 14 | def tracking_import(module, locals=None, globals=None, fromlist=None, 15 | level=0): 16 | rv = real_import(module, locals, globals, fromlist, level) 17 | if globals and '__name__' in globals and globals['__name__'].startswith('asyncclick') and level == 0: 18 | found_imports.add(module) 19 | return rv 20 | builtins.__import__ = tracking_import 21 | 22 | import asyncclick 23 | rv = list(found_imports) 24 | import json 25 | asyncclick.echo(json.dumps(rv)) 26 | """ 27 | 28 | ALLOWED_IMPORTS = { 29 | "anyio", 30 | "weakref", 31 | "os", 32 | "struct", 33 | "collections", 34 | "sys", 35 | "contextlib", 36 | "functools", 37 | "stat", 38 | "re", 39 | "codecs", 40 | "inspect", 41 | "itertools", 42 | "io", 43 | "threading", 44 | "errno", 45 | "fcntl", 46 | "datetime", 47 | "enum", 48 | "typing", 49 | "types", 50 | "gettext", 51 | "shutil", 52 | } 53 | 54 | if WIN: 55 | ALLOWED_IMPORTS.update(["ctypes", "ctypes.wintypes", "msvcrt", "time"]) 56 | 57 | 58 | def test_light_imports(): 59 | c = subprocess.Popen( 60 | [sys.executable, "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE 61 | ) 62 | rv = c.communicate(IMPORT_TEST)[0] 63 | rv = rv.decode("utf-8") 64 | imported = json.loads(rv) 65 | 66 | for module in imported: 67 | if module == "asyncclick" or module.startswith("asyncclick."): 68 | continue 69 | assert module in ALLOWED_IMPORTS 70 | -------------------------------------------------------------------------------- /tests/test_normalization.py: -------------------------------------------------------------------------------- 1 | import asyncclick as click 2 | 3 | CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower()) 4 | 5 | 6 | def test_option_normalization(runner): 7 | @click.command(context_settings=CONTEXT_SETTINGS) 8 | @click.option("--foo") 9 | @click.option("-x") 10 | def cli(foo, x): 11 | click.echo(foo) 12 | click.echo(x) 13 | 14 | result = runner.invoke(cli, ["--FOO", "42", "-X", 23]) 15 | assert result.output == "42\n23\n" 16 | 17 | 18 | def test_choice_normalization(runner): 19 | @click.command(context_settings=CONTEXT_SETTINGS) 20 | @click.option("--choice", type=click.Choice(["Foo", "Bar"])) 21 | def cli(choice): 22 | click.echo(choice) 23 | 24 | result = runner.invoke(cli, ["--CHOICE", "FOO"]) 25 | assert result.output == "Foo\n" 26 | 27 | 28 | def test_command_normalization(runner): 29 | @click.group(context_settings=CONTEXT_SETTINGS) 30 | def cli(): 31 | pass 32 | 33 | @cli.command() 34 | def foo(): 35 | click.echo("here!") 36 | 37 | result = runner.invoke(cli, ["FOO"]) 38 | assert result.output == "here!\n" 39 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import asyncclick as click 4 | from asyncclick.parser import OptionParser 5 | from asyncclick.parser 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 asyncclick as click 9 | from asyncclick import FileError 10 | 11 | 12 | @pytest.mark.parametrize( 13 | ("type", "value", "expect"), 14 | [ 15 | (click.IntRange(0, 5), "3", 3), 16 | (click.IntRange(5), "5", 5), 17 | (click.IntRange(5), "100", 100), 18 | (click.IntRange(max=5), "5", 5), 19 | (click.IntRange(max=5), "-100", -100), 20 | (click.IntRange(0, clamp=True), "-1", 0), 21 | (click.IntRange(max=5, clamp=True), "6", 5), 22 | (click.IntRange(0, min_open=True, clamp=True), "0", 1), 23 | (click.IntRange(max=5, max_open=True, clamp=True), "5", 4), 24 | (click.FloatRange(0.5, 1.5), "1.2", 1.2), 25 | (click.FloatRange(0.5, min_open=True), "0.51", 0.51), 26 | (click.FloatRange(max=1.5, max_open=True), "1.49", 1.49), 27 | (click.FloatRange(0.5, clamp=True), "-0.0", 0.5), 28 | (click.FloatRange(max=1.5, clamp=True), "inf", 1.5), 29 | ], 30 | ) 31 | def test_range(type, value, expect): 32 | assert type.convert(value, None, None) == expect 33 | 34 | 35 | @pytest.mark.parametrize( 36 | ("type", "value", "expect"), 37 | [ 38 | (click.IntRange(0, 5), "6", "6 is not in the range 0<=x<=5."), 39 | (click.IntRange(5), "4", "4 is not in the range x>=5."), 40 | (click.IntRange(max=5), "6", "6 is not in the range x<=5."), 41 | (click.IntRange(0, 5, min_open=True), 0, "00.5"), 44 | (click.FloatRange(max=1.5, max_open=True), 1.5, "x<1.5"), 45 | ], 46 | ) 47 | def test_range_fail(type, value, expect): 48 | with pytest.raises(click.BadParameter) as exc_info: 49 | type.convert(value, None, None) 50 | 51 | assert expect in exc_info.value.message 52 | 53 | 54 | def test_float_range_no_clamp_open(): 55 | with pytest.raises(TypeError): 56 | click.FloatRange(0, 1, max_open=True, clamp=True) 57 | 58 | sneaky = click.FloatRange(0, 1, max_open=True) 59 | sneaky.clamp = True 60 | 61 | with pytest.raises(RuntimeError): 62 | sneaky.convert("1.5", None, None) 63 | 64 | 65 | @pytest.mark.parametrize( 66 | ("nargs", "multiple", "default", "expect"), 67 | [ 68 | (2, False, None, None), 69 | (2, False, (None, None), (None, None)), 70 | (None, True, None, ()), 71 | (None, True, (None, None), (None, None)), 72 | (2, True, None, ()), 73 | (2, True, [(None, None)], ((None, None),)), 74 | (-1, None, None, ()), 75 | ], 76 | ) 77 | def test_cast_multi_default(runner, nargs, multiple, default, expect): 78 | if nargs == -1: 79 | param = click.Argument(["a"], nargs=nargs, default=default) 80 | else: 81 | param = click.Option(["-a"], nargs=nargs, multiple=multiple, default=default) 82 | 83 | cli = click.Command("cli", params=[param], callback=lambda a: a) 84 | result = runner.invoke(cli, standalone_mode=False) 85 | assert result.exception is None 86 | assert result.return_value == expect 87 | 88 | 89 | @pytest.mark.parametrize( 90 | ("cls", "expect"), 91 | [ 92 | (None, "a/b/c.txt"), 93 | (str, "a/b/c.txt"), 94 | (bytes, b"a/b/c.txt"), 95 | (pathlib.Path, pathlib.Path("a", "b", "c.txt")), 96 | ], 97 | ) 98 | def test_path_type(runner, cls, expect): 99 | cli = click.Command( 100 | "cli", 101 | params=[click.Argument(["p"], type=click.Path(path_type=cls))], 102 | callback=lambda p: p, 103 | ) 104 | result = runner.invoke(cli, ["a/b/c.txt"], standalone_mode=False) 105 | assert result.exception is None 106 | assert result.return_value == expect 107 | 108 | 109 | def _symlinks_supported(): 110 | with tempfile.TemporaryDirectory(prefix="click-pytest-") as tempdir: 111 | target = os.path.join(tempdir, "target") 112 | open(target, "w").close() 113 | link = os.path.join(tempdir, "link") 114 | 115 | try: 116 | os.symlink(target, link) 117 | return True 118 | except OSError: 119 | return False 120 | 121 | 122 | @pytest.mark.skipif( 123 | not _symlinks_supported(), reason="The current OS or FS doesn't support symlinks." 124 | ) 125 | def test_path_resolve_symlink(tmp_path, runner): 126 | test_file = tmp_path / "file" 127 | test_file_str = os.fspath(test_file) 128 | test_file.write_text("") 129 | 130 | path_type = click.Path(resolve_path=True) 131 | param = click.Argument(["a"], type=path_type) 132 | ctx = click.Context(click.Command("cli", params=[param])) 133 | 134 | test_dir = tmp_path / "dir" 135 | test_dir.mkdir() 136 | 137 | abs_link = test_dir / "abs" 138 | abs_link.symlink_to(test_file) 139 | abs_rv = path_type.convert(os.fspath(abs_link), param, ctx) 140 | assert abs_rv == test_file_str 141 | 142 | rel_link = test_dir / "rel" 143 | rel_link.symlink_to(pathlib.Path("..") / "file") 144 | rel_rv = path_type.convert(os.fspath(rel_link), param, ctx) 145 | assert rel_rv == test_file_str 146 | 147 | 148 | def _non_utf8_filenames_supported(): 149 | with tempfile.TemporaryDirectory(prefix="click-pytest-") as tempdir: 150 | try: 151 | f = open(os.path.join(tempdir, "\udcff"), "w") 152 | except OSError: 153 | return False 154 | 155 | f.close() 156 | return True 157 | 158 | 159 | @pytest.mark.skipif( 160 | not _non_utf8_filenames_supported(), 161 | reason="The current OS or FS doesn't support non-UTF-8 filenames.", 162 | ) 163 | def test_path_surrogates(tmp_path, monkeypatch): 164 | monkeypatch.chdir(tmp_path) 165 | type = click.Path(exists=True) 166 | path = pathlib.Path("\udcff") 167 | 168 | with pytest.raises(click.BadParameter, match="'�' does not exist"): 169 | type.convert(path, None, None) 170 | 171 | type = click.Path(file_okay=False) 172 | path.touch() 173 | 174 | with pytest.raises(click.BadParameter, match="'�' is a file"): 175 | type.convert(path, None, None) 176 | 177 | path.unlink() 178 | type = click.Path(dir_okay=False) 179 | path.mkdir() 180 | 181 | with pytest.raises(click.BadParameter, match="'�' is a directory"): 182 | type.convert(path, None, None) 183 | 184 | path.rmdir() 185 | 186 | def no_access(*args, **kwargs): 187 | """Test environments may be running as root, so we have to fake the result of 188 | the access tests that use os.access 189 | """ 190 | p = args[0] 191 | assert p == path, f"unexpected os.access call on file not under test: {p!r}" 192 | return False 193 | 194 | path.touch() 195 | type = click.Path(readable=True) 196 | 197 | with pytest.raises(click.BadParameter, match="'�' is not readable"): 198 | with monkeypatch.context() as m: 199 | m.setattr(os, "access", no_access) 200 | type.convert(path, None, None) 201 | 202 | type = click.Path(readable=False, writable=True) 203 | 204 | with pytest.raises(click.BadParameter, match="'�' is not writable"): 205 | with monkeypatch.context() as m: 206 | m.setattr(os, "access", no_access) 207 | type.convert(path, None, None) 208 | 209 | type = click.Path(readable=False, executable=True) 210 | 211 | with pytest.raises(click.BadParameter, match="'�' is not executable"): 212 | with monkeypatch.context() as m: 213 | m.setattr(os, "access", no_access) 214 | type.convert(path, None, None) 215 | 216 | path.unlink() 217 | 218 | 219 | @pytest.mark.parametrize( 220 | "type", 221 | [ 222 | click.File(mode="r"), 223 | click.File(mode="r", lazy=True), 224 | ], 225 | ) 226 | def test_file_surrogates(type, tmp_path): 227 | path = tmp_path / "\udcff" 228 | 229 | with pytest.raises(click.BadParameter, match="�': No such file or directory"): 230 | type.convert(path, None, None) 231 | 232 | 233 | def test_file_error_surrogates(): 234 | message = FileError(filename="\udcff").format_message() 235 | assert message == "Could not open file '�': unknown error" 236 | 237 | 238 | @pytest.mark.skipif( 239 | platform.system() == "Windows", reason="Filepath syntax differences." 240 | ) 241 | def test_invalid_path_with_esc_sequence(): 242 | with pytest.raises(click.BadParameter) as exc_info: 243 | with tempfile.TemporaryDirectory(prefix="my\ndir") as tempdir: 244 | click.Path(dir_okay=False).convert(tempdir, None, None) 245 | 246 | assert "my\\ndir" in exc_info.value.message 247 | -------------------------------------------------------------------------------- /tests/typing/typing_aliased_group.py: -------------------------------------------------------------------------------- 1 | """Example from https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases""" 2 | 3 | from __future__ import annotations 4 | 5 | from typing_extensions import assert_type 6 | 7 | import asyncclick as click 8 | 9 | 10 | class AliasedGroup(click.Group): 11 | def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None: 12 | rv = click.Group.get_command(self, ctx, cmd_name) 13 | if rv is not None: 14 | return rv 15 | matches = [x for x in self.list_commands(ctx) if x.startswith(cmd_name)] 16 | if not matches: 17 | return None 18 | elif len(matches) == 1: 19 | return click.Group.get_command(self, ctx, matches[0]) 20 | ctx.fail(f"Too many matches: {', '.join(sorted(matches))}") 21 | 22 | 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/8.1.x/options/#yes-parameters""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.confirmation_option(prompt="Are you sure you want to drop the db?") 10 | def dropdb() -> None: 11 | click.echo("Dropped all tables!") 12 | 13 | 14 | assert_type(dropdb, click.Command) 15 | -------------------------------------------------------------------------------- /tests/typing/typing_group_kw_options.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import assert_type 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.group(context_settings={}) 7 | def hello() -> None: 8 | pass 9 | 10 | 11 | assert_type(hello, click.Group) 12 | -------------------------------------------------------------------------------- /tests/typing/typing_help_option.py: -------------------------------------------------------------------------------- 1 | from typing_extensions import assert_type 2 | 3 | import asyncclick as click 4 | 5 | 6 | @click.command() 7 | @click.help_option("-h", "--help") 8 | def hello() -> None: 9 | """Simple program that greets NAME for a total of COUNT times.""" 10 | click.echo("Hello!") 11 | 12 | 13 | assert_type(hello, click.Command) 14 | -------------------------------------------------------------------------------- /tests/typing/typing_options.py: -------------------------------------------------------------------------------- 1 | """From https://click.palletsprojects.com/en/8.1.x/quickstart/#adding-parameters""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.option("--count", default=1, help="number of greetings") 10 | @click.argument("name") 11 | def hello(count: int, name: str) -> None: 12 | for _ in range(count): 13 | click.echo(f"Hello {name}!") 14 | 15 | 16 | assert_type(hello, click.Command) 17 | -------------------------------------------------------------------------------- /tests/typing/typing_password_option.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.password_option() 10 | def encrypt(password: str) -> None: 11 | click.echo(f"encoded: to {codecs.encode(password, 'rot13')}") 12 | 13 | 14 | assert_type(encrypt, click.Command) 15 | -------------------------------------------------------------------------------- /tests/typing/typing_simple_example.py: -------------------------------------------------------------------------------- 1 | """The simple example from https://github.com/pallets/click#a-simple-example.""" 2 | 3 | from typing_extensions import assert_type 4 | 5 | import asyncclick as click 6 | 7 | 8 | @click.command() 9 | @click.option("--count", default=1, help="Number of greetings.") 10 | @click.option("--name", prompt="Your name", help="The person to greet.") 11 | def hello(count: int, name: str) -> None: 12 | """Simple program that greets NAME for a total of COUNT times.""" 13 | for _ in range(count): 14 | click.echo(f"Hello, {name}!") 15 | 16 | 17 | assert_type(hello, click.Command) 18 | -------------------------------------------------------------------------------- /tests/typing/typing_version_option.py: -------------------------------------------------------------------------------- 1 | """ 2 | From https://click.palletsprojects.com/en/8.1.x/options/#callbacks-and-eager-options. 3 | """ 4 | 5 | from typing_extensions import assert_type 6 | 7 | import asyncclick as click 8 | 9 | 10 | @click.command() 11 | @click.version_option("0.1") 12 | def hello() -> None: 13 | click.echo("Hello World!") 14 | 15 | 16 | assert_type(hello, click.Command) 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{13,12,11,10,9} 4 | pypy310 5 | style 6 | typing 7 | docs 8 | skip_missing_interpreters = true 9 | 10 | [testenv] 11 | package = wheel 12 | wheel_build_env = .pkg 13 | constrain_package_deps = true 14 | use_frozen_constraints = true 15 | deps = -r requirements/tests.txt 16 | commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} 17 | 18 | [testenv:py37,py3.7] 19 | deps = -r requirements/tests37.txt 20 | 21 | [testenv:style] 22 | deps = pre-commit 23 | skip_install = true 24 | commands = pre-commit run --all-files 25 | 26 | [testenv:typing] 27 | deps = -r requirements/typing.txt 28 | commands = 29 | mypy 30 | pyright tests/typing 31 | pyright --verifytypes asyncclick --ignoreexternal 32 | 33 | [testenv:docs] 34 | deps = -r requirements/docs.txt 35 | commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml 36 | 37 | [testenv:update-actions] 38 | labels = update 39 | deps = gha-update 40 | commands = gha-update 41 | 42 | [testenv:update-pre_commit] 43 | labels = update 44 | deps = pre-commit 45 | skip_install = true 46 | commands = pre-commit autoupdate -j4 47 | 48 | [testenv:update-requirements] 49 | labels = update 50 | deps = pip-tools 51 | skip_install = true 52 | change_dir = requirements 53 | commands = 54 | pip-compile build.in -q {posargs:-U} 55 | pip-compile docs.in -q {posargs:-U} 56 | pip-compile tests.in -q {posargs:-U} 57 | pip-compile typing.in -q {posargs:-U} 58 | pip-compile dev.in -q {posargs:-U} 59 | 60 | [testenv:update-requirements37] 61 | base_python = 3.7 62 | labels = update 63 | deps = pip-tools 64 | skip_install = true 65 | change_dir = requirements 66 | commands = pip-compile tests.in -q -o tests37.txt {posargs:-U} 67 | --------------------------------------------------------------------------------