From 078c9fb4c73a874881ab23dd1d134c5470174263 Mon Sep 17 00:00:00 2001 From: "deepin-community-bot[bot]" <156989552+deepin-community-bot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:57:31 +0000 Subject: [PATCH] feat: update python-click to 8.2.0+0.really.8.1.8-1 --- .devcontainer/devcontainer.json | 17 ++ .devcontainer/on-create-command.sh | 7 + .editorconfig | 2 +- .gitignore | 24 +- .pre-commit-config.yaml | 31 +-- .readthedocs.yaml | 4 +- CHANGES.rst | 41 +++ CODE_OF_CONDUCT.md | 76 ------ CONTRIBUTING.rst | 2 +- LICENSE.rst => LICENSE.txt | 0 MANIFEST.in | 10 - README.md | 52 ++++ README.rst | 78 ------ debian/changelog | 67 +++++ debian/control | 56 ++-- debian/copyright | 1 + debian/gbp.conf | 4 + debian/patches/build-uninstalled-docs.patch | 33 +++ ...e-local-Python-intersphinx-inventary.patch | 16 +- debian/patches/series | 1 + debian/rules | 14 +- debian/source/lintian-overrides | 10 + debian/tests/control | 12 +- debian/tests/unittests | 4 +- docs/advanced.rst | 1 + docs/arguments.rst | 239 +++++------------- docs/conf.py | 21 +- docs/contrib.rst | 37 +++ docs/documentation.rst | 2 +- docs/handling-files.rst | 90 +++++++ docs/index.rst | 2 + docs/license.rst | 3 +- docs/parameters.rst | 114 ++++----- docs/quickstart.rst | 102 ++------ docs/setuptools.rst | 10 +- docs/shell-completion.rst | 26 +- docs/virtualenv.rst | 54 ++++ pyproject.toml | 103 ++++++++ requirements/build.txt | 15 +- requirements/dev.txt | 146 ++++++++--- requirements/docs.txt | 62 ++--- requirements/tests.txt | 15 +- requirements/tests37.txt | 26 ++ requirements/typing.txt | 26 +- setup.cfg | 104 -------- setup.py | 9 - src/click/__init__.py | 4 +- src/click/_termui_impl.py | 113 ++++++--- src/click/core.py | 131 +++++----- src/click/decorators.py | 81 +++--- src/click/exceptions.py | 14 +- src/click/globals.py | 7 +- src/click/parser.py | 4 +- src/click/shell_completion.py | 58 +++-- src/click/termui.py | 2 +- src/click/testing.py | 6 +- src/click/types.py | 16 +- src/click/utils.py | 10 +- tests/test_commands.py | 132 ++++++++++ tests/test_defaults.py | 29 ++- tests/test_imports.py | 2 +- tests/test_normalization.py | 1 - tests/test_options.py | 8 + tests/test_testing.py | 25 +- tests/test_types.py | 12 + tests/test_utils.py | 23 +- tests/typing/typing_aliased_group.py | 1 + tests/typing/typing_confirmation_option.py | 1 + tests/typing/typing_options.py | 1 + tests/typing/typing_simple_example.py | 1 + tests/typing/typing_version_option.py | 1 + tox.ini | 42 ++- 72 files changed, 1493 insertions(+), 1001 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100755 .devcontainer/on-create-command.sh delete mode 100644 CODE_OF_CONDUCT.md rename LICENSE.rst => LICENSE.txt (100%) delete mode 100644 MANIFEST.in create mode 100644 README.md delete mode 100644 README.rst create mode 100644 debian/gbp.conf create mode 100644 debian/patches/build-uninstalled-docs.patch create mode 100644 debian/source/lintian-overrides create mode 100644 docs/handling-files.rst create mode 100644 docs/virtualenv.rst create mode 100644 pyproject.toml create mode 100644 requirements/tests37.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..677d72a --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +{ + "name": "pallets/click", + "image": "mcr.microsoft.com/devcontainers/python:3", + "customizations": { + "vscode": { + "settings": { + "python.defaultInterpreterPath": "${workspaceFolder}/.venv", + "python.terminal.activateEnvInCurrentTerminal": true, + "python.terminal.launchArgs": [ + "-X", + "dev" + ] + } + } + }, + "onCreateCommand": ".devcontainer/on-create-command.sh" +} diff --git a/.devcontainer/on-create-command.sh b/.devcontainer/on-create-command.sh new file mode 100755 index 0000000..eaebea6 --- /dev/null +++ b/.devcontainer/on-create-command.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +python3 -m venv --upgrade-deps .venv +. .venv/bin/activate +pip install -r requirements/dev.txt +pip install -e . +pre-commit install --install-hooks diff --git a/.editorconfig b/.editorconfig index e32c802..2ff985a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,5 +9,5 @@ end_of_line = lf charset = utf-8 max_line_length = 88 -[*.{yml,yaml,json,js,css,html}] +[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}] indent_size = 2 diff --git a/.gitignore b/.gitignore index c1a1723..62c1b88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,10 @@ -/.idea/ -/.vscode/ -.DS_Store/ -/env/ -/venv/ +.idea/ +.vscode/ +.venv*/ +venv*/ __pycache__/ -*.pyc -*.egg-info/ -/build/ -/dist/ -/.pytest_cache/ -/.tox/ -.coverage -.coverage.* -/htmlcov/ -/docs/_build/ +dist/ +.coverage* +htmlcov/ +.tox/ +docs/_build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 277a0a5..4cbbc0e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,31 +1,14 @@ -ci: - autoupdate_branch: "8.1.x" - autoupdate_schedule: monthly repos: - - repo: https://github.com/asottile/pyupgrade - rev: v3.8.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.1 hooks: - - id: pyupgrade - args: ["--py37-plus"] - - repo: https://github.com/asottile/reorder-python-imports - rev: v3.10.0 - hooks: - - id: reorder-python-imports - args: ["--application-directories", "src"] - - repo: https://github.com/psf/black - rev: 23.3.0 - hooks: - - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - additional_dependencies: - - flake8-bugbear - - flake8-implicit-str-concat + - id: ruff + - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v5.0.0 hooks: + - id: check-merge-conflict + - id: debug-statements - id: fix-byte-order-marker - id: trailing-whitespace - id: end-of-file-fixer diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 346900b..865c685 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,8 +1,8 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.10" + python: '3.12' python: install: - requirements: requirements/docs.txt diff --git a/CHANGES.rst b/CHANGES.rst index 77dceb5..2b7a672 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,46 @@ .. currentmodule:: click +Version 8.1.8 +------------- + +Released 2024-12-19 + +- Fix an issue with type hints for ``click.open_file()``. :issue:`2717` +- Fix issue where error message for invalid ``click.Path`` displays on + multiple lines. :issue:`2697` +- Fixed issue that prevented a default value of ``""`` from being displayed in + the help for an option. :issue:`2500` +- The test runner handles stripping color consistently on Windows. + :issue:`2705` +- Show correct value for flag default when using ``default_map``. + :issue:`2632` +- Fix ``click.echo(color=...)`` passing ``color`` to coloroma so it can be + forced on Windows. :issue:`2606`. +- More robust bash version check, fixing problem on Windows with git-bash. + :issue:`2638` +- Cache the help option generated by the ``help_option_names`` setting to + respect its eagerness. :pr:`2811` +- Replace uses of ``os.system`` with ``subprocess.Popen``. :issue:`1476` +- Exceptions generated during a command will use the context's ``color`` + setting when being displayed. :issue:`2193` +- Error message when defining option with invalid name is more descriptive. + :issue:`2452` +- Refactor code generating default ``--help`` option to deduplicate code. + :pr:`2563` +- Test ``CLIRunner`` resets patched ``_compat.should_strip_ansi``. + :issue:`2732` + + +Version 8.1.7 +------------- + +Released 2023-08-17 + +- Fix issue with regex flags in shell completion. :issue:`2581` +- Bash version detection issues a warning instead of an error. :issue:`2574` +- Fix issue with completion script for Fish shell. :issue:`2567` + + Version 8.1.6 ------------- diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index f4ba197..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at report@palletsprojects.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 1ad3bdc..d67a153 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -144,7 +144,7 @@ Start coding .. code-block:: text $ git fetch origin - $ git checkout -b your-branch-name origin/8.0.x + $ git checkout -b your-branch-name origin/8.1.x If you're submitting a feature addition or change, branch off of the "main" branch. diff --git a/LICENSE.rst b/LICENSE.txt similarity index 100% rename from LICENSE.rst rename to LICENSE.txt diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index e5b231d..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,10 +0,0 @@ -include CHANGES.rst -include tox.ini -include requirements/*.txt -graft artwork -graft docs -prune docs/_build -graft examples -graft tests -include src/click/py.typed -global-exclude *.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..1aa055d --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# $ click_ + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +## A Simple Example + +```python +import click + +@click.command() +@click.option("--count", default=1, help="Number of greetings.") +@click.option("--name", prompt="Your name", help="The person to greet.") +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + +if __name__ == '__main__': + hello() +``` + +``` +$ python hello.py --count=3 +Your name: Click +Hello, Click! +Hello, Click! +Hello, Click! +``` + + +## Donate + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate diff --git a/README.rst b/README.rst deleted file mode 100644 index 76f2697..0000000 --- a/README.rst +++ /dev/null @@ -1,78 +0,0 @@ -\$ click\_ -========== - -Click is a Python package for creating beautiful command line interfaces -in a composable way with as little code as necessary. It's the "Command -Line Interface Creation Kit". It's highly configurable but comes with -sensible defaults out of the box. - -It aims to make the process of writing command line tools quick and fun -while also preventing any frustration caused by the inability to -implement an intended CLI API. - -Click in three points: - -- Arbitrary nesting of commands -- Automatic help page generation -- Supports lazy loading of subcommands at runtime - - -Installing ----------- - -Install and update using `pip`_: - -.. code-block:: text - - $ pip install -U click - -.. _pip: https://pip.pypa.io/en/stable/getting-started/ - - -A Simple Example ----------------- - -.. code-block:: python - - import click - - @click.command() - @click.option("--count", default=1, help="Number of greetings.") - @click.option("--name", prompt="Your name", help="The person to greet.") - def hello(count, name): - """Simple program that greets NAME for a total of COUNT times.""" - for _ in range(count): - click.echo(f"Hello, {name}!") - - if __name__ == '__main__': - hello() - -.. code-block:: text - - $ python hello.py --count=3 - Your name: Click - Hello, Click! - Hello, Click! - Hello, Click! - - -Donate ------- - -The Pallets organization develops and supports Click and other popular -packages. In order to grow the community of contributors and users, and -allow the maintainers to devote more time to the projects, `please -donate today`_. - -.. _please donate today: https://palletsprojects.com/donate - - -Links ------ - -- Documentation: https://click.palletsprojects.com/ -- Changes: https://click.palletsprojects.com/changes/ -- PyPI Releases: https://pypi.org/project/click/ -- Source Code: https://github.com/pallets/click -- Issue Tracker: https://github.com/pallets/click/issues -- Chat: https://discord.gg/pallets diff --git a/debian/changelog b/debian/changelog index d186d22..9925576 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,70 @@ +python-click (8.2.0+0.really.8.1.8-1) unstable; urgency=medium + + * Revert to version 8.1.8; the not-completely-released 8.2.0 + version introduces incompatibilities that affect + too many other packages. Closes: #1098507 + * Declare compliance with Policy 4.7.2 with no changes. + + -- Peter Pentchev Fri, 28 Feb 2025 16:47:49 +0200 + +python-click (8.2.0-1) unstable; urgency=medium + + * Apply debputy's X-Style: black. + * Add the year 2025 to my debian/* copyright notice. + * New upstream release. + + -- Peter Pentchev Mon, 17 Feb 2025 11:53:09 +0200 + +python-click (8.1.8-2) unstable; urgency=medium + + * Team upload. + * Remove unnecessare python3-docutils build dep + * Trim autopkgtest build deps & avoid @builddeps@ + + -- Alexandre Detiste Sun, 16 Feb 2025 20:49:19 +0100 + +python-click (8.1.8-1) unstable; urgency=medium + + * Team upload. + + [ Alexandre Detiste ] + * Fix syntax error in Maintainer field. + + [ Colin Watson ] + * New upstream release. + * Drop unnecessary python3-colorama (build-)dependency, since it's only + used on Windows. + + -- Colin Watson Sun, 29 Dec 2024 01:00:26 +0000 + +python-click (8.1.7-2) unstable; urgency=medium + + * New maintainer. Closes: #1065251 + * Add my notice to the copyright file. + * Bring the package short description up to date. + * Declare compliance with Policy 4.7.0 with no changes. + * Switch to debhelper compat level 14. + * Use dpkg's default.mk for completeness. + * Declare dpkg-build-api v1, drop the implied Rules-Requires-Root: no. + * Let debhelper take care of some dependencies automatically. + * Simplify the rules file using `execute_before/after_*` targets. + * Use dh-sequence-sphinxdoc to fix the nodoc build. + * Add a trivial git-buildpackage config file. + + -- Peter Pentchev Fri, 26 Jul 2024 17:14:54 +0300 + +python-click (8.1.7-1) unstable; urgency=medium + + * New upstream release. + + -- Colin Watson Sat, 16 Mar 2024 00:38:13 +0000 + +python-click (8.1.6-2) unstable; urgency=medium + + * orphan + + -- Sandro Tosi Sat, 02 Mar 2024 04:06:06 -0500 + python-click (8.1.6-1) unstable; urgency=medium * Team upload diff --git a/debian/control b/debian/control index 0dd9330..c9b0a56 100644 --- a/debian/control +++ b/debian/control @@ -1,36 +1,40 @@ Source: python-click Section: python Priority: optional -Maintainer: Sandro Tosi -Uploaders: Debian Python Team , -Build-Depends: debhelper-compat (= 13), - dh-sequence-python3, - python3-all, - python3-doc , - python3-colorama, - python3-docutils, - python3-pallets-sphinx-themes (>= 2.0.1) , - python3-pytest , - python3-requests, - python3-setuptools, - python3-sphinx , - python3-sphinx-issues , - python3-sphinx-tabs , - python3-sphinxcontrib-log-cabinet , -Standards-Version: 4.6.2.0 +Maintainer: Debian Python Team +Uploaders: + Peter Pentchev , +Build-Depends: + debhelper (>> 13.15.2~), + dh-sequence-python3, + dh-sequence-sphinxdoc , + dpkg-build-api (= 1), + flit, + pybuild-plugin-pyproject, + python3-all, + python3-doc , + python3-pallets-sphinx-themes (>= 2.0.1) , + python3-pytest , + python3-requests, + python3-sphinx , + python3-sphinx-issues , + python3-sphinx-tabs , + python3-sphinxcontrib-log-cabinet , +X-DH-Compat: 14 +Standards-Version: 4.7.2 Vcs-Browser: https://salsa.debian.org/python-team/packages/python-click Vcs-Git: https://salsa.debian.org/python-team/packages/python-click.git Homepage: https://github.com/pallets/click -Rules-Requires-Root: no Testsuite: autopkgtest-pkg-python +X-Style: black Package: python3-click Architecture: all -Depends: python3-colorama, - ${misc:Depends}, - ${python3:Depends}, -Breaks: python3-click-threading (<< 0.5.0), -Description: Wrapper around optparse for command line utilities - Python 3.x +Depends: + ${python3:Depends}, +Breaks: + python3-click-threading (<< 0.5.0), +Description: Command-Line Interface Creation Kit - Python 3.x Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the "Command Line Interface Creation Kit". It's highly configurable but comes with @@ -45,10 +49,10 @@ Description: Wrapper around optparse for command line utilities - Python 3.x Package: python-click-doc Section: doc Architecture: all -Depends: ${misc:Depends}, - ${sphinxdoc:Depends}, +Depends: + ${sphinxdoc:Depends}, Multi-Arch: foreign -Description: Wrapper around optparse for command line utilities - documentation +Description: Command-Line Interface Creation Kit - documentation Click is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It's the "Command Line Interface Creation Kit". It's highly configurable but comes with diff --git a/debian/copyright b/debian/copyright index 34531c5..b473e6e 100644 --- a/debian/copyright +++ b/debian/copyright @@ -16,6 +16,7 @@ License: BSD-3-clause Files: debian/* Copyright: 2014 Alexandre Viau Copyright (C) 2019-2023 Sandro Tosi + Copyright (C) 2024, 2025 Peter Pentchev License: BSD-3-clause License: BSD-3-clause diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 0000000..82871c2 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,4 @@ +[DEFAULT] +pristine-tar = True +sign-tags = True +debian-branch = debian/master diff --git a/debian/patches/build-uninstalled-docs.patch b/debian/patches/build-uninstalled-docs.patch new file mode 100644 index 0000000..82db70d --- /dev/null +++ b/debian/patches/build-uninstalled-docs.patch @@ -0,0 +1,33 @@ +From: Colin Watson +Date: Sun, 29 Dec 2024 00:47:01 +0000 +Subject: Handle building docs without installation + +Forwarded: not-needed +Last-Update: 2024-12-29 +--- + docs/conf.py | 8 +++++++- + 1 file changed, 7 insertions(+), 1 deletion(-) + +diff --git a/docs/conf.py b/docs/conf.py +index 47faeb1..363c503 100644 +--- a/docs/conf.py ++++ b/docs/conf.py +@@ -1,3 +1,5 @@ ++import os ++ + from pallets_sphinx_themes import get_version + from pallets_sphinx_themes import ProjectLink + +@@ -6,7 +8,11 @@ from pallets_sphinx_themes import ProjectLink + project = "Click" + copyright = "2014 Pallets" + author = "Pallets" +-release, version = get_version("Click") ++if "DEB_VERSION_UPSTREAM" in os.environ: ++ version = os.environ["DEB_VERSION_UPSTREAM"] ++ release = f"{version.split('.', 2)[:2]}.x" ++else: ++ release, version = get_version("Click") + + # General -------------------------------------------------------------- + diff --git a/debian/patches/docs-Use-local-Python-intersphinx-inventary.patch b/debian/patches/docs-Use-local-Python-intersphinx-inventary.patch index cf0c490..c10c3ef 100644 --- a/debian/patches/docs-Use-local-Python-intersphinx-inventary.patch +++ b/debian/patches/docs-Use-local-Python-intersphinx-inventary.patch @@ -8,15 +8,15 @@ Forwarded: Not-Needed 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py -index b0ed9ff..f4eab7a 100644 +index db253ea..47faeb1 100644 --- a/docs/conf.py +++ b/docs/conf.py -@@ -25,7 +25,7 @@ extensions = [ - "sphinx_tabs.tabs", - ] - autodoc_typehints = "description" --intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} -+intersphinx_mapping = {"python": ("/usr/share/doc/python3-doc/html", None)} - issues_github_path = "pallets/click" +@@ -27,7 +27,7 @@ extlinks = { + "pr": ("https://github.com/pallets/click/pull/%s", "#%s"), + } + intersphinx_mapping = { +- "python": ("https://docs.python.org/3/", None), ++ "python": ("/usr/share/doc/python3-doc/html", None), + } # HTML ----------------------------------------------------------------- diff --git a/debian/patches/series b/debian/patches/series index 84ceced..95c8d20 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1 +1,2 @@ docs-Use-local-Python-intersphinx-inventary.patch +build-uninstalled-docs.patch diff --git a/debian/rules b/debian/rules index a54f781..5cfe077 100755 --- a/debian/rules +++ b/debian/rules @@ -3,7 +3,7 @@ #export DH_VERBOSE=1 -include /usr/share/dpkg/pkg-info.mk +include /usr/share/dpkg/default.mk BUILD_DATE = $(shell LC_ALL=C date -u "+%d %B %Y" -d "@$(SOURCE_DATE_EPOCH)") SPHINXOPTS := -E -N -D html_last_updated_fmt="$(BUILD_DATE)" @@ -11,6 +11,8 @@ SPHINXOPTS := -E -N -D html_last_updated_fmt="$(BUILD_DATE)" export LC_ALL=C.UTF-8 export LANG=C.UTF-8 +export DEB_VERSION_UPSTREAM # used by patched docs/conf.py + export PYBUILD_NAME=click export PYBUILD_TEST_PYTEST=1 export PYBUILD_TEST_ARGS=-k 'not test_expand_args' {dir}/tests/ @@ -18,17 +20,13 @@ export PYBUILD_TEST_ARGS=-k 'not test_expand_args' {dir}/tests/ export SOURCE = $(CURDIR)/debian/ %: - dh $@ --with sphinxdoc --buildsystem=pybuild + dh $@ --buildsystem=pybuild -override_dh_sphinxdoc: -ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) +execute_before_dh_sphinxdoc: PYTHONPATH=src/ python3 -m sphinx -b html $(SPHINXOPTS) docs/ debian/python-click-doc/usr/share/doc/python-click-doc/html/ - dh_sphinxdoc -endif override_dh_installexamples: dh_installexamples -ppython-click-doc examples/* -override_dh_fixperms: - dh_fixperms +execute_after_dh_fixperms: rm -f debian/python-click-doc/usr/share/doc/python-click-doc/examples/imagepipe/.gitignore diff --git a/debian/source/lintian-overrides b/debian/source/lintian-overrides new file mode 100644 index 0000000..cfd2b55 --- /dev/null +++ b/debian/source/lintian-overrides @@ -0,0 +1,10 @@ +# We use X-DH-Compat: 14 +# +python-click source: debhelper-compat-file-is-missing +python-click source: package-uses-deprecated-debhelper-compat-version 1 + +# The "default" dependencies are added automatically by debhelper +# in compat level 14. +# +python-click source: debhelper-but-no-misc-depends python3-click +python-click source: debhelper-but-no-misc-depends python-click-doc diff --git a/debian/tests/control b/debian/tests/control index 5699968..839bd1e 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -1,4 +1,8 @@ -Tests: unittests -Depends: @, - @builddeps@, -Restrictions: allow-stderr +Tests: + unittests, +Depends: + python3-all, + python3-pytest, + @, +Restrictions: + allow-stderr, diff --git a/debian/tests/unittests b/debian/tests/unittests index bef050b..96edb25 100644 --- a/debian/tests/unittests +++ b/debian/tests/unittests @@ -3,12 +3,12 @@ set -efu pys="$(py3versions -s 2> /dev/null)" -cp -a setup.cfg tests "$AUTOPKGTEST_TMP" +cp -a pyproject.toml tests "$AUTOPKGTEST_TMP" # needed by `test_expand_args` mkdir $AUTOPKGTEST_TMP/docs cp -a docs/conf.py $AUTOPKGTEST_TMP/docs -cp -a setup.cfg "$AUTOPKGTEST_TMP" +cp -a pyproject.toml "$AUTOPKGTEST_TMP" cd "$AUTOPKGTEST_TMP" diff --git a/docs/advanced.rst b/docs/advanced.rst index 5649561..9ef0b14 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -435,6 +435,7 @@ defined as a context manager: def __enter__(self): path = os.path.join(self.home, "repo.db") self.db = open_database(path) + return self def __exit__(self, exc_type, exc_value, tb): self.db.close() diff --git a/docs/arguments.rst b/docs/arguments.rst index 60b53c5..ae2991b 100644 --- a/docs/arguments.rst +++ b/docs/arguments.rst @@ -5,18 +5,21 @@ Arguments .. currentmodule:: click -Arguments work similarly to :ref:`options ` but are positional. -They also only support a subset of the features of options due to their -syntactical nature. Click will also not attempt to document arguments for -you and wants you to :ref:`document them manually ` -in order to avoid ugly help pages. +Arguments are: + +* Are positional in nature. +* Similar to a limited version of :ref:`options ` that can take an arbitrary number of inputs +* :ref:`Documented manually `. + +Useful and often used kwargs are: + +* ``default``: Passes a default. +* ``nargs``: Sets the number of arguments. Set to -1 to take an arbitrary number. Basic Arguments --------------- -The most basic option is a simple string argument of one value. If no -type is provided, the type of the default value is used, and if no default -value is provided, the type is assumed to be :data:`STRING`. +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``. Example: @@ -24,173 +27,96 @@ Example: @click.command() @click.argument('filename') - def touch(filename): + def touch(filename: str): """Print FILENAME.""" click.echo(filename) -And what it looks like: +And from the command line: .. click:run:: invoke(touch, args=['foo.txt']) -Variadic Arguments ------------------- -The second most common version is variadic arguments where a specific (or -unlimited) number of arguments is accepted. This can be controlled with -the ``nargs`` parameter. If it is set to ``-1``, then an unlimited number -of arguments is accepted. +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`. -The value is then passed as a tuple. Note that only one argument can be -set to ``nargs=-1``, as it will eat up all arguments. +.. admonition:: Note on Required Arguments -Example: + 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. + +Multiple Arguments +----------------------------------- + +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. .. click:example:: @click.command() - @click.argument('src', nargs=-1) - @click.argument('dst', nargs=1) - def copy(src, dst): + @click.argument('src', nargs=1) + @click.argument('dsts', nargs=-1) + def copy(src: str, dsts: tuple[str, ...]): """Move file SRC to DST.""" - for fn in src: - click.echo(f"move {fn} to folder {dst}") + for destination in dsts: + click.echo(f"Copy {src} to folder {destination}") -And what it looks like: +And from the command line: .. click:run:: - invoke(copy, args=['foo.txt', 'bar.txt', 'my_folder']) - -Note that this is not how you would write this application. The reason -for this is that in this particular example the arguments are defined as -strings. Filenames, however, are not strings! They might be on certain -operating systems, but not necessarily on all. For better ways to write -this, see the next sections. - -.. admonition:: Note on Non-Empty Variadic Arguments + invoke(copy, args=['foo.txt', 'usr/david/foo.txt', 'usr/mitsuko/foo.txt']) - If you come from ``argparse``, you might be missing support for setting - ``nargs`` to ``+`` to indicate that at least one argument is required. +.. admonition:: Note on Handling Files - This is supported by setting ``required=True``. However, this should - not be used if you can avoid it as we believe scripts should gracefully - degrade into becoming noops if a variadic argument is empty. The - reason for this is that very often, scripts are invoked with wildcard - inputs from the command line and they should not error out if the - wildcard is empty. + 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. -.. _file-args: +Argument Escape Sequences +--------------------------- -File Arguments --------------- +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. -Since all the examples have already worked with filenames, it makes sense -to explain how to deal with files properly. Command line tools are more -fun if they work with files the Unix way, which is to accept ``-`` as a -special file that refers to stdin/stdout. - -Click supports this through the :class:`click.File` type which -intelligently handles files for you. It also deals with Unicode and bytes -correctly for all versions of Python so your script stays very portable. - -Example: +Example usage: .. click:example:: @click.command() - @click.argument('input', type=click.File('rb')) - @click.argument('output', type=click.File('wb')) - def inout(input, output): - """Copy contents of INPUT to OUTPUT.""" - while True: - chunk = input.read(1024) - if not chunk: - break - output.write(chunk) - -And what it does: - -.. click:run:: + @click.argument('files', nargs=-1, type=click.Path()) + def touch(files): + """Print all FILES file names.""" + for filename in files: + click.echo(filename) - with isolated_filesystem(): - invoke(inout, args=['-', 'hello.txt'], input=['hello'], - terminate_input=True) - invoke(inout, args=['hello.txt', '-']) +And from the command line: -File Path Arguments -------------------- +.. click:run:: -In the previous example, the files were opened immediately. But what if -we just want the filename? The naïve way is to use the default string -argument type. The :class:`Path` type has several checks available which raise nice -errors if they fail, such as existence. Filenames in these error messages are formatted -with :func:`format_filename`, so any undecodable bytes will be printed nicely. + invoke(touch, ['--', '-foo.txt', 'bar.txt']) -Example: +If you don't like the ``--`` marker, you can set ignore_unknown_options to True to avoid checking unknown options: .. click:example:: - @click.command() - @click.argument('filename', type=click.Path(exists=True)) - def touch(filename): - """Print FILENAME if the file exists.""" - click.echo(click.format_filename(filename)) + @click.command(context_settings={"ignore_unknown_options": True}) + @click.argument('files', nargs=-1, type=click.Path()) + def touch(files): + """Print all FILES file names.""" + for filename in files: + click.echo(filename) -And what it does: +And from the command line: .. click:run:: - with isolated_filesystem(): - with open('hello.txt', 'w') as f: - f.write('Hello World!\n') - invoke(touch, args=['hello.txt']) - println() - invoke(touch, args=['missing.txt']) - - -File Opening Safety -------------------- - -The :class:`FileType` type has one problem it needs to deal with, and that -is to decide when to open a file. The default behavior is to be -"intelligent" about it. What this means is that it will open stdin/stdout -and files opened for reading immediately. This will give the user direct -feedback when a file cannot be opened, but it will only open files -for writing the first time an IO operation is performed by automatically -wrapping the file in a special wrapper. - -This behavior can be forced by passing ``lazy=True`` or ``lazy=False`` to -the constructor. If the file is opened lazily, it will fail its first IO -operation by raising an :exc:`FileError`. - -Since files opened for writing will typically immediately empty the file, -the lazy mode should only be disabled if the developer is absolutely sure -that this is intended behavior. - -Forcing lazy mode is also very useful to avoid resource handling -confusion. If a file is opened in lazy mode, it will receive a -``close_intelligently`` method that can help figure out if the file -needs closing or not. This is not needed for parameters, but is -necessary for manually prompting with the :func:`prompt` function as you -do not know if a stream like stdout was opened (which was already open -before) or a real file that needs closing. - -Starting with Click 2.0, 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. + invoke(touch, ['-foo.txt', 'bar.txt']) + + +.. _environment-variables: Environment Variables --------------------- -Like options, arguments can also grab values from an environment variable. -Unlike options, however, this is only supported for explicitly named -environment variables. +Arguments can use environment variables. To do so, pass the name(s) of the environment variable(s) via `envvar` in ``click.argument``. -Example usage: +Checking one environment variable: .. click:example:: @@ -205,59 +131,28 @@ And from the command line: .. click:run:: with isolated_filesystem(): + # Writing the file in the filesystem. with open('hello.txt', 'w') as f: f.write('Hello World!') invoke(echo, env={'SRC': 'hello.txt'}) -In that case, it can also be a list of different environment variables -where the first one is picked. -Generally, this feature is not recommended because it can cause the user -a lot of confusion. - -Option-Like Arguments ---------------------- - -Sometimes, you want to process arguments that look like options. For -instance, imagine you have a file named ``-foo.txt``. If you pass this as -an argument in this manner, Click will treat it as an option. - -To solve this, Click does what any POSIX style command line script does, -and that is to accept the string ``--`` as a separator for options and -arguments. After the ``--`` marker, all further parameters are accepted as -arguments. - -Example usage: +Checking multiple environment variables: .. click:example:: @click.command() - @click.argument('files', nargs=-1, type=click.Path()) - def touch(files): - """Print all FILES file names.""" - for filename in files: - click.echo(filename) - -And from the command line: - -.. click:run:: - - invoke(touch, ['--', '-foo.txt', 'bar.txt']) - -If you don't like the ``--`` marker, you can set ignore_unknown_options to -True to avoid checking unknown options: - -.. click:example:: - - @click.command(context_settings={"ignore_unknown_options": True}) - @click.argument('files', nargs=-1, type=click.Path()) - def touch(files): - """Print all FILES file names.""" - for filename in files: - click.echo(filename) + @click.argument('src', envvar=['SRC', 'SRC_2'], type=click.File('r')) + def echo(src): + """Print value of SRC environment variable.""" + click.echo(src.read()) And from the command line: .. click:run:: - invoke(touch, ['-foo.txt', 'bar.txt']) + with isolated_filesystem(): + # Writing the file in the filesystem. + with open('hello.txt', 'w') as f: + f.write('Hello World from second variable!') + invoke(echo, env={'SRC_2': 'hello.txt'}) diff --git a/docs/conf.py b/docs/conf.py index db71e39..db253ea 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,18 +10,25 @@ # General -------------------------------------------------------------- -master_doc = "index" +default_role = "code" extensions = [ "sphinx.ext.autodoc", + "sphinx.ext.extlinks", "sphinx.ext.intersphinx", + "sphinx_tabs.tabs", "sphinxcontrib.log_cabinet", "pallets_sphinx_themes", - "sphinx_issues", - "sphinx_tabs.tabs", ] +autodoc_member_order = "bysource" autodoc_typehints = "description" -intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} -issues_github_path = "pallets/click" +autodoc_preserve_defaults = True +extlinks = { + "issue": ("https://github.com/pallets/click/issues/%s", "#%s"), + "pr": ("https://github.com/pallets/click/pull/%s", "#%s"), +} +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), +} # HTML ----------------------------------------------------------------- @@ -46,7 +53,3 @@ html_logo = "_static/click-logo-sidebar.png" html_title = f"Click Documentation ({version})" html_show_sourcelink = False - -# LaTeX ---------------------------------------------------------------- - -latex_documents = [(master_doc, f"Click-{version}.tex", html_title, author, "manual")] diff --git a/docs/contrib.rst b/docs/contrib.rst index 287862c..628edd7 100644 --- a/docs/contrib.rst +++ b/docs/contrib.rst @@ -18,4 +18,41 @@ Please note that the quality and stability of those packages may be different than Click itself. While published under a common organization, they are still separate from Click and the Pallets maintainers. + +Third-party projects +-------------------- + +Other projects that extend Click's features are available outside of the +click-contrib_ organization. + +Some of the most popular and actively maintained are listed below: + +========================================================== =========================================================================================== ================================================================================================= ====================================================================================================== +Project Description Popularity Activity +========================================================== =========================================================================================== ================================================================================================= ====================================================================================================== +`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 + :alt: GitHub stars :alt: Last commit +`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 + :alt: GitHub stars :alt: Last commit +`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 + :alt: GitHub stars :alt: Last commit +`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 + :alt: GitHub stars :alt: Last commit +`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 + :alt: GitHub stars :alt: Last commit +========================================================== =========================================================================================== ================================================================================================= ====================================================================================================== + +.. note:: + + To make it into the list above, a project: + + - must be actively maintained (at least one commit in the last year) + - must have a reasonable number of stars (at least 20) + + If you have a project that meets these criteria, please open a pull request + to add it to the list. + + If a project is no longer maintained or does not meet the criteria above, + please open a pull request to remove it from the list. + .. _click-contrib: https://github.com/click-contrib/ diff --git a/docs/documentation.rst b/docs/documentation.rst index 349acfb..da0aaa1 100644 --- a/docs/documentation.rst +++ b/docs/documentation.rst @@ -36,7 +36,7 @@ And what it looks like: .. _documenting-arguments: Documenting Arguments -~~~~~~~~~~~~~~~~~~~~~ +---------------------- :func:`click.argument` does not take a ``help`` parameter. This is to follow the general convention of Unix tools of using arguments for only diff --git a/docs/handling-files.rst b/docs/handling-files.rst new file mode 100644 index 0000000..0a61966 --- /dev/null +++ b/docs/handling-files.rst @@ -0,0 +1,90 @@ +.. _handling-files: + +Handling Files +================ + +.. currentmodule:: click + +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. + +.. _file-args: + +File Arguments +----------------- + +Click supports working with files with the :class:`File` type. Some notable features are: + +* 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. +* Deals with ``str`` and ``bytes`` correctly for all versions of Python. + +Example: + +.. click:example:: + + @click.command() + @click.argument('input', type=click.File('rb')) + @click.argument('output', type=click.File('wb')) + def inout(input, output): + """Copy contents of INPUT to OUTPUT.""" + while True: + chunk = input.read(1024) + if not chunk: + break + output.write(chunk) + +And from the command line: + +.. click:run:: + + with isolated_filesystem(): + invoke(inout, args=['-', 'hello.txt'], input=['hello'], + terminate_input=True) + invoke(inout, args=['hello.txt', '-']) + +File Path Arguments +---------------------- + +For handling paths, the :class:`Path` type is better than a ``str``. Some notable features are: + +* The ``exists`` argument will verify whether the path exists. +* ``readable``, ``writable``, and ``executable`` can perform permission checks. +* ``file_okay`` and ``dir_okay`` allow specifying whether files/directories are accepted. +* Error messages are nicely formatted using :func:`format_filename` so any undecodable bytes will be printed nicely. + +See :class:`Path` for all features. + +Example: + +.. click:example:: + + @click.command() + @click.argument('filename', type=click.Path(exists=True)) + def touch(filename): + """Print FILENAME if the file exists.""" + click.echo(click.format_filename(filename)) + +And from the command line: + +.. click:run:: + + with isolated_filesystem(): + with open('hello.txt', 'w') as f: + f.write('Hello World!\n') + invoke(touch, args=['hello.txt']) + println() + invoke(touch, args=['missing.txt']) + + +File Opening Behaviors +----------------------------- + +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. + +File open behavior can be controlled by the boolean kwarg ``lazy``. If a file is opened lazily: + +* A failure at first IO operation will happen by raising an :exc:`FileError`. +* 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). + +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. + +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. diff --git a/docs/index.rst b/docs/index.rst index ad965a3..08266b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -68,12 +68,14 @@ usage patterns. why quickstart + virtualenv setuptools parameters options arguments commands prompts + handling-files documentation complex advanced diff --git a/docs/license.rst b/docs/license.rst index a53a98c..2a445f9 100644 --- a/docs/license.rst +++ b/docs/license.rst @@ -1,4 +1,5 @@ BSD-3-Clause License ==================== -.. include:: ../LICENSE.rst +.. literalinclude:: ../LICENSE.txt + :language: text diff --git a/docs/parameters.rst b/docs/parameters.rst index b3604e7..6ce19b6 100644 --- a/docs/parameters.rst +++ b/docs/parameters.rst @@ -3,39 +3,62 @@ Parameters .. currentmodule:: click -Click supports two types of parameters for scripts: options and arguments. -There is generally some confusion among authors of command line scripts of -when to use which, so here is a quick overview of the differences. As its -name indicates, an option is optional. While arguments can be optional -within reason, they are much more restricted in how optional they can be. - -To help you decide between options and arguments, the recommendation is -to use arguments exclusively for things like going to subcommands or input -filenames / URLs, and have everything else be an option instead. - -Differences ------------ - -Arguments can do less than options. The following features are only -available for options: - -* automatic prompting for missing input -* act as flags (boolean or otherwise) -* option values can be pulled from environment variables, arguments can not -* options are fully documented in the help page, arguments are not - (:ref:`this is intentional ` as arguments - might be too specific to be automatically documented) - -On the other hand arguments, unlike options, can accept an arbitrary number -of arguments. Options can strictly ever only accept a fixed number of -arguments (defaults to 1), or they may be specified multiple times using -:ref:`multiple-options`. +Click supports only two types of parameters for scripts (by design): options and arguments. + +Options +---------------- + +* Are optional. +* Recommended to use for everything except subcommands, urls, or files. +* Can take a fixed number of arguments. The default is 1. They may be specified multiple times using :ref:`multiple-options`. +* Are fully documented by the help page. +* Have automatic prompting for missing input. +* Can act as flags (boolean or otherwise). +* Can be pulled from environment variables. + +Arguments +---------------- + +* Are optional with in reason, but not entirely so. +* Recommended to use for subcommands, urls, or files. +* Can take an arbitrary number of arguments. +* Are not fully documented by the help page since they may be too specific to be automatically documented. For more see :ref:`documenting-arguments`. +* Can be pulled from environment variables but only explicitly named ones. For more see :ref:`environment-variables`. + +.. _parameter_names: + +Parameter Names +--------------- + +Parameters (options and arguments) have a name that will be used as +the Python argument name when calling the decorated function with +values. + +.. click:example:: + + @click.command() + @click.argument('filename') + @click.option('-t', '--times', type=int) + def multi_echo(filename, times): + """Print value filename multiple times.""" + for x in range(times): + click.echo(filename) + +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`. +The option's names are ``-t`` and ``--times``. More names are available for options and are covered in :ref:`options`. + +And what it looks like when run: + +.. click:run:: + + invoke(multi_echo, ['--times=3', 'index.txt'], prog_name='multi_echo') + +.. _parameter-types: Parameter Types --------------- -Parameters can be of different types. Types can be implemented with -different behavior and some are supported out of the box: +The supported parameter types are: ``str`` / :data:`click.STRING`: The default parameter type which indicates unicode strings. @@ -74,37 +97,10 @@ different behavior and some are supported out of the box: .. autoclass:: DateTime :noindex: -Custom parameter types can be implemented by subclassing -:class:`click.ParamType`. For simple cases, passing a Python function that -fails with a `ValueError` is also supported, though discouraged. - -.. _parameter_names: - -Parameter Names ---------------- - -Parameters (both options and arguments) have a name that will be used as -the Python argument name when calling the decorated function with -values. - -Arguments take only one positional name. To provide a different name for -use in help text, see :ref:`doc-meta-variables`. - -Options can have many names that may be prefixed with one or two dashes. -Names with one dash are parsed as short options, names with two are -parsed as long options. If a name is not prefixed, it is used as the -Python argument name and not parsed as an option name. Otherwise, the -first name with a two dash prefix is used, or the first with a one dash -prefix if there are none with two. The prefix is removed and dashes are -converted to underscores to get the Python argument name. - - -Implementing Custom Types -------------------------- +How to Implement Custom Types +------------------------------- -To implement a custom type, you need to subclass the :class:`ParamType` -class. Override the :meth:`~ParamType.convert` method to convert the -value from a string to the correct type. +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. The following code implements an integer type that accepts hex and octal numbers in addition to normal integers, and converts them into regular diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 7cd26ca..f240bf9 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -3,98 +3,28 @@ Quickstart .. currentmodule:: click -You can get the library directly from PyPI:: +Install +---------------------- +Install from PyPI:: pip install click -The installation into a :ref:`virtualenv` is heavily recommended. +Installing into a virtual environment is highly recommended. We suggest :ref:`virtualenv-heading`. -.. _virtualenv: - -virtualenv ----------- - -Virtualenv is probably what you want to use for developing Click -applications. - -What problem does virtualenv solve? Chances are that you want to use it -for other projects besides your Click script. But the more projects you -have, the more likely it is that you will be working with different -versions of Python itself, or at least different versions of Python -libraries. Let's face it: quite often libraries break backwards -compatibility, and it's unlikely that any serious application will have -zero dependencies. So what do you do if two or more of your projects have -conflicting dependencies? - -Virtualenv to the rescue! Virtualenv enables multiple side-by-side -installations of Python, one for each project. It doesn't actually -install separate copies of Python, but it does provide a clever way to -keep different project environments isolated. - -Create your project folder, then a virtualenv within it:: - - $ mkdir myproject - $ cd myproject - $ python3 -m venv .venv - -Now, whenever you want to work on a project, you only have to activate the -corresponding environment. On OS X and Linux, do the following:: - - $ . .venv/bin/activate - (venv) $ - -If you are a Windows user, the following command is for you:: - - > .venv\scripts\activate - (venv) > - -Either way, you should now be using your virtualenv (notice how the prompt of -your shell has changed to show the active environment). - -And if you want to stop using the virtualenv, use the following command:: - - $ deactivate - -After doing this, the prompt of your shell should be as familiar as before. - -Now, let's move on. Enter the following command to get Click activated in your -virtualenv:: - - $ pip install click - -A few seconds later and you are good to go. - -Screencast and Examples +Examples ----------------------- -There is a screencast available which shows the basic API of Click and -how to build simple applications with it. It also explores how to build -commands with subcommands. - -* `Building Command Line Applications with Click - `_ - -Examples of Click applications can be found in the documentation as well -as in the GitHub repository together with readme files: - -* ``inout``: `File input and output - `_ -* ``naval``: `Port of docopt naval example - `_ -* ``aliases``: `Command alias example - `_ -* ``repo``: `Git-/Mercurial-like command line interface - `_ -* ``complex``: `Complex example with plugin loading - `_ -* ``validation``: `Custom parameter validation example - `_ -* ``colors``: `Color support demo - `_ -* ``termui``: `Terminal UI functions demo - `_ -* ``imagepipe``: `Multi command chaining demo - `_ +Some standalone examples of Click applications are packaged with Click. They are available in the `examples folder `_ of the repo. + +* `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. +* `validation `_ : A simple example of an application that performs custom validation of parameters in different ways. +* `naval `_ : Port of the `docopt `_ naval example. +* `colors `_ : A simple example that colorizes text. Uses colorama on Windows. +* `aliases `_ : An advanced example that implements :ref:`aliases`. +* `imagepipe `_ : A complex example that implements some :ref:`multi-command-chaining` . It chains together image processing instructions. Requires pillow. +* `repo `_ : An advanced example that implements a Git-/Mercurial-like command line interface. +* `complex `_ : A very advanced example that implements loading subcommands dynamically from a plugin folder. +* `termui `_ : A simple example that showcases terminal UI helpers provided by click. Basic Concepts - Creating a Command ----------------------------------- diff --git a/docs/setuptools.rst b/docs/setuptools.rst index 7c60c7d..1b41414 100644 --- a/docs/setuptools.rst +++ b/docs/setuptools.rst @@ -81,10 +81,12 @@ Contents of ``setup.py``: }, ) -The magic is in the ``entry_points`` parameter. Below -``console_scripts``, each line identifies one console script. The first -part before the equals sign (``=``) is the name of the script that should -be generated, the second part is the import path followed by a colon +The magic is in the ``entry_points`` parameter. Read the full +`entry_points `_ +specification for more details. Below ``console_scripts``, each +line identifies one console script. The first part before the +equals sign (``=``) is the name of the script that should be +generated, the second part is the import path followed by a colon (``:``) with the Click command. That's it. diff --git a/docs/shell-completion.rst b/docs/shell-completion.rst index ebf73c3..dd9ddd4 100644 --- a/docs/shell-completion.rst +++ b/docs/shell-completion.rst @@ -28,12 +28,16 @@ through an entry point, not through the ``python`` command. See :doc:`/setuptools`. Once the executable is installed, calling it with a special environment variable will put Click in completion mode. -In order for completion to be used, the user needs to register a special -function with their shell. The script is different for every shell, and -Click will output it when called with ``_{PROG_NAME}_COMPLETE`` set to -``{shell}_source``. ``{PROG_NAME}`` is the executable name in uppercase -with dashes replaced by underscores. The built-in shells are ``bash``, -``zsh``, and ``fish``. +To enable shell completion, the user needs to register a special +function with their shell. The exact script varies depending on the +shell you are using. Click will output it when called with +``_{FOO_BAR}_COMPLETE`` set to ``{shell}_source``. ``{FOO_BAR}`` is +the executable name in uppercase with dashes replaced by underscores. +It is conventional but not strictly required for environment variable +names to be in upper case. This convention helps distinguish environment +variables from regular shell variables and commands, making scripts +and configuration files more readable and easier to maintain. The +built-in shells are ``bash``, ``zsh``, and ``fish``. Provide your users with the following instructions customized to your program name. This uses ``foo-bar`` as an example. @@ -62,7 +66,7 @@ program name. This uses ``foo-bar`` as an example. .. code-block:: fish - eval (env _FOO_BAR_COMPLETE=fish_source foo-bar) + _FOO_BAR_COMPLETE=fish_source foo-bar | source This is the same file used for the activation script method below. For Fish it's probably always easier to use that method. @@ -194,7 +198,7 @@ require implementing some smaller parts. First, you'll need to figure out how your shell's completion system works and write a script to integrate it with Click. It must invoke your -program with the environment variable ``_{PROG_NAME}_COMPLETE`` set to +program with the environment variable ``_{FOO_BAR}_COMPLETE`` set to ``{shell}_complete`` and pass the complete args and incomplete value. How it passes those values, and the format of the completion response from Click is up to you. @@ -207,7 +211,7 @@ formatting with the following variables: in the script. - ``complete_var`` - The environment variable name for passing the ``{shell}_complete`` instruction. -- ``prog_name`` - The name of the executable being completed. +- ``foo_bar`` - The name of the executable being completed. The example code is for a made up shell "My Shell" or "mysh" for short. @@ -218,10 +222,10 @@ The example code is for a made up shell "My Shell" or "mysh" for short. _mysh_source = """\ %(complete_func)s { - response=$(%(complete_var)s=mysh_complete %(prog_name)s) + response=$(%(complete_var)s=mysh_complete %(foo_bar)s) # parse response and set completions somehow } - call-on-complete %(prog_name)s %(complete_func)s + call-on-complete %(foo_bar)s %(complete_func)s """ @add_completion_class diff --git a/docs/virtualenv.rst b/docs/virtualenv.rst new file mode 100644 index 0000000..2a565c1 --- /dev/null +++ b/docs/virtualenv.rst @@ -0,0 +1,54 @@ +.. _virtualenv-heading: + +Virtualenv +========================= + +Why Use Virtualenv? +------------------------- + +You should use `Virtualenv `_ because: + +* It allows you to install multiple versions of the same dependency. + +* If you have an operating system version of Python, it prevents you from changing its dependencies and potentially messing up your os. + +How to Use Virtualenv +----------------------------- + +Create your project folder, then a virtualenv within it:: + + $ mkdir myproject + $ cd myproject + $ python3 -m venv .venv + +Now, whenever you want to work on a project, you only have to activate the +corresponding environment. + +.. tabs:: + + .. group-tab:: OSX/Linux + + .. code-block:: text + + $ . .venv/bin/activate + (venv) $ + + .. group-tab:: Windows + + .. code-block:: text + + > .venv\scripts\activate + (venv) > + + +You are now using your virtualenv (notice how the prompt of your shell has changed to show the active environment). + +To install packages in the virtual environment:: + + $ pip install click + +And if you want to stop using the virtualenv, use the following command:: + + $ deactivate + +After doing this, the prompt of your shell should be as familiar as before. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..358f524 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,103 @@ +[project] +name = "click" +description = "Composable command line interface toolkit" +readme = "README.md" +license = {file = "LICENSE.txt"} +maintainers = [{name = "Pallets", email = "contact@palletsprojects.com"}] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Typing :: Typed", +] +requires-python = ">=3.7" +dependencies = [ + "colorama; platform_system == 'Windows'", + "importlib-metadata; python_version < '3.8'", +] +dynamic = ["version"] + +[project.urls] +Donate = "https://palletsprojects.com/donate" +Documentation = "https://click.palletsprojects.com/" +Changes = "https://click.palletsprojects.com/changes/" +Source = "https://github.com/pallets/click/" +Chat = "https://discord.gg/pallets" + +[build-system] +requires = ["flit_core<4"] +build-backend = "flit_core.buildapi" + +[tool.flit.module] +name = "click" + +[tool.flit.sdist] +include = [ + "docs/", + "requirements/", + "tests/", + "CHANGES.rst", + "tox.ini", +] +exclude = [ + "docs/_build/", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +filterwarnings = [ + "error", +] + +[tool.coverage.run] +branch = true +source = ["click", "tests"] + +[tool.coverage.paths] +source = ["src", "*/site-packages"] + +[tool.mypy] +python_version = "3.8" +files = ["src/click", "tests/typing"] +show_error_codes = true +pretty = true +strict = true + +[[tool.mypy.overrides]] +module = [ + "colorama.*", +] +ignore_missing_imports = true + +[tool.pyright] +pythonVersion = "3.8" +include = ["src/click", "tests/typing"] +typeCheckingMode = "basic" + +[tool.ruff] +extend-exclude = ["examples/"] +src = ["src"] +fix = true +show-fixes = true +output-format = "full" + +[tool.ruff.lint] +select = [ + "B", # flake8-bugbear + "E", # pycodestyle error + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "W", # pycodestyle warning +] + +[tool.ruff.lint.isort] +force-single-line = true +order-by-type = false + +[tool.gha-update] +tag-only = [ + "slsa-framework/slsa-github-generator", +] diff --git a/requirements/build.txt b/requirements/build.txt index 196545d..9d6dd10 100644 --- a/requirements/build.txt +++ b/requirements/build.txt @@ -1,13 +1,12 @@ -# SHA1:80754af91bfb6d1073585b046fe0a474ce868509 # -# This file is autogenerated by pip-compile-multi -# To update, run: +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: # -# pip-compile-multi +# pip-compile build.in # -build==0.10.0 - # via -r requirements/build.in -packaging==23.1 +build==1.2.2.post1 + # via -r build.in +packaging==24.2 # via build -pyproject-hooks==1.0.0 +pyproject-hooks==1.2.0 # via build diff --git a/requirements/dev.txt b/requirements/dev.txt index 1a4ecc7..144178e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,60 +1,148 @@ -# SHA1:54b5b77ec8c7a0064ffa93b2fd16cb0130ba177c # -# This file is autogenerated by pip-compile-multi -# To update, run: +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: # -# pip-compile-multi +# pip-compile dev.in # --r docs.txt --r tests.txt --r typing.txt -build==0.10.0 +alabaster==1.0.0 + # via sphinx +babel==2.16.0 + # via sphinx +build==1.2.2.post1 # via pip-tools -cachetools==5.3.1 +cachetools==5.5.0 # via tox -cfgv==3.3.1 +certifi==2024.8.30 + # via requests +cfgv==3.4.0 # via pre-commit -chardet==5.1.0 +chardet==5.2.0 # via tox -click==8.1.3 +charset-normalizer==3.4.0 + # via requests +click==8.1.7 # via # pip-compile-multi # pip-tools colorama==0.4.6 # via tox -distlib==0.3.6 +distlib==0.3.9 # via virtualenv -filelock==3.12.2 +docutils==0.21.2 + # via + # sphinx + # sphinx-tabs +filelock==3.16.1 # via # tox # virtualenv -identify==2.5.24 +identify==2.6.3 # via pre-commit -pip-compile-multi==2.6.3 - # via -r requirements/dev.in -pip-tools==6.13.0 +idna==3.10 + # via requests +imagesize==1.4.1 + # via sphinx +iniconfig==2.0.0 + # via pytest +jinja2==3.1.4 + # via sphinx +markupsafe==3.0.2 + # via jinja2 +mypy==1.13.0 + # via -r /Users/david/Projects/click/requirements/typing.in +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.9.1 + # via + # pre-commit + # pyright +packaging==24.2 + # via + # build + # pallets-sphinx-themes + # pyproject-api + # pytest + # sphinx + # tox +pallets-sphinx-themes==2.3.0 + # via -r /Users/david/Projects/click/requirements/docs.in +pip-compile-multi==2.7.1 + # via -r dev.in +pip-tools==7.4.1 # via pip-compile-multi -platformdirs==3.8.0 +platformdirs==4.3.6 # via # tox # virtualenv -pre-commit==3.3.3 - # via -r requirements/dev.in -pyproject-api==1.5.2 +pluggy==1.5.0 + # via + # pytest + # tox +pre-commit==4.0.1 + # via -r dev.in +pygments==2.18.0 + # via + # sphinx + # sphinx-tabs +pyproject-api==1.8.0 # via tox -pyproject-hooks==1.0.0 - # via build -pyyaml==6.0 +pyproject-hooks==1.2.0 + # via + # build + # pip-tools +pyright==1.1.390 + # via -r /Users/david/Projects/click/requirements/typing.in +pytest==8.3.4 + # via -r /Users/david/Projects/click/requirements/tests.in +pyyaml==6.0.2 # via pre-commit +requests==2.32.3 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==8.1.3 + # via + # -r /Users/david/Projects/click/requirements/docs.in + # pallets-sphinx-themes + # sphinx-issues + # sphinx-notfound-page + # sphinx-tabs + # sphinxcontrib-log-cabinet +sphinx-issues==5.0.0 + # via -r /Users/david/Projects/click/requirements/docs.in +sphinx-notfound-page==1.0.4 + # via pallets-sphinx-themes +sphinx-tabs==3.4.7 + # via -r /Users/david/Projects/click/requirements/docs.in +sphinxcontrib-applehelp==2.0.0 + # via sphinx +sphinxcontrib-devhelp==2.0.0 + # via sphinx +sphinxcontrib-htmlhelp==2.1.0 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-log-cabinet==1.0.1 + # via -r /Users/david/Projects/click/requirements/docs.in +sphinxcontrib-qthelp==2.0.0 + # via sphinx +sphinxcontrib-serializinghtml==2.0.0 + # via sphinx toposort==1.10 # via pip-compile-multi -tox==4.6.3 - # via -r requirements/dev.in -virtualenv==20.23.1 +tox==4.23.2 + # via -r dev.in +typing-extensions==4.12.2 + # via + # mypy + # pyright +urllib3==2.2.3 + # via requests +virtualenv==20.28.0 # via # pre-commit # tox -wheel==0.40.0 +wheel==0.45.1 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements/docs.txt b/requirements/docs.txt index 26922dd..0f3e90c 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -1,68 +1,70 @@ -# SHA1:34fd4ca6516e97c7348e6facdd9c4ebb68209d1c # -# This file is autogenerated by pip-compile-multi -# To update, run: +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: # -# pip-compile-multi +# pip-compile docs.in # -alabaster==0.7.13 +alabaster==1.0.0 # via sphinx -babel==2.12.1 +babel==2.16.0 # via sphinx -certifi==2023.5.7 +certifi==2024.8.30 # via requests -charset-normalizer==3.1.0 +charset-normalizer==3.4.0 # via requests -docutils==0.18.1 +docutils==0.21.2 # via # sphinx # sphinx-tabs -idna==3.4 +idna==3.10 # via requests imagesize==1.4.1 # via sphinx -jinja2==3.1.2 +jinja2==3.1.4 # via sphinx -markupsafe==2.1.3 +markupsafe==3.0.2 # via jinja2 -packaging==23.1 +packaging==24.2 # via # pallets-sphinx-themes # sphinx -pallets-sphinx-themes==2.1.1 - # via -r requirements/docs.in -pygments==2.15.1 +pallets-sphinx-themes==2.3.0 + # via -r docs.in +pygments==2.18.0 # via # sphinx # sphinx-tabs -requests==2.31.0 +requests==2.32.3 # via sphinx snowballstemmer==2.2.0 # via sphinx -sphinx==7.0.1 +sphinx==8.1.3 # via - # -r requirements/docs.in + # -r docs.in # pallets-sphinx-themes # sphinx-issues + # sphinx-notfound-page # sphinx-tabs # sphinxcontrib-log-cabinet -sphinx-issues==3.0.1 - # via -r requirements/docs.in -sphinx-tabs==3.4.1 - # via -r requirements/docs.in -sphinxcontrib-applehelp==1.0.4 +sphinx-issues==5.0.0 + # via -r docs.in +sphinx-notfound-page==1.0.4 + # via pallets-sphinx-themes +sphinx-tabs==3.4.7 + # via -r docs.in +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-log-cabinet==1.0.1 - # via -r requirements/docs.in -sphinxcontrib-qthelp==1.0.3 + # via -r docs.in +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -urllib3==2.0.3 +urllib3==2.2.3 # via requests diff --git a/requirements/tests.txt b/requirements/tests.txt index 6a68088..b0e7799 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,15 +1,14 @@ -# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee # -# This file is autogenerated by pip-compile-multi -# To update, run: +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: # -# pip-compile-multi +# pip-compile tests.in # iniconfig==2.0.0 # via pytest -packaging==23.1 +packaging==24.2 # via pytest -pluggy==1.2.0 +pluggy==1.5.0 # via pytest -pytest==7.4.0 - # via -r requirements/tests.in +pytest==8.3.4 + # via -r tests.in diff --git a/requirements/tests37.txt b/requirements/tests37.txt new file mode 100644 index 0000000..d490541 --- /dev/null +++ b/requirements/tests37.txt @@ -0,0 +1,26 @@ +# +# This file is autogenerated by pip-compile with Python 3.7 +# by the following command: +# +# pip-compile --output-file=tests37.txt tests.in +# +exceptiongroup==1.2.2 + # via pytest +importlib-metadata==6.7.0 + # via + # pluggy + # pytest +iniconfig==2.0.0 + # via pytest +packaging==24.0 + # via pytest +pluggy==1.2.0 + # via pytest +pytest==7.4.4 + # via -r tests.in +tomli==2.0.1 + # via pytest +typing-extensions==4.7.1 + # via importlib-metadata +zipp==3.15.0 + # via importlib-metadata diff --git a/requirements/typing.txt b/requirements/typing.txt index f04fde5..f9402d6 100644 --- a/requirements/typing.txt +++ b/requirements/typing.txt @@ -1,20 +1,18 @@ -# SHA1:0d25c235a98f3c8c55aefb59b91c82834e185f0a # -# This file is autogenerated by pip-compile-multi -# To update, run: +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: # -# pip-compile-multi +# pip-compile typing.in # -mypy==1.4.1 - # via -r requirements/typing.in +mypy==1.13.0 + # via -r typing.in mypy-extensions==1.0.0 # via mypy -nodeenv==1.8.0 +nodeenv==1.9.1 # via pyright -pyright==1.1.317 - # via -r requirements/typing.in -typing-extensions==4.6.3 - # via mypy - -# The following packages are considered to be unsafe in a requirements file: -# setuptools +pyright==1.1.390 + # via -r typing.in +typing-extensions==4.12.2 + # via + # mypy + # pyright diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index d5fcdcf..0000000 --- a/setup.cfg +++ /dev/null @@ -1,104 +0,0 @@ -[metadata] -name = click -version = attr: click.__version__ -url = https://palletsprojects.com/p/click/ -project_urls = - Donate = https://palletsprojects.com/donate - Documentation = https://click.palletsprojects.com/ - Changes = https://click.palletsprojects.com/changes/ - Source Code = https://github.com/pallets/click/ - Issue Tracker = https://github.com/pallets/click/issues/ - Chat = https://discord.gg/pallets -license = BSD-3-Clause -license_files = LICENSE.rst -maintainer = Pallets -maintainer_email = contact@palletsprojects.com -description = Composable command line interface toolkit -long_description = file: README.rst -long_description_content_type = text/x-rst -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - -[options] -packages = find: -package_dir = = src -include_package_data = True -python_requires = >= 3.7 -# Dependencies are in setup.py for GitHub's dependency graph. - -[options.packages.find] -where = src - -[tool:pytest] -testpaths = tests -filterwarnings = - error - -[coverage:run] -branch = True -source = - click - tests - -[coverage:paths] -source = - src - */site-packages - -[flake8] -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -# ISC = implicit str concat -select = B, E, F, W, B9, ISC -ignore = - # slice notation whitespace, invalid - E203 - # line length, handled by bugbear B950 - E501 - # bare except, handled by bugbear B001 - E722 - # bin op line break, invalid - W503 - # zip with strict=, requires python >= 3.10 - B905 - # string formatting opinion, B028 renamed to B907 - B028 - B907 -# up to 88 allowed by bugbear B950 -max-line-length = 80 -per-file-ignores = - # __init__ exports names - src/click/__init__.py: F401 - -[mypy] -files = src/click, tests/typing -python_version = 3.7 -show_error_codes = True -disallow_subclassing_any = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -disallow_any_generics = True -check_untyped_defs = True -no_implicit_optional = True -local_partial_types = True -no_implicit_reexport = True -strict_equality = True -warn_redundant_casts = True -warn_unused_configs = True -warn_unused_ignores = True -warn_return_any = True -warn_unreachable = True - -[mypy-colorama.*] -ignore_missing_imports = True - -[mypy-importlib_metadata.*] -ignore_missing_imports = True diff --git a/setup.py b/setup.py deleted file mode 100644 index 0a74d41..0000000 --- a/setup.py +++ /dev/null @@ -1,9 +0,0 @@ -from setuptools import setup - -setup( - name="click", - install_requires=[ - "colorama; platform_system == 'Windows'", - "importlib-metadata; python_version < '3.8'", - ], -) diff --git a/src/click/__init__.py b/src/click/__init__.py index 520ae88..2610d0e 100644 --- a/src/click/__init__.py +++ b/src/click/__init__.py @@ -4,6 +4,7 @@ around a simple API that does not come with too much magic and is composable. """ + from .core import Argument as Argument from .core import BaseCommand as BaseCommand from .core import Command as Command @@ -18,6 +19,7 @@ from .decorators import confirmation_option as confirmation_option from .decorators import group as group from .decorators import help_option as help_option +from .decorators import HelpOption as HelpOption from .decorators import make_pass_decorator as make_pass_decorator from .decorators import option as option from .decorators import pass_context as pass_context @@ -70,4 +72,4 @@ from .utils import get_text_stream as get_text_stream from .utils import open_file as open_file -__version__ = "8.1.6" +__version__ = "8.1.8" diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index f744657..ad9f8f6 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -3,6 +3,7 @@ import time of Click down, some infrequently used functionality is placed in this module and only imported as needed. """ + import contextlib import math import os @@ -11,6 +12,7 @@ import typing as t from gettext import gettext as _ from io import StringIO +from shutil import which from types import TracebackType from ._compat import _default_text_stdout @@ -371,31 +373,42 @@ def pager(generator: t.Iterable[str], color: t.Optional[bool] = None) -> None: pager_cmd = (os.environ.get("PAGER", None) or "").strip() if pager_cmd: if WIN: - return _tempfilepager(generator, pager_cmd, color) - return _pipepager(generator, pager_cmd, color) + if _tempfilepager(generator, pager_cmd, color): + return + elif _pipepager(generator, pager_cmd, color): + return if os.environ.get("TERM") in ("dumb", "emacs"): return _nullpager(stdout, generator, color) - if WIN or sys.platform.startswith("os2"): - return _tempfilepager(generator, "more <", color) - if hasattr(os, "system") and os.system("(less) 2>/dev/null") == 0: - return _pipepager(generator, "less", color) + if (WIN or sys.platform.startswith("os2")) and _tempfilepager( + generator, "more", color + ): + return + if _pipepager(generator, "less", color): + return import tempfile fd, filename = tempfile.mkstemp() os.close(fd) try: - if hasattr(os, "system") and os.system(f'more "{filename}"') == 0: - return _pipepager(generator, "more", color) + if _pipepager(generator, "more", color): + return return _nullpager(stdout, generator, color) finally: os.unlink(filename) -def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> None: +def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> bool: """Page through text by feeding it to another program. Invoking a pager through this might support colors. + + Returns True if the command was found, False otherwise and thus another + pager should be attempted. """ + cmd_absolute = which(cmd) + if cmd_absolute is None: + return False + import subprocess env = dict(os.environ) @@ -411,19 +424,25 @@ def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> elif "r" in less_flags or "R" in less_flags: color = True - c = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, env=env) - stdin = t.cast(t.BinaryIO, c.stdin) - encoding = get_best_encoding(stdin) + c = subprocess.Popen( + [cmd_absolute], + shell=True, + stdin=subprocess.PIPE, + env=env, + errors="replace", + text=True, + ) + assert c.stdin is not None try: for text in generator: if not color: text = strip_ansi(text) - stdin.write(text.encode(encoding, "replace")) + c.stdin.write(text) except (OSError, KeyboardInterrupt): pass else: - stdin.close() + c.stdin.close() # Less doesn't respect ^C, but catches it for its own UI purposes (aborting # search or other commands inside less). @@ -441,11 +460,25 @@ def _pipepager(generator: t.Iterable[str], cmd: str, color: t.Optional[bool]) -> else: break + return True + def _tempfilepager( - generator: t.Iterable[str], cmd: str, color: t.Optional[bool] -) -> None: - """Page through text by invoking a program on a temporary file.""" + generator: t.Iterable[str], + cmd: str, + color: t.Optional[bool], +) -> bool: + """Page through text by invoking a program on a temporary file. + + Returns True if the command was found, False otherwise and thus another + pager should be attempted. + """ + # Which is necessary for Windows, it is also recommended in the Popen docs. + cmd_absolute = which(cmd) + if cmd_absolute is None: + return False + + import subprocess import tempfile fd, filename = tempfile.mkstemp() @@ -457,11 +490,16 @@ def _tempfilepager( with open_stream(filename, "wb")[0] as f: f.write(text.encode(encoding)) try: - os.system(f'{cmd} "{filename}"') + subprocess.call([cmd_absolute, filename]) + except OSError: + # Command not found + pass finally: os.close(fd) os.unlink(filename) + return True + def _nullpager( stream: t.TextIO, generator: t.Iterable[str], color: t.Optional[bool] @@ -496,7 +534,7 @@ def get_editor(self) -> str: if WIN: return "notepad" for editor in "sensible-editor", "vim", "nano": - if os.system(f"which {editor} >/dev/null 2>&1") == 0: + if which(editor) is not None: return editor return "vi" @@ -595,22 +633,33 @@ def _unquote_file(url: str) -> str: null.close() elif WIN: if locate: - url = _unquote_file(url.replace('"', "")) - args = f'explorer /select,"{url}"' + url = _unquote_file(url) + args = ["explorer", f"/select,{url}"] else: - url = url.replace('"', "") - wait_str = "/WAIT" if wait else "" - args = f'start {wait_str} "" "{url}"' - return os.system(args) + args = ["start"] + if wait: + args.append("/WAIT") + args.append("") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 elif CYGWIN: if locate: - url = os.path.dirname(_unquote_file(url).replace('"', "")) - args = f'cygstart "{url}"' + url = _unquote_file(url) + args = ["cygstart", os.path.dirname(url)] else: - url = url.replace('"', "") - wait_str = "-w" if wait else "" - args = f'cygstart {wait_str} "{url}"' - return os.system(args) + args = ["cygstart"] + if wait: + args.append("-w") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 try: if locate: @@ -698,8 +747,8 @@ def getchar(echo: bool) -> str: return rv else: - import tty import termios + import tty @contextlib.contextmanager def raw_terminal() -> t.Iterator[int]: diff --git a/src/click/core.py b/src/click/core.py index cc65e89..e630501 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -39,6 +39,8 @@ if t.TYPE_CHECKING: import typing_extensions as te + + from .decorators import HelpOption from .shell_completion import CompletionItem F = t.TypeVar("F", bound=t.Callable[..., t.Any]) @@ -115,9 +117,16 @@ def iter_params_for_processing( invocation_order: t.Sequence["Parameter"], declaration_order: t.Sequence["Parameter"], ) -> t.List["Parameter"]: - """Given a sequence of parameters in the order as should be considered - for processing and an iterable of parameters that exist, this returns - a list in the correct order as they should be processed. + """Returns all declared parameters in the order they should be processed. + + The declared parameters are re-shuffled depending on the order in which + they were invoked, as well as the eagerness of each parameters. + + The invocation order takes precedence over the declaration order. I.e. the + order in which the user provided them to the CLI is respected. + + This behavior and its effect on callback evaluation is detailed at: + https://click.palletsprojects.com/en/stable/advanced/#callback-evaluation-order """ def sort_key(item: "Parameter") -> t.Tuple[bool, float]: @@ -383,9 +392,9 @@ def __init__( #: An optional normalization function for tokens. This is #: options, choices, commands etc. - self.token_normalize_func: t.Optional[ - t.Callable[[str], str] - ] = token_normalize_func + self.token_normalize_func: t.Optional[t.Callable[[str], str]] = ( + token_normalize_func + ) #: Indicates if resilient parsing is enabled. In that case Click #: will do its best to not cause any failures and default values @@ -624,7 +633,7 @@ def find_root(self) -> "Context": def find_object(self, object_type: t.Type[V]) -> t.Optional[V]: """Finds the closest object of a given type.""" - node: t.Optional["Context"] = self + node: t.Optional[Context] = self while node is not None: if isinstance(node.obj, object_type): @@ -646,14 +655,12 @@ def ensure_object(self, object_type: t.Type[V]) -> V: @t.overload def lookup_default( self, name: str, call: "te.Literal[True]" = True - ) -> t.Optional[t.Any]: - ... + ) -> t.Optional[t.Any]: ... @t.overload def lookup_default( self, name: str, call: "te.Literal[False]" = ... - ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: - ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: ... def lookup_default(self, name: str, call: bool = True) -> t.Optional[t.Any]: """Get the default for a parameter from :attr:`default_map`. @@ -713,24 +720,22 @@ def _make_sub_context(self, command: "Command") -> "Context": @t.overload def invoke( - __self, # noqa: B902 + __self, __callback: "t.Callable[..., V]", *args: t.Any, **kwargs: t.Any, - ) -> V: - ... + ) -> V: ... @t.overload def invoke( - __self, # noqa: B902 + __self, __callback: "Command", *args: t.Any, **kwargs: t.Any, - ) -> t.Any: - ... + ) -> t.Any: ... def invoke( - __self, # noqa: B902 + __self, __callback: t.Union["Command", "t.Callable[..., V]"], *args: t.Any, **kwargs: t.Any, @@ -782,9 +787,7 @@ def invoke( with ctx: return __callback(*args, **kwargs) - def forward( - __self, __cmd: "Command", *args: t.Any, **kwargs: t.Any # noqa: B902 - ) -> t.Any: + def forward(__self, __cmd: "Command", *args: t.Any, **kwargs: t.Any) -> t.Any: """Similar to :meth:`invoke` but fills in default keyword arguments from the current context if the other command expects it. This cannot invoke callbacks directly, only other commands. @@ -936,7 +939,10 @@ def make_context( extra[key] = value ctx = self.context_class( - self, info_name=info_name, parent=parent, **extra # type: ignore + self, # type: ignore[arg-type] + info_name=info_name, + parent=parent, + **extra, ) with ctx.scope(cleanup=False): @@ -971,7 +977,7 @@ def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionIte """ from click.shell_completion import CompletionItem - results: t.List["CompletionItem"] = [] + results: t.List[CompletionItem] = [] while ctx.parent is not None: ctx = ctx.parent @@ -993,8 +999,7 @@ def main( complete_var: t.Optional[str] = None, standalone_mode: "te.Literal[True]" = True, **extra: t.Any, - ) -> "te.NoReturn": - ... + ) -> "te.NoReturn": ... @t.overload def main( @@ -1004,8 +1009,7 @@ def main( complete_var: t.Optional[str] = None, standalone_mode: bool = ..., **extra: t.Any, - ) -> t.Any: - ... + ) -> t.Any: ... def main( self, @@ -1221,12 +1225,13 @@ def __init__( #: the list of parameters for this command in the order they #: should show up in the help page and execute. Eager parameters #: will automatically be handled before non eager ones. - self.params: t.List["Parameter"] = params or [] + self.params: t.List[Parameter] = params or [] self.help = help self.epilog = epilog self.options_metavar = options_metavar self.short_help = short_help self.add_help_option = add_help_option + self._help_option: t.Optional[HelpOption] = None self.no_args_is_help = no_args_is_help self.hidden = hidden self.deprecated = deprecated @@ -1289,25 +1294,29 @@ def get_help_option_names(self, ctx: Context) -> t.List[str]: return list(all_names) def get_help_option(self, ctx: Context) -> t.Optional["Option"]: - """Returns the help option object.""" + """Returns the help option object. + + Unless ``add_help_option`` is ``False``. + + .. versionchanged:: 8.1.8 + The help option is now cached to avoid creating it multiple times. + """ help_options = self.get_help_option_names(ctx) if not help_options or not self.add_help_option: return None - def show_help(ctx: Context, param: "Parameter", value: str) -> None: - if value and not ctx.resilient_parsing: - echo(ctx.get_help(), color=ctx.color) - ctx.exit() - - return Option( - help_options, - is_flag=True, - is_eager=True, - expose_value=False, - callback=show_help, - help=_("Show this message and exit."), - ) + # Cache the help option object in private _help_option attribute to + # avoid creating it multiple times. Not doing this will break the + # callback odering by iter_params_for_processing(), which relies on + # object comparison. + if self._help_option is None: + # Avoid circular import. + from .decorators import HelpOption + + self._help_option = HelpOption(help_options) + + return self._help_option def make_parser(self, ctx: Context) -> OptionParser: """Creates the underlying option parser for this command.""" @@ -1444,7 +1453,7 @@ def shell_complete(self, ctx: Context, incomplete: str) -> t.List["CompletionIte """ from click.shell_completion import CompletionItem - results: t.List["CompletionItem"] = [] + results: t.List[CompletionItem] = [] if incomplete and not incomplete[0].isalnum(): for param in self.get_params(ctx): @@ -1604,7 +1613,7 @@ def function(__value, *args, **kwargs): # type: ignore return f(inner, *args, **kwargs) self._result_callback = rv = update_wrapper(t.cast(F, function), f) - return rv + return rv # type: ignore[return-value] return decorator @@ -1843,14 +1852,12 @@ def add_command(self, cmd: Command, name: t.Optional[str] = None) -> None: self.commands[name] = cmd @t.overload - def command(self, __func: t.Callable[..., t.Any]) -> Command: - ... + def command(self, __func: t.Callable[..., t.Any]) -> Command: ... @t.overload def command( self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], Command]: - ... + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: ... def command( self, *args: t.Any, **kwargs: t.Any @@ -1894,14 +1901,12 @@ def decorator(f: t.Callable[..., t.Any]) -> Command: return decorator @t.overload - def group(self, __func: t.Callable[..., t.Any]) -> "Group": - ... + def group(self, __func: t.Callable[..., t.Any]) -> "Group": ... @t.overload def group( self, *args: t.Any, **kwargs: t.Any - ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: - ... + ) -> t.Callable[[t.Callable[..., t.Any]], "Group"]: ... def group( self, *args: t.Any, **kwargs: t.Any @@ -2227,14 +2232,12 @@ def make_metavar(self) -> str: @t.overload def get_default( self, ctx: Context, call: "te.Literal[True]" = True - ) -> t.Optional[t.Any]: - ... + ) -> t.Optional[t.Any]: ... @t.overload def get_default( self, ctx: Context, call: bool = ... - ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: - ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: ... def get_default( self, ctx: Context, call: bool = True @@ -2681,7 +2684,9 @@ def _parse_decls( if name is None: if not expose_value: return None, opts, secondary_opts - raise TypeError("Could not determine name for option") + raise TypeError( + f"Could not determine name for option with declarations {decls!r}" + ) if not opts and not secondary_opts: raise TypeError( @@ -2810,10 +2815,12 @@ def _write_opts(opts: t.Sequence[str]) -> str: # For boolean flags that have distinct True/False opts, # use the opt without prefix instead of the value. default_string = split_opt( - (self.opts if self.default else self.secondary_opts)[0] + (self.opts if default_value else self.secondary_opts)[0] )[1] elif self.is_bool_flag and not self.secondary_opts and not default_value: default_string = "" + elif default_value == "": + default_string = '""' else: default_string = str(default_value) @@ -2842,14 +2849,12 @@ def _write_opts(opts: t.Sequence[str]) -> str: @t.overload def get_default( self, ctx: Context, call: "te.Literal[True]" = True - ) -> t.Optional[t.Any]: - ... + ) -> t.Optional[t.Any]: ... @t.overload def get_default( self, ctx: Context, call: bool = ... - ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: - ... + ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: ... def get_default( self, ctx: Context, call: bool = True @@ -3021,7 +3026,7 @@ def _parse_decls( if not decls: if not expose_value: return None, [], [] - raise TypeError("Could not determine name for argument") + raise TypeError("Argument is marked as exposed, but does not have a name.") if len(decls) == 1: name = arg = decls[0] name = name.replace("-", "_").lower() diff --git a/src/click/decorators.py b/src/click/decorators.py index d9bba95..bcf8906 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -93,7 +93,7 @@ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R": return update_wrapper(new_func, f) - return decorator # type: ignore[return-value] + return decorator def pass_meta_key( @@ -126,7 +126,7 @@ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> R: f"Decorator that passes {doc_description} as the first argument" " to the decorated function." ) - return decorator # type: ignore[return-value] + return decorator CmdType = t.TypeVar("CmdType", bound=Command) @@ -134,8 +134,7 @@ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> R: # variant: no call, directly as decorator for a function. @t.overload -def command(name: _AnyCallable) -> Command: - ... +def command(name: _AnyCallable) -> Command: ... # variant: with positional name and with positional or keyword cls argument: @@ -145,8 +144,7 @@ def command( name: t.Optional[str], cls: t.Type[CmdType], **attrs: t.Any, -) -> t.Callable[[_AnyCallable], CmdType]: - ... +) -> t.Callable[[_AnyCallable], CmdType]: ... # variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...) @@ -156,16 +154,14 @@ def command( *, cls: t.Type[CmdType], **attrs: t.Any, -) -> t.Callable[[_AnyCallable], CmdType]: - ... +) -> t.Callable[[_AnyCallable], CmdType]: ... # variant: with optional string name, no cls argument provided. @t.overload def command( name: t.Optional[str] = ..., cls: None = None, **attrs: t.Any -) -> t.Callable[[_AnyCallable], Command]: - ... +) -> t.Callable[[_AnyCallable], Command]: ... def command( @@ -255,8 +251,7 @@ def decorator(f: _AnyCallable) -> CmdType: # variant: no call, directly as decorator for a function. @t.overload -def group(name: _AnyCallable) -> Group: - ... +def group(name: _AnyCallable) -> Group: ... # variant: with positional name and with positional or keyword cls argument: @@ -266,8 +261,7 @@ def group( name: t.Optional[str], cls: t.Type[GrpType], **attrs: t.Any, -) -> t.Callable[[_AnyCallable], GrpType]: - ... +) -> t.Callable[[_AnyCallable], GrpType]: ... # variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...) @@ -277,16 +271,14 @@ def group( *, cls: t.Type[GrpType], **attrs: t.Any, -) -> t.Callable[[_AnyCallable], GrpType]: - ... +) -> t.Callable[[_AnyCallable], GrpType]: ... # variant: with optional string name, no cls argument provided. @t.overload def group( name: t.Optional[str] = ..., cls: None = None, **attrs: t.Any -) -> t.Callable[[_AnyCallable], Group]: - ... +) -> t.Callable[[_AnyCallable], Group]: ... def group( @@ -495,7 +487,7 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: metadata: t.Optional[types.ModuleType] try: - from importlib import metadata # type: ignore + from importlib import metadata except ImportError: # Python < 3.8 import importlib_metadata as metadata # type: ignore @@ -530,32 +522,41 @@ def callback(ctx: Context, param: Parameter, value: bool) -> None: return option(*param_decls, **kwargs) -def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: - """Add a ``--help`` option which immediately prints the help page +class HelpOption(Option): + """Pre-configured ``--help`` option which immediately prints the help page and exits the program. + """ - This is usually unnecessary, as the ``--help`` option is added to - each command automatically unless ``add_help_option=False`` is - passed. + def __init__( + self, + param_decls: t.Optional[t.Sequence[str]] = None, + **kwargs: t.Any, + ) -> None: + if not param_decls: + param_decls = ("--help",) - :param param_decls: One or more option names. Defaults to the single - value ``"--help"``. - :param kwargs: Extra arguments are passed to :func:`option`. - """ + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs.setdefault("callback", self.show_help) - def callback(ctx: Context, param: Parameter, value: bool) -> None: - if not value or ctx.resilient_parsing: - return + super().__init__(param_decls, **kwargs) - echo(ctx.get_help(), color=ctx.color) - ctx.exit() + @staticmethod + def show_help(ctx: Context, param: Parameter, value: bool) -> None: + """Callback that print the help page on ```` and exits.""" + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() - if not param_decls: - param_decls = ("--help",) - kwargs.setdefault("is_flag", True) - kwargs.setdefault("expose_value", False) - kwargs.setdefault("is_eager", True) - kwargs.setdefault("help", _("Show this message and exit.")) - kwargs["callback"] = callback +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Decorator for the pre-configured ``--help`` option defined above. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + kwargs.setdefault("cls", HelpOption) return option(*param_decls, **kwargs) diff --git a/src/click/exceptions.py b/src/click/exceptions.py index fe68a36..0b83151 100644 --- a/src/click/exceptions.py +++ b/src/click/exceptions.py @@ -3,6 +3,7 @@ from gettext import ngettext from ._compat import get_text_stderr +from .globals import resolve_color_default from .utils import echo from .utils import format_filename @@ -13,7 +14,7 @@ def _join_param_hints( - param_hint: t.Optional[t.Union[t.Sequence[str], str]] + param_hint: t.Optional[t.Union[t.Sequence[str], str]], ) -> t.Optional[str]: if param_hint is not None and not isinstance(param_hint, str): return " / ".join(repr(x) for x in param_hint) @@ -29,6 +30,9 @@ class ClickException(Exception): def __init__(self, message: str) -> None: super().__init__(message) + # The context will be removed by the time we print the message, so cache + # the color settings here to be used later on (in `show`) + self.show_color: t.Optional[bool] = resolve_color_default() self.message = message def format_message(self) -> str: @@ -41,7 +45,11 @@ def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None: if file is None: file = get_text_stderr() - echo(_("Error: {message}").format(message=self.format_message()), file=file) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=self.show_color, + ) class UsageError(ClickException): @@ -58,7 +66,7 @@ class UsageError(ClickException): def __init__(self, message: str, ctx: t.Optional["Context"] = None) -> None: super().__init__(message) self.ctx = ctx - self.cmd: t.Optional["Command"] = self.ctx.command if self.ctx else None + self.cmd: t.Optional[Command] = self.ctx.command if self.ctx else None def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None: if file is None: diff --git a/src/click/globals.py b/src/click/globals.py index 480058f..191e712 100644 --- a/src/click/globals.py +++ b/src/click/globals.py @@ -3,19 +3,18 @@ if t.TYPE_CHECKING: import typing_extensions as te + from .core import Context _local = local() @t.overload -def get_current_context(silent: "te.Literal[False]" = False) -> "Context": - ... +def get_current_context(silent: "te.Literal[False]" = False) -> "Context": ... @t.overload -def get_current_context(silent: bool = ...) -> t.Optional["Context"]: - ... +def get_current_context(silent: bool = ...) -> t.Optional["Context"]: ... def get_current_context(silent: bool = False) -> t.Optional["Context"]: diff --git a/src/click/parser.py b/src/click/parser.py index 5fa7adf..600b843 100644 --- a/src/click/parser.py +++ b/src/click/parser.py @@ -17,6 +17,7 @@ Copyright 2001-2006 Gregory P. Ward. All rights reserved. Copyright 2002-2006 Python Software Foundation. All rights reserved. """ + # This code uses parts of optparse written by Gregory P. Ward and # maintained by the Python Software Foundation. # Copyright 2001-2006 Gregory P. Ward @@ -33,6 +34,7 @@ if t.TYPE_CHECKING: import typing_extensions as te + from .core import Argument as CoreArgument from .core import Context from .core import Option as CoreOption @@ -247,7 +249,7 @@ def __init__(self, rargs: t.List[str]) -> None: self.opts: t.Dict[str, t.Any] = {} self.largs: t.List[str] = [] self.rargs = rargs - self.order: t.List["CoreParameter"] = [] + self.order: t.List[CoreParameter] = [] class OptionParser: diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index 5de1247..07d0f09 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -167,25 +167,25 @@ def __getattr__(self, name: str) -> t.Any: """ _SOURCE_FISH = """\ -function %(complete_func)s +function %(complete_func)s; set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ -COMP_CWORD=(commandline -t) %(prog_name)s) +COMP_CWORD=(commandline -t) %(prog_name)s); - for completion in $response - set -l metadata (string split "," $completion) + for completion in $response; + set -l metadata (string split "," $completion); - if test $metadata[1] = "dir" - __fish_complete_directories $metadata[2] - else if test $metadata[1] = "file" - __fish_complete_path $metadata[2] - else if test $metadata[1] = "plain" - echo $metadata[2] - end - end -end + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; complete --no-files --command %(prog_name)s --arguments \ -"(%(complete_func)s)" +"(%(complete_func)s)"; """ @@ -230,7 +230,7 @@ def func_name(self) -> str: """The name of the shell function defined by the completion script. """ - safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), re.ASCII) + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII) return f"_{safe_name}_completion" def source_vars(self) -> t.Dict[str, t.Any]: @@ -301,27 +301,37 @@ class BashComplete(ShellComplete): name = "bash" source_template = _SOURCE_BASH - def _check_version(self) -> None: + @staticmethod + def _check_version() -> None: + import shutil import subprocess - output = subprocess.run( - ["bash", "-c", 'echo "${BASH_VERSION}"'], stdout=subprocess.PIPE - ) - match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + bash_exe = shutil.which("bash") + + if bash_exe is None: + match = None + else: + output = subprocess.run( + [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'], + stdout=subprocess.PIPE, + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) if match is not None: major, minor = match.groups() if major < "4" or major == "4" and minor < "4": - raise RuntimeError( + echo( _( "Shell completion is not supported for Bash" " versions older than 4.4." - ) + ), + err=True, ) else: - raise RuntimeError( - _("Couldn't detect Bash version, shell completion is not supported.") + echo( + _("Couldn't detect Bash version, shell completion is not supported."), + err=True, ) def source(self) -> str: diff --git a/src/click/termui.py b/src/click/termui.py index db7a4b2..c084f19 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -173,7 +173,7 @@ def prompt_func(text: str) -> str: if hide_input: echo(_("Error: The value you entered was invalid."), err=err) else: - echo(_("Error: {e.message}").format(e=e), err=err) # noqa: B306 + echo(_("Error: {e.message}").format(e=e), err=err) continue if not confirmation_prompt: return result diff --git a/src/click/testing.py b/src/click/testing.py index e0df0d2..772b215 100644 --- a/src/click/testing.py +++ b/src/click/testing.py @@ -8,6 +8,7 @@ import typing as t from types import TracebackType +from . import _compat from . import formatting from . import termui from . import utils @@ -311,10 +312,12 @@ def should_strip_ansi( old_hidden_prompt_func = termui.hidden_prompt_func old__getchar_func = termui._getchar old_should_strip_ansi = utils.should_strip_ansi # type: ignore + old__compat_should_strip_ansi = _compat.should_strip_ansi termui.visible_prompt_func = visible_input termui.hidden_prompt_func = hidden_input termui._getchar = _getchar utils.should_strip_ansi = should_strip_ansi # type: ignore + _compat.should_strip_ansi = should_strip_ansi old_env = {} try: @@ -344,6 +347,7 @@ def should_strip_ansi( termui.hidden_prompt_func = old_hidden_prompt_func termui._getchar = old__getchar_func utils.should_strip_ansi = old_should_strip_ansi # type: ignore + _compat.should_strip_ansi = old__compat_should_strip_ansi formatting.FORCED_WIDTH = old_forced_width def invoke( @@ -475,5 +479,5 @@ def isolated_filesystem( if temp_dir is None: try: shutil.rmtree(dt) - except OSError: # noqa: B014 + except OSError: pass diff --git a/src/click/types.py b/src/click/types.py index 2b1d179..a70fd58 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -15,6 +15,7 @@ if t.TYPE_CHECKING: import typing_extensions as te + from .core import Context from .core import Parameter from .shell_completion import CompletionItem @@ -658,12 +659,15 @@ class File(ParamType): will not be held open until first IO. lazy is mainly useful when opening for writing to avoid creating the file until it is needed. - Starting with Click 2.0, files can also be opened atomically in which - case 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. + Files can also be opened atomically in which case 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. See :ref:`file-args` for more information. + + .. versionchanged:: 2.0 + Added the ``atomic`` parameter. """ name = "filename" @@ -737,7 +741,7 @@ def convert( ctx.call_on_close(safecall(f.flush)) return f - except OSError as e: # noqa: B014 + except OSError as e: self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) def shell_complete( @@ -891,7 +895,7 @@ def convert( ) if not self.dir_okay and stat.S_ISDIR(st.st_mode): self.fail( - _("{name} '{filename}' is a directory.").format( + _("{name} {filename!r} is a directory.").format( name=self.name.title(), filename=format_filename(value) ), param, diff --git a/src/click/utils.py b/src/click/utils.py index d536434..836c6f2 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -156,7 +156,7 @@ def open(self) -> t.IO[t.Any]: rv, self.should_close = open_stream( self.name, self.mode, self.encoding, self.errors, atomic=self.atomic ) - except OSError as e: # noqa: E402 + except OSError as e: from .exceptions import FileError raise FileError(self.name, hint=e.strerror) from e @@ -311,7 +311,7 @@ def echo( out = strip_ansi(out) elif WIN: if auto_wrap_for_ansi is not None: - file = auto_wrap_for_ansi(file) # type: ignore + file = auto_wrap_for_ansi(file, color) # type: ignore elif not color: out = strip_ansi(out) @@ -353,7 +353,7 @@ def get_text_stream( def open_file( - filename: str, + filename: t.Union[str, "os.PathLike[str]"], mode: str = "r", encoding: t.Optional[str] = None, errors: t.Optional[str] = "strict", @@ -374,7 +374,7 @@ def open_file( with open_file(filename) as f: ... - :param filename: The name of the file to open, or ``'-'`` for + :param filename: The name or Path of the file to open, or ``'-'`` for ``stdin``/``stdout``. :param mode: The mode in which to open the file. :param encoding: The encoding to decode or encode a file opened in @@ -410,7 +410,7 @@ def format_filename( with the replacement character ``�``. Invalid bytes or surrogate escapes will raise an error when written to a - stream with ``errors="strict". This will typically happen with ``stdout`` + stream with ``errors="strict"``. This will typically happen with ``stdout`` when the locale is something like ``en_GB.UTF-8``. Many scenarios *are* safe to write surrogates though, due to PEP 538 and diff --git a/tests/test_commands.py b/tests/test_commands.py index ed9d96f..dcf66ac 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -305,6 +305,138 @@ def test_group_add_command_name(runner): assert result.exit_code == 0 +@pytest.mark.parametrize( + ("invocation_order", "declaration_order", "expected_order"), + [ + # Non-eager options. + ([], ["-a"], ["-a"]), + (["-a"], ["-a"], ["-a"]), + ([], ["-a", "-c"], ["-a", "-c"]), + (["-a"], ["-a", "-c"], ["-a", "-c"]), + (["-c"], ["-a", "-c"], ["-c", "-a"]), + ([], ["-c", "-a"], ["-c", "-a"]), + (["-a"], ["-c", "-a"], ["-a", "-c"]), + (["-c"], ["-c", "-a"], ["-c", "-a"]), + (["-a", "-c"], ["-a", "-c"], ["-a", "-c"]), + (["-c", "-a"], ["-a", "-c"], ["-c", "-a"]), + # Eager options. + ([], ["-b"], ["-b"]), + (["-b"], ["-b"], ["-b"]), + ([], ["-b", "-d"], ["-b", "-d"]), + (["-b"], ["-b", "-d"], ["-b", "-d"]), + (["-d"], ["-b", "-d"], ["-d", "-b"]), + ([], ["-d", "-b"], ["-d", "-b"]), + (["-b"], ["-d", "-b"], ["-b", "-d"]), + (["-d"], ["-d", "-b"], ["-d", "-b"]), + (["-b", "-d"], ["-b", "-d"], ["-b", "-d"]), + (["-d", "-b"], ["-b", "-d"], ["-d", "-b"]), + # Mixed options. + ([], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + (["-a"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + (["-b"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + (["-c"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-c", "-a"]), + (["-d"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-a", "-c"]), + (["-a", "-b"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + (["-b", "-a"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + (["-d", "-c"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-c", "-a"]), + (["-c", "-d"], ["-a", "-b", "-c", "-d"], ["-d", "-b", "-c", "-a"]), + (["-a", "-b", "-c", "-d"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + (["-b", "-d", "-a", "-c"], ["-a", "-b", "-c", "-d"], ["-b", "-d", "-a", "-c"]), + ([], ["-b", "-d", "-e", "-a", "-c"], ["-b", "-d", "-e", "-a", "-c"]), + (["-a", "-d"], ["-b", "-d", "-e", "-a", "-c"], ["-d", "-b", "-e", "-a", "-c"]), + (["-c", "-d"], ["-b", "-d", "-e", "-a", "-c"], ["-d", "-b", "-e", "-c", "-a"]), + ], +) +def test_iter_params_for_processing( + invocation_order, declaration_order, expected_order +): + parameters = { + "-a": click.Option(["-a"]), + "-b": click.Option(["-b"], is_eager=True), + "-c": click.Option(["-c"]), + "-d": click.Option(["-d"], is_eager=True), + "-e": click.Option(["-e"], is_eager=True), + } + + invocation_params = [parameters[opt_id] for opt_id in invocation_order] + declaration_params = [parameters[opt_id] for opt_id in declaration_order] + expected_params = [parameters[opt_id] for opt_id in expected_order] + + assert ( + click.core.iter_params_for_processing(invocation_params, declaration_params) + == expected_params + ) + + +def test_help_param_priority(runner): + """Cover the edge-case in which the eagerness of help option was not + respected, because it was internally generated multiple times. + + See: https://github.com/pallets/click/pull/2811 + """ + + def print_and_exit(ctx, param, value): + if value: + click.echo(f"Value of {param.name} is: {value}") + ctx.exit() + + @click.command(context_settings={"help_option_names": ("--my-help",)}) + @click.option("-a", is_flag=True, expose_value=False, callback=print_and_exit) + @click.option( + "-b", is_flag=True, expose_value=False, callback=print_and_exit, is_eager=True + ) + def cli(): + pass + + # --my-help is properly called and stop execution. + result = runner.invoke(cli, ["--my-help"]) + assert "Value of a is: True" not in result.stdout + assert "Value of b is: True" not in result.stdout + assert "--my-help" in result.stdout + assert result.exit_code == 0 + + # -a is properly called and stop execution. + result = runner.invoke(cli, ["-a"]) + assert "Value of a is: True" in result.stdout + assert "Value of b is: True" not in result.stdout + assert "--my-help" not in result.stdout + assert result.exit_code == 0 + + # -a takes precedence over -b and stop execution. + result = runner.invoke(cli, ["-a", "-b"]) + assert "Value of a is: True" not in result.stdout + assert "Value of b is: True" in result.stdout + assert "--my-help" not in result.stdout + assert result.exit_code == 0 + + # --my-help is eager by default so takes precedence over -a and stop + # execution, whatever the order. + for args in [["-a", "--my-help"], ["--my-help", "-a"]]: + result = runner.invoke(cli, args) + assert "Value of a is: True" not in result.stdout + assert "Value of b is: True" not in result.stdout + assert "--my-help" in result.stdout + assert result.exit_code == 0 + + # Both -b and --my-help are eager so they're called in the order they're + # invoked by the user. + result = runner.invoke(cli, ["-b", "--my-help"]) + assert "Value of a is: True" not in result.stdout + assert "Value of b is: True" in result.stdout + assert "--my-help" not in result.stdout + assert result.exit_code == 0 + + # But there was a bug when --my-help is called before -b, because the + # --my-help option created by click via help_option_names is internally + # created twice and is not the same object, breaking the priority order + # produced by iter_params_for_processing. + result = runner.invoke(cli, ["--my-help", "-b"]) + assert "Value of a is: True" not in result.stdout + assert "Value of b is: True" not in result.stdout + assert "--my-help" in result.stdout + assert result.exit_code == 0 + + def test_unprocessed_options(runner): @click.command(context_settings=dict(ignore_unknown_options=True)) @click.argument("args", nargs=-1, type=click.UNPROCESSED) diff --git a/tests/test_defaults.py b/tests/test_defaults.py index 8ef5ea2..5c5e168 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -5,7 +5,7 @@ def test_basic_defaults(runner): @click.command() @click.option("--foo", default=42, type=click.FLOAT) def cli(foo): - assert type(foo) is float + assert type(foo) is float # noqa E721 click.echo(f"FOO:[{foo}]") result = runner.invoke(cli, []) @@ -18,7 +18,7 @@ def test_multiple_defaults(runner): @click.option("--foo", default=[23, 42], type=click.FLOAT, multiple=True) def cli(foo): for item in foo: - assert type(item) is float + assert type(item) is float # noqa E721 click.echo(item) result = runner.invoke(cli, []) @@ -59,3 +59,28 @@ def cli(y, f, v): result = runner.invoke(cli, ["-y", "-n", "-f", "-v", "-q"], standalone_mode=False) assert result.return_value == ((True, False), (True,), (1, -1)) + + +def test_flag_default_map(runner): + """test flag with default map""" + + @click.group() + def cli(): + pass + + @cli.command() + @click.option("--name/--no-name", is_flag=True, show_default=True, help="name flag") + def foo(name): + click.echo(name) + + result = runner.invoke(cli, ["foo"]) + assert "False" in result.output + + result = runner.invoke(cli, ["foo", "--help"]) + assert "default: no-name" in result.output + + result = runner.invoke(cli, ["foo"], default_map={"foo": {"name": True}}) + assert "True" in result.output + + result = runner.invoke(cli, ["foo", "--help"], default_map={"foo": {"name": True}}) + assert "default: name" in result.output diff --git a/tests/test_imports.py b/tests/test_imports.py index ec32fca..aaf294e 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -4,7 +4,6 @@ from click._compat import WIN - IMPORT_TEST = b"""\ import builtins @@ -48,6 +47,7 @@ def tracking_import(module, locals=None, globals=None, fromlist=None, "typing", "types", "gettext", + "shutil", } if WIN: diff --git a/tests/test_normalization.py b/tests/test_normalization.py index 32df098..502e654 100644 --- a/tests/test_normalization.py +++ b/tests/test_normalization.py @@ -1,6 +1,5 @@ import click - CONTEXT_SETTINGS = dict(token_normalize_func=lambda x: x.lower()) diff --git a/tests/test_options.py b/tests/test_options.py index 91249b2..7397f36 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -786,6 +786,14 @@ def test_show_default_string(runner): assert "[default: (unlimited)]" in message +def test_show_default_with_empty_string(runner): + """When show_default is True and default is set to an empty string.""" + opt = click.Option(["--limit"], default="", show_default=True) + ctx = click.Context(click.Command("cli")) + message = opt.get_help_record(ctx)[1] + assert '[default: ""]' in message + + def test_do_not_show_no_default(runner): """When show_default is True and no default is set do not show None.""" opt = click.Option(["--limit"], show_default=True) diff --git a/tests/test_testing.py b/tests/test_testing.py index 9f294b3..0d227f2 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -5,7 +5,7 @@ import pytest import click -from click._compat import WIN +from click.exceptions import ClickException from click.testing import CliRunner @@ -184,7 +184,6 @@ def cli(): assert result.exit_code == 1 -@pytest.mark.skipif(WIN, reason="Test does not make sense on Windows.") def test_with_color(): @click.command() def cli(): @@ -201,6 +200,26 @@ def cli(): assert not result.exception +def test_with_color_errors(): + class CLIError(ClickException): + def format_message(self) -> str: + return click.style(self.message, fg="red") + + @click.command() + def cli(): + raise CLIError("Red error") + + runner = CliRunner() + + result = runner.invoke(cli) + assert result.output == "Error: Red error\n" + assert result.exception + + result = runner.invoke(cli, color=True) + assert result.output == f"Error: {click.style('Red error', fg='red')}\n" + assert result.exception + + def test_with_color_but_pause_not_blocking(): @click.command() def cli(): @@ -321,7 +340,7 @@ def cli_stderr(): assert result_mix.stdout == "stdout\nstderr\n" with pytest.raises(ValueError): - result_mix.stderr + result_mix.stderr # noqa B018 @click.command() def cli_empty_stderr(): diff --git a/tests/test_types.py b/tests/test_types.py index 0a25088..79068e1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,5 +1,6 @@ import os.path import pathlib +import platform import tempfile import pytest @@ -232,3 +233,14 @@ def test_file_surrogates(type, tmp_path): def test_file_error_surrogates(): message = FileError(filename="\udcff").format_message() assert message == "Could not open file '�': unknown error" + + +@pytest.mark.skipif( + platform.system() == "Windows", reason="Filepath syntax differences." +) +def test_invalid_path_with_esc_sequence(): + with pytest.raises(click.BadParameter) as exc_info: + with tempfile.TemporaryDirectory(prefix="my\ndir") as tempdir: + click.Path(dir_okay=False).convert(tempdir, None, None) + + assert "my\\ndir" in exc_info.value.message diff --git a/tests/test_utils.py b/tests/test_utils.py index 12709a1..d80aee7 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -36,9 +36,7 @@ def cli(): def test_echo_custom_file(): - import io - - f = io.StringIO() + f = StringIO() click.echo("hello", file=f) assert f.getvalue() == "hello\n" @@ -181,7 +179,6 @@ def _test_gen_func(): yield "abc" -@pytest.mark.skipif(WIN, reason="Different behavior on windows.") @pytest.mark.parametrize("cat", ["cat", "cat ", "cat "]) @pytest.mark.parametrize( "test", @@ -209,7 +206,6 @@ def test_echo_via_pager(monkeypatch, capfd, cat, test): assert out == expected_output -@pytest.mark.skipif(WIN, reason="Test does not make sense on Windows.") def test_echo_color_flag(monkeypatch, capfd): isatty = True monkeypatch.setattr(click._compat, "isatty", lambda x: isatty) @@ -232,16 +228,23 @@ def test_echo_color_flag(monkeypatch, capfd): assert out == f"{styled_text}\n" isatty = False - click.echo(styled_text) - out, err = capfd.readouterr() - assert out == f"{text}\n" + # Faking isatty() is not enough on Windows; + # the implementation caches the colorama wrapped stream + # so we have to use a new stream for each test + stream = StringIO() + click.echo(styled_text, file=stream) + assert stream.getvalue() == f"{text}\n" + + stream = StringIO() + click.echo(styled_text, file=stream, color=True) + assert stream.getvalue() == f"{styled_text}\n" def test_prompt_cast_default(capfd, monkeypatch): monkeypatch.setattr(sys, "stdin", StringIO("\n")) value = click.prompt("value", default="100", type=int) capfd.readouterr() - assert type(value) is int + assert type(value) is int # noqa E721 @pytest.mark.skipif(WIN, reason="Test too complex to make work windows.") @@ -464,7 +467,7 @@ def test_expand_args(monkeypatch): assert user in click.utils._expand_args(["~"]) monkeypatch.setenv("CLICK_TEST", "hello") assert "hello" in click.utils._expand_args(["$CLICK_TEST"]) - assert "setup.cfg" in click.utils._expand_args(["*.cfg"]) + assert "pyproject.toml" in click.utils._expand_args(["*.toml"]) assert os.path.join("docs", "conf.py") in click.utils._expand_args(["**/conf.py"]) assert "*.not-found" in click.utils._expand_args(["*.not-found"]) # a bad glob pattern, such as a pytest identifier, should return itself diff --git a/tests/typing/typing_aliased_group.py b/tests/typing/typing_aliased_group.py index ccc706b..a1fdac4 100644 --- a/tests/typing/typing_aliased_group.py +++ b/tests/typing/typing_aliased_group.py @@ -1,4 +1,5 @@ """Example from https://click.palletsprojects.com/en/8.1.x/advanced/#command-aliases""" + from __future__ import annotations from typing_extensions import assert_type diff --git a/tests/typing/typing_confirmation_option.py b/tests/typing/typing_confirmation_option.py index 09ecaa9..a568a6a 100644 --- a/tests/typing/typing_confirmation_option.py +++ b/tests/typing/typing_confirmation_option.py @@ -1,4 +1,5 @@ """From https://click.palletsprojects.com/en/8.1.x/options/#yes-parameters""" + from typing_extensions import assert_type import click diff --git a/tests/typing/typing_options.py b/tests/typing/typing_options.py index 6a66119..613e8d4 100644 --- a/tests/typing/typing_options.py +++ b/tests/typing/typing_options.py @@ -1,4 +1,5 @@ """From https://click.palletsprojects.com/en/8.1.x/quickstart/#adding-parameters""" + from typing_extensions import assert_type import click diff --git a/tests/typing/typing_simple_example.py b/tests/typing/typing_simple_example.py index 0a94b8c..641937c 100644 --- a/tests/typing/typing_simple_example.py +++ b/tests/typing/typing_simple_example.py @@ -1,4 +1,5 @@ """The simple example from https://github.com/pallets/click#a-simple-example.""" + from typing_extensions import assert_type import click diff --git a/tests/typing/typing_version_option.py b/tests/typing/typing_version_option.py index fd473a0..7ff37a7 100644 --- a/tests/typing/typing_version_option.py +++ b/tests/typing/typing_version_option.py @@ -1,6 +1,7 @@ """ From https://click.palletsprojects.com/en/8.1.x/options/#callbacks-and-eager-options. """ + from typing_extensions import assert_type import click diff --git a/tox.ini b/tox.ini index df53602..84ef184 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py3{12,11,10,9,8,7} + py3{13,12,11,10,9,8,7} pypy310 style typing @@ -10,9 +10,14 @@ skip_missing_interpreters = true [testenv] package = wheel wheel_build_env = .pkg +constrain_package_deps = true +use_frozen_constraints = true deps = -r requirements/tests.txt commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs} +[testenv:py37,py3.7] +deps = -r requirements/tests37.txt + [testenv:style] deps = pre-commit skip_install = true @@ -22,9 +27,40 @@ commands = pre-commit run --all-files deps = -r requirements/typing.txt commands = mypy - pyright --verifytypes click pyright tests/typing + pyright --verifytypes click --ignoreexternal [testenv:docs] deps = -r requirements/docs.txt -commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html +commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml + +[testenv:update-actions] +labels = update +deps = gha-update +commands = gha-update + +[testenv:update-pre_commit] +labels = update +deps = pre-commit +skip_install = true +commands = pre-commit autoupdate -j4 + +[testenv:update-requirements] +labels = update +deps = pip-tools +skip_install = true +change_dir = requirements +commands = + pip-compile build.in -q {posargs:-U} + pip-compile docs.in -q {posargs:-U} + pip-compile tests.in -q {posargs:-U} + pip-compile typing.in -q {posargs:-U} + pip-compile dev.in -q {posargs:-U} + +[testenv:update-requirements37] +base_python = 3.7 +labels = update +deps = pip-tools +skip_install = true +change_dir = requirements +commands = pip-compile tests.in -q -o tests37.txt {posargs:-U}