New upstream version 8.1.3

This commit is contained in:
Carsten Schoenert 2022-11-30 09:52:01 +01:00
parent 4555a76448
commit e3834772a6
45 changed files with 1097 additions and 775 deletions

View file

@ -1,8 +1,9 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: pip - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: monthly interval: "monthly"
time: "08:00" day: "monday"
open-pull-requests-limit: 99 time: "16:00"
timezone: "UTC"

View file

@ -8,8 +8,8 @@ jobs:
lock: lock:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v2 - uses: dessant/lock-threads@v3
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-lock-inactive-days: 14 issue-inactive-days: 14
pr-lock-inactive-days: 14 pr-inactive-days: 14

View file

@ -24,32 +24,27 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39} - {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
- {name: Windows, python: '3.9', os: windows-latest, tox: py39} - {name: Windows, python: '3.10', os: windows-latest, tox: py310}
- {name: Mac, python: '3.9', os: macos-latest, tox: py39} - {name: Mac, python: '3.10', os: macos-latest, tox: py310}
- {name: '3.11-dev', python: '3.11-dev', os: ubuntu-latest, tox: py311}
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
- {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37} - {name: '3.7', python: '3.7', os: ubuntu-latest, tox: py37}
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36} - {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3} - {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
cache: 'pip'
cache-dependency-path: 'requirements/*.txt'
- name: update pip - name: update pip
run: | run: |
pip install -U wheel pip install -U wheel
pip install -U setuptools pip install -U setuptools
python -m pip install -U pip python -m pip install -U pip
- name: get pip cache dir
id: pip-cache
run: echo "::set-output name=dir::$(pip cache dir)"
- name: cache pip
uses: actions/cache@v2
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}|${{ hashFiles('requirements/*.txt') }}
- name: cache mypy - name: cache mypy
uses: actions/cache@v2 uses: actions/cache@v2
with: with:

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
/.idea/ /.idea/
/.vscode/ /.vscode/
.DS_Store/
/env/ /env/
/venv/ /venv/
__pycache__/ __pycache__/

View file

@ -1,29 +1,31 @@
ci: ci:
autoupdate_branch: "8.1.x"
autoupdate_schedule: monthly autoupdate_schedule: monthly
repos: repos:
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.29.0 rev: v2.32.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: ["--py36-plus"] args: ["--py37-plus"]
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder_python_imports
rev: v2.6.0 rev: v3.1.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ["--application-directories", "src"] args: ["--application-directories", "src"]
additional_dependencies: ["setuptools>60.9"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 21.9b0 rev: 22.3.0
hooks: hooks:
- id: black - id: black
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 3.9.2 rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: additional_dependencies:
- flake8-bugbear - flake8-bugbear
- flake8-implicit-str-concat - flake8-implicit-str-concat
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1 rev: v4.2.0
hooks: hooks:
- id: fix-byte-order-marker - id: fix-byte-order-marker
- id: trailing-whitespace - id: trailing-whitespace

View file

@ -1,4 +1,8 @@
version: 2 version: 2
build:
os: ubuntu-20.04
tools:
python: "3.10"
python: python:
install: install:
- requirements: requirements/docs.txt - requirements: requirements/docs.txt

View file

@ -1,5 +1,115 @@
.. currentmodule:: click .. currentmodule:: click
Version 8.1.3
-------------
Released 2022-04-28
- Use verbose form of ``typing.Callable`` for ``@command`` and
``@group``. :issue:`2255`
- Show error when attempting to create an option with
``multiple=True, is_flag=True``. Use ``count`` instead.
:issue:`2246`
Version 8.1.2
-------------
Released 2022-03-31
- Fix error message for readable path check that was mixed up with the
executable check. :pr:`2236`
- Restore parameter order for ``Path``, placing the ``executable``
parameter at the end. It is recommended to use keyword arguments
instead of positional arguments. :issue:`2235`
Version 8.1.1
-------------
Released 2022-03-30
- Fix an issue with decorator typing that caused type checking to
report that a command was not callable. :issue:`2227`
Version 8.1.0
-------------
Released 2022-03-28
- Drop support for Python 3.6. :pr:`2129`
- Remove previously deprecated code. :pr:`2130`
- ``Group.resultcallback`` is renamed to ``result_callback``.
- ``autocompletion`` parameter to ``Command`` is renamed to
``shell_complete``.
- ``get_terminal_size`` is removed, use
``shutil.get_terminal_size`` instead.
- ``get_os_args`` is removed, use ``sys.argv[1:]`` instead.
- Rely on :pep:`538` and :pep:`540` to handle selecting UTF-8 encoding
instead of ASCII. Click's locale encoding detection is removed.
:issue:`2198`
- Single options boolean flags with ``show_default=True`` only show
the default if it is ``True``. :issue:`1971`
- The ``command`` and ``group`` decorators can be applied with or
without parentheses. :issue:`1359`
- The ``Path`` type can check whether the target is executable.
:issue:`1961`
- ``Command.show_default`` overrides ``Context.show_default``, instead
of the other way around. :issue:`1963`
- Parameter decorators and ``@group`` handles ``cls=None`` the same as
not passing ``cls``. ``@option`` handles ``help=None`` the same as
not passing ``help``. :issue:`#1959`
- A flag option with ``required=True`` requires that the flag is
passed instead of choosing the implicit default value. :issue:`1978`
- Indentation in help text passed to ``Option`` and ``Command`` is
cleaned the same as using the ``@option`` and ``@command``
decorators does. A command's ``epilog`` and ``short_help`` are also
processed. :issue:`1985`
- Store unprocessed ``Command.help``, ``epilog`` and ``short_help``
strings. Processing is only done when formatting help text for
output. :issue:`2149`
- Allow empty str input for ``prompt()`` when
``confirmation_prompt=True`` and ``default=""``. :issue:`2157`
- Windows glob pattern expansion doesn't fail if a value is an invalid
pattern. :issue:`2195`
- It's possible to pass a list of ``params`` to ``@command``. Any
params defined with decorators are appended to the passed params.
:issue:`2131`.
- ``@command`` decorator is annotated as returning the correct type if
a ``cls`` argument is used. :issue:`2211`
- A ``Group`` with ``invoke_without_command=True`` and ``chain=False``
will invoke its result callback with the group function's return
value. :issue:`2124`
- ``to_info_dict`` will not fail if a ``ParamType`` doesn't define a
``name``. :issue:`2168`
- Shell completion prioritizes option values with option prefixes over
new options. :issue:`2040`
- Options that get an environment variable value using
``autoenvvar_prefix`` treat an empty value as ``None``, consistent
with a direct ``envvar``. :issue:`2146`
Version 8.0.4
-------------
Released 2022-02-18
- ``open_file`` recognizes ``Path("-")`` as a standard stream, the
same as the string ``"-"``. :issue:`2106`
- The ``option`` and ``argument`` decorators preserve the type
annotation of the decorated function. :pr:`2155`
- A callable default value can customize its help text by overriding
``__str__`` instead of always showing ``(dynamic)``. :issue:`2099`
- Fix a typo in the Bash completion script that affected file and
directory completion. If this script was generated by a previous
version, it should be regenerated. :issue:`2163`
- Fix typing for ``echo`` and ``secho`` file argument.
:issue:`2174, 2185`
Version 8.0.3 Version 8.0.3
------------- -------------

BIN
docs/.DS_Store vendored

Binary file not shown.

View file

@ -63,8 +63,6 @@ Utilities
.. autofunction:: pause .. autofunction:: pause
.. autofunction:: get_terminal_size
.. autofunction:: get_binary_stream .. autofunction:: get_binary_stream
.. autofunction:: get_text_stream .. autofunction:: get_text_stream

View file

@ -109,6 +109,27 @@ To show the default values when showing command help, use ``show_default=True``
invoke(dots, args=['--help']) invoke(dots, args=['--help'])
For single option boolean flags, the default remains hidden if the default
value is False.
.. click:example::
@click.command()
@click.option('--n', default=1, show_default=True)
@click.option("--gr", is_flag=True, show_default=True, default=False, help="Greet the world.")
@click.option("--br", is_flag=True, show_default=True, default=True, help="Add a thematic break")
def dots(n, gr, br):
if gr:
click.echo('Hello world!')
click.echo('.' * n)
if br:
click.echo('-' * n)
.. click:run::
invoke(dots, args=['--help'])
Multi Value Options Multi Value Options
------------------- -------------------
@ -538,10 +559,10 @@ parameter ``--foo`` was required and defined before, you would need to
specify it for ``--version`` to work. For more information, see specify it for ``--version`` to work. For more information, see
:ref:`callback-evaluation-order`. :ref:`callback-evaluation-order`.
A callback is a function that is invoked with two parameters: the current A callback is a function that is invoked with three parameters: the
:class:`Context` and the value. The context provides some useful features current :class:`Context`, the current :class:`Parameter`, and the value.
such as quitting the application and gives access to other already The context provides some useful features such as quitting the
processed parameters. application and gives access to other already processed parameters.
Here an example for a ``--version`` flag: Here an example for a ``--version`` flag:

View file

@ -163,7 +163,7 @@ functional at least on a basic level even if everything is completely
broken. broken.
What this means is that the :func:`echo` function applies some error What this means is that the :func:`echo` function applies some error
correction in case the terminal is misconfigured instead of dying with an correction in case the terminal is misconfigured instead of dying with a
:exc:`UnicodeError`. :exc:`UnicodeError`.
The echo function also supports color and other styles in output. It The echo function also supports color and other styles in output. It

View file

@ -132,6 +132,8 @@ with the incomplete value.
.. code-block:: python .. code-block:: python
class EnvVarType(ParamType): class EnvVarType(ParamType):
name = "envvar"
def shell_complete(self, ctx, param, incomplete): def shell_complete(self, ctx, param, incomplete):
return [ return [
CompletionItem(name) CompletionItem(name)

View file

@ -9,4 +9,4 @@ Click Examples
through the wrong interpreter. through the wrong interpreter.
For more information about this see the documentation: For more information about this see the documentation:
https://click.palletsprojects.com/en/7.x/setuptools/ https://click.palletsprojects.com/setuptools/

View file

@ -1,6 +1,6 @@
-r docs.in -r docs.in
-r tests.in -r tests.in
-r typing.in -r typing.in
pip-tools pip-compile-multi
pre-commit pre-commit
tox tox

View file

@ -1,141 +1,58 @@
# SHA1:54b5b77ec8c7a0064ffa93b2fd16cb0130ba177c
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
# #
# pip-compile requirements/dev.in # pip-compile-multi
# #
alabaster==0.7.12 -r docs.txt
# via sphinx -r tests.txt
attrs==21.2.0 -r typing.txt
# via pytest
babel==2.9.1
# via sphinx
backports.entry-points-selectable==1.1.0
# via virtualenv
certifi==2021.5.30
# via requests
cfgv==3.3.1 cfgv==3.3.1
# via pre-commit # via pre-commit
charset-normalizer==2.0.6 click==8.1.2
# via requests
click==8.0.1
# via pip-tools
distlib==0.3.3
# via virtualenv
docutils==0.16
# via # via
# sphinx # pip-compile-multi
# sphinx-tabs # pip-tools
filelock==3.3.0 distlib==0.3.4
# via virtualenv
filelock==3.6.0
# via # via
# tox # tox
# virtualenv # virtualenv
identify==2.3.0 identify==2.5.0
# via pre-commit # via pre-commit
idna==3.2
# via requests
imagesize==1.2.0
# via sphinx
iniconfig==1.1.1
# via pytest
jinja2==3.0.2
# via sphinx
markupsafe==2.0.1
# via jinja2
mypy-extensions==0.4.3
# via mypy
mypy==0.910
# via -r requirements/typing.in
nodeenv==1.6.0 nodeenv==1.6.0
# via pre-commit # via pre-commit
packaging==21.0 pep517==0.12.0
# via
# pallets-sphinx-themes
# pytest
# sphinx
# tox
pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in
pep517==0.11.0
# via pip-tools # via pip-tools
pip-tools==6.3.0 pip-compile-multi==2.4.5
# via -r requirements/dev.in # via -r requirements/dev.in
platformdirs==2.4.0 pip-tools==6.6.0
# via pip-compile-multi
platformdirs==2.5.2
# via virtualenv # via virtualenv
pluggy==1.0.0 pre-commit==2.18.1
# via
# pytest
# tox
pre-commit==2.15.0
# via -r requirements/dev.in # via -r requirements/dev.in
py==1.10.0 pyyaml==6.0
# via
# pytest
# tox
pygments==2.10.0
# via
# sphinx
# sphinx-tabs
pyparsing==2.4.7
# via packaging
pytest==6.2.5
# via -r requirements/tests.in
pytz==2021.3
# via babel
pyyaml==5.4.1
# via pre-commit # via pre-commit
requests==2.26.0
# via sphinx
six==1.16.0 six==1.16.0
# via # via
# tox # tox
# virtualenv # virtualenv
snowballstemmer==2.1.0
# via sphinx
sphinx-issues==1.2.0
# via -r requirements/docs.in
sphinx-tabs==3.2.0
# via -r requirements/docs.in
sphinx==4.2.0
# via
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-tabs
# sphinxcontrib-log-cabinet
sphinxcontrib-applehelp==1.0.2
# via sphinx
sphinxcontrib-devhelp==1.0.2
# via sphinx
sphinxcontrib-htmlhelp==2.0.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 sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
toml==0.10.2 toml==0.10.2
# via # via
# mypy
# pre-commit # pre-commit
# pytest
# tox # tox
tomli==1.2.1 toposort==1.7
# via pep517 # via pip-compile-multi
tox==3.24.4 tox==3.25.0
# via -r requirements/dev.in # via -r requirements/dev.in
typing-extensions==3.10.0.2 virtualenv==20.14.1
# via mypy
urllib3==1.26.7
# via requests
virtualenv==20.8.1
# via # via
# pre-commit # pre-commit
# tox # tox
wheel==0.37.0 wheel==0.37.1
# via pip-tools # via pip-tools
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View file

@ -1,58 +1,59 @@
# SHA1:34fd4ca6516e97c7348e6facdd9c4ebb68209d1c
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
# #
# pip-compile requirements/docs.in # pip-compile-multi
# #
alabaster==0.7.12 alabaster==0.7.12
# via sphinx # via sphinx
babel==2.9.1 babel==2.10.1
# via sphinx # via sphinx
certifi==2021.5.30 certifi==2021.10.8
# via requests # via requests
charset-normalizer==2.0.6 charset-normalizer==2.0.12
# via requests # via requests
docutils==0.16 docutils==0.17.1
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
idna==3.2 idna==3.3
# via requests # via requests
imagesize==1.2.0 imagesize==1.3.0
# via sphinx # via sphinx
jinja2==3.0.2 jinja2==3.1.2
# via sphinx # via sphinx
markupsafe==2.0.1 markupsafe==2.1.1
# via jinja2 # via jinja2
packaging==21.0 packaging==21.3
# via # via
# pallets-sphinx-themes # pallets-sphinx-themes
# sphinx # sphinx
pallets-sphinx-themes==2.0.1 pallets-sphinx-themes==2.0.2
# via -r requirements/docs.in # via -r requirements/docs.in
pygments==2.10.0 pygments==2.12.0
# via # via
# sphinx # sphinx
# sphinx-tabs # sphinx-tabs
pyparsing==2.4.7 pyparsing==3.0.8
# via packaging # via packaging
pytz==2021.3 pytz==2022.1
# via babel # via babel
requests==2.26.0 requests==2.27.1
# via sphinx # via sphinx
snowballstemmer==2.1.0 snowballstemmer==2.2.0
# via sphinx # via sphinx
sphinx-issues==1.2.0 sphinx==4.5.0
# via -r requirements/docs.in
sphinx-tabs==3.2.0
# via -r requirements/docs.in
sphinx==4.2.0
# via # via
# -r requirements/docs.in # -r requirements/docs.in
# pallets-sphinx-themes # pallets-sphinx-themes
# sphinx-issues # sphinx-issues
# sphinx-tabs # sphinx-tabs
# sphinxcontrib-log-cabinet # sphinxcontrib-log-cabinet
sphinx-issues==3.0.1
# via -r requirements/docs.in
sphinx-tabs==3.3.1
# via -r requirements/docs.in
sphinxcontrib-applehelp==1.0.2 sphinxcontrib-applehelp==1.0.2
# via sphinx # via sphinx
sphinxcontrib-devhelp==1.0.2 sphinxcontrib-devhelp==1.0.2
@ -67,8 +68,5 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx # via sphinx
sphinxcontrib-serializinghtml==1.1.5 sphinxcontrib-serializinghtml==1.1.5
# via sphinx # via sphinx
urllib3==1.26.7 urllib3==1.26.9
# via requests # via requests
# The following packages are considered to be unsafe in a requirements file:
# setuptools

View file

@ -1,22 +1,23 @@
# SHA1:0eaa389e1fdb3a1917c0f987514bd561be5718ee
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
# #
# pip-compile requirements/tests.in # pip-compile-multi
# #
attrs==21.2.0 attrs==21.4.0
# via pytest # via pytest
iniconfig==1.1.1 iniconfig==1.1.1
# via pytest # via pytest
packaging==21.0 packaging==21.3
# via pytest # via pytest
pluggy==1.0.0 pluggy==1.0.0
# via pytest # via pytest
py==1.10.0 py==1.11.0
# via pytest # via pytest
pyparsing==2.4.7 pyparsing==3.0.8
# via packaging # via packaging
pytest==6.2.5 pytest==7.1.2
# via -r requirements/tests.in # via -r requirements/tests.in
toml==0.10.2 tomli==2.0.1
# via pytest # via pytest

View file

@ -1,14 +1,15 @@
# SHA1:7983aaa01d64547827c20395d77e248c41b2572f
# #
# This file is autogenerated by pip-compile # This file is autogenerated by pip-compile-multi
# To update, run: # To update, run:
# #
# pip-compile requirements/typing.in # pip-compile-multi
# #
mypy==0.950
# via -r requirements/typing.in
mypy-extensions==0.4.3 mypy-extensions==0.4.3
# via mypy # via mypy
mypy==0.910 tomli==2.0.1
# via -r requirements/typing.in
toml==0.10.2
# via mypy # via mypy
typing-extensions==3.10.0.2 typing-extensions==4.2.0
# via mypy # via mypy

View file

@ -29,8 +29,8 @@ classifiers =
[options] [options]
packages = find: packages = find:
package_dir = = src package_dir = = src
include_package_data = true include_package_data = True
python_requires = >= 3.6 python_requires = >= 3.7
# Dependencies are in setup.py for GitHub's dependency graph. # Dependencies are in setup.py for GitHub's dependency graph.
[options.packages.find] [options.packages.find]
@ -42,14 +42,14 @@ filterwarnings =
error error
[coverage:run] [coverage:run]
branch = true branch = True
source = source =
click click
tests tests
[coverage:paths] [coverage:paths]
source = source =
click src
*/site-packages */site-packages
[flake8] [flake8]
@ -57,7 +57,7 @@ source =
# E = pycodestyle errors # E = pycodestyle errors
# F = flake8 pyflakes # F = flake8 pyflakes
# W = pycodestyle warnings # W = pycodestyle warnings
# B9 = bugbear opinions, # B9 = bugbear opinions
# ISC = implicit str concat # ISC = implicit str concat
select = B, E, F, W, B9, ISC select = B, E, F, W, B9, ISC
ignore = ignore =
@ -72,12 +72,13 @@ ignore =
# up to 88 allowed by bugbear B950 # up to 88 allowed by bugbear B950
max-line-length = 80 max-line-length = 80
per-file-ignores = per-file-ignores =
# __init__ module exports names # __init__ exports names
src/click/__init__.py: F401 src/click/__init__.py: F401
[mypy] [mypy]
files = src/click files = src/click
python_version = 3.6 python_version = 3.7
show_error_codes = True
disallow_subclassing_any = True disallow_subclassing_any = True
disallow_untyped_calls = True disallow_untyped_calls = True
disallow_untyped_defs = True disallow_untyped_defs = True

View file

@ -41,7 +41,6 @@ from .termui import clear as clear
from .termui import confirm as confirm from .termui import confirm as confirm
from .termui import echo_via_pager as echo_via_pager from .termui import echo_via_pager as echo_via_pager
from .termui import edit as edit from .termui import edit as edit
from .termui import get_terminal_size as get_terminal_size
from .termui import getchar as getchar from .termui import getchar as getchar
from .termui import launch as launch from .termui import launch as launch
from .termui import pause as pause from .termui import pause as pause
@ -68,8 +67,7 @@ from .utils import echo as echo
from .utils import format_filename as format_filename from .utils import format_filename as format_filename
from .utils import get_app_dir as get_app_dir from .utils import get_app_dir as get_app_dir
from .utils import get_binary_stream as get_binary_stream from .utils import get_binary_stream as get_binary_stream
from .utils import get_os_args as get_os_args
from .utils import get_text_stream as get_text_stream from .utils import get_text_stream as get_text_stream
from .utils import open_file as open_file from .utils import open_file as open_file
__version__ = "8.0.3" __version__ = "8.1.3"

View file

@ -388,9 +388,9 @@ def open_stream(
) -> t.Tuple[t.IO, bool]: ) -> t.Tuple[t.IO, bool]:
binary = "b" in mode binary = "b" in mode
# Standard streams first. These are simple because they don't need # Standard streams first. These are simple because they ignore the
# special handling for the atomic flag. It's entirely ignored. # atomic flag. Use fsdecode to handle Path("-").
if filename == "-": if os.fsdecode(filename) == "-":
if any(m in mode for m in ["w", "a", "x"]): if any(m in mode for m in ["w", "a", "x"]):
if binary: if binary:
return get_binary_stdout(), False return get_binary_stdout(), False
@ -561,7 +561,6 @@ if sys.platform.startswith("win") and WIN:
return rv return rv
else: else:
def _get_argv_encoding() -> str: def _get_argv_encoding() -> str:

View file

@ -675,7 +675,6 @@ if WIN:
_translate_ch_to_exc(rv) _translate_ch_to_exc(rv)
return rv return rv
else: else:
import tty import tty
import termios import termios

View file

@ -1,100 +0,0 @@
import codecs
import os
from gettext import gettext as _
def _verify_python_env() -> None:
"""Ensures that the environment is good for Unicode."""
try:
from locale import getpreferredencoding
fs_enc = codecs.lookup(getpreferredencoding()).name
except Exception:
fs_enc = "ascii"
if fs_enc != "ascii":
return
extra = [
_(
"Click will abort further execution because Python was"
" configured to use ASCII as encoding for the environment."
" Consult https://click.palletsprojects.com/unicode-support/"
" for mitigation steps."
)
]
if os.name == "posix":
import subprocess
try:
rv = subprocess.Popen(
["locale", "-a"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="ascii",
errors="replace",
).communicate()[0]
except OSError:
rv = ""
good_locales = set()
has_c_utf8 = False
for line in rv.splitlines():
locale = line.strip()
if locale.lower().endswith((".utf-8", ".utf8")):
good_locales.add(locale)
if locale.lower() in ("c.utf8", "c.utf-8"):
has_c_utf8 = True
if not good_locales:
extra.append(
_(
"Additional information: on this system no suitable"
" UTF-8 locales were discovered. This most likely"
" requires resolving by reconfiguring the locale"
" system."
)
)
elif has_c_utf8:
extra.append(
_(
"This system supports the C.UTF-8 locale which is"
" recommended. You might be able to resolve your"
" issue by exporting the following environment"
" variables:"
)
)
extra.append(" export LC_ALL=C.UTF-8\n export LANG=C.UTF-8")
else:
extra.append(
_(
"This system lists some UTF-8 supporting locales"
" that you can pick from. The following suitable"
" locales were discovered: {locales}"
).format(locales=", ".join(sorted(good_locales)))
)
bad_locale = None
for env_locale in os.environ.get("LC_ALL"), os.environ.get("LANG"):
if env_locale and env_locale.lower().endswith((".utf-8", ".utf8")):
bad_locale = env_locale
if env_locale is not None:
break
if bad_locale is not None:
extra.append(
_(
"Click discovered that you exported a UTF-8 locale"
" but the locale system could not pick up from it"
" because it does not exist. The exported locale is"
" {locale!r} but it is not supported."
).format(locale=bad_locale)
)
raise RuntimeError("\n\n".join(extra))

View file

@ -1,8 +1,8 @@
import enum import enum
import errno import errno
import inspect
import os import os
import sys import sys
import typing
import typing as t import typing as t
from collections import abc from collections import abc
from contextlib import contextmanager from contextlib import contextmanager
@ -14,7 +14,6 @@ from gettext import ngettext
from itertools import repeat from itertools import repeat
from . import types from . import types
from ._unicodefun import _verify_python_env
from .exceptions import Abort from .exceptions import Abort
from .exceptions import BadParameter from .exceptions import BadParameter
from .exceptions import ClickException from .exceptions import ClickException
@ -224,9 +223,14 @@ class Context:
codes are used in texts that Click prints which is by codes are used in texts that Click prints which is by
default not the case. This for instance would affect default not the case. This for instance would affect
help output. help output.
:param show_default: Show defaults for all options. If not set, :param show_default: Show the default value for commands. If this
defaults to the value from a parent context. Overrides an value is not set, it defaults to the value from the parent
option's ``show_default`` argument. context. ``Command.show_default`` overrides this default for the
specific command.
.. versionchanged:: 8.1
The ``show_default`` parameter is overridden by
``Command.show_default``, instead of the other way around.
.. versionchanged:: 8.0 .. versionchanged:: 8.0
The ``show_default`` parameter defaults to the value from the The ``show_default`` parameter defaults to the value from the
@ -288,6 +292,8 @@ class Context:
#: must be never propagated to another arguments. This is used #: must be never propagated to another arguments. This is used
#: to implement nested parsing. #: to implement nested parsing.
self.protected_args: t.List[str] = [] self.protected_args: t.List[str] = []
#: the collected prefixes of the command's options.
self._opt_prefixes: t.Set[str] = set(parent._opt_prefixes) if parent else set()
if obj is None and parent is not None: if obj is None and parent is not None:
obj = parent.obj obj = parent.obj
@ -632,13 +638,13 @@ class Context:
self.obj = rv = object_type() self.obj = rv = object_type()
return rv return rv
@typing.overload @t.overload
def lookup_default( def lookup_default(
self, name: str, call: "te.Literal[True]" = True self, name: str, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]: ) -> t.Optional[t.Any]:
... ...
@typing.overload @t.overload
def lookup_default( def lookup_default(
self, name: str, call: "te.Literal[False]" = ... 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]]]:
@ -956,7 +962,7 @@ class BaseCommand:
return results return results
@typing.overload @t.overload
def main( def main(
self, self,
args: t.Optional[t.Sequence[str]] = None, args: t.Optional[t.Sequence[str]] = None,
@ -967,7 +973,7 @@ class BaseCommand:
) -> "te.NoReturn": ) -> "te.NoReturn":
... ...
@typing.overload @t.overload
def main( def main(
self, self,
args: t.Optional[t.Sequence[str]] = None, args: t.Optional[t.Sequence[str]] = None,
@ -1029,10 +1035,6 @@ class BaseCommand:
.. versionchanged:: 3.0 .. versionchanged:: 3.0
Added the ``standalone_mode`` parameter. Added the ``standalone_mode`` parameter.
""" """
# Verify that the environment is configured correctly, or reject
# further execution to avoid a broken script.
_verify_python_env()
if args is None: if args is None:
args = sys.argv[1:] args = sys.argv[1:]
@ -1133,13 +1135,6 @@ class Command(BaseCommand):
Click. A basic command handles command line parsing and might dispatch Click. A basic command handles command line parsing and might dispatch
more parsing to commands nested below it. more parsing to commands nested below it.
.. versionchanged:: 2.0
Added the `context_settings` parameter.
.. versionchanged:: 8.0
Added repr showing the command name
.. versionchanged:: 7.1
Added the `no_args_is_help` parameter.
:param name: the name of the command to use unless a group overrides it. :param name: the name of the command to use unless a group overrides it.
:param context_settings: an optional dictionary with defaults that are :param context_settings: an optional dictionary with defaults that are
passed to the context object. passed to the context object.
@ -1161,6 +1156,20 @@ class Command(BaseCommand):
:param deprecated: issues a message indicating that :param deprecated: issues a message indicating that
the command is deprecated. the command is deprecated.
.. versionchanged:: 8.1
``help``, ``epilog``, and ``short_help`` are stored unprocessed,
all formatting is done when outputting help text, not at init,
and is done even if not using the ``@command`` decorator.
.. versionchanged:: 8.0
Added a ``repr`` showing the command name.
.. versionchanged:: 7.1
Added the ``no_args_is_help`` parameter.
.. versionchanged:: 2.0
Added the ``context_settings`` parameter.
""" """
def __init__( def __init__(
@ -1186,12 +1195,6 @@ class Command(BaseCommand):
#: should show up in the help page and execute. Eager parameters #: should show up in the help page and execute. Eager parameters
#: will automatically be handled before non eager ones. #: will automatically be handled before non eager ones.
self.params: t.List["Parameter"] = params or [] self.params: t.List["Parameter"] = params or []
# if a form feed (page break) is found in the help text, truncate help
# text to the content preceding the first form feed
if help and "\f" in help:
help = help.split("\f", 1)[0]
self.help = help self.help = help
self.epilog = epilog self.epilog = epilog
self.options_metavar = options_metavar self.options_metavar = options_metavar
@ -1299,10 +1302,12 @@ class Command(BaseCommand):
"""Gets short help for the command or makes it by shortening the """Gets short help for the command or makes it by shortening the
long help string. long help string.
""" """
text = self.short_help or "" if self.short_help:
text = inspect.cleandoc(self.short_help)
if not text and self.help: elif self.help:
text = make_default_short_help(self.help, limit) text = make_default_short_help(self.help, limit)
else:
text = ""
if self.deprecated: if self.deprecated:
text = _("(Deprecated) {text}").format(text=text) text = _("(Deprecated) {text}").format(text=text)
@ -1328,12 +1333,13 @@ class Command(BaseCommand):
def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the help text to the formatter if it exists.""" """Writes the help text to the formatter if it exists."""
text = self.help or "" text = self.help if self.help is not None else ""
if self.deprecated: if self.deprecated:
text = _("(Deprecated) {text}").format(text=text) text = _("(Deprecated) {text}").format(text=text)
if text: if text:
text = inspect.cleandoc(text).partition("\f")[0]
formatter.write_paragraph() formatter.write_paragraph()
with formatter.indentation(): with formatter.indentation():
@ -1354,9 +1360,11 @@ class Command(BaseCommand):
def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the epilog into the formatter if it exists.""" """Writes the epilog into the formatter if it exists."""
if self.epilog: if self.epilog:
epilog = inspect.cleandoc(self.epilog)
formatter.write_paragraph() formatter.write_paragraph()
with formatter.indentation(): with formatter.indentation():
formatter.write_text(self.epilog) formatter.write_text(epilog)
def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]: def parse_args(self, ctx: Context, args: t.List[str]) -> t.List[str]:
if not args and self.no_args_is_help and not ctx.resilient_parsing: if not args and self.no_args_is_help and not ctx.resilient_parsing:
@ -1379,6 +1387,7 @@ class Command(BaseCommand):
) )
ctx.args = args ctx.args = args
ctx._opt_prefixes.update(parser._opt_prefixes)
return args return args
def invoke(self, ctx: Context) -> t.Any: def invoke(self, ctx: Context) -> t.Any:
@ -1568,17 +1577,6 @@ class MultiCommand(Command):
return decorator return decorator
def resultcallback(self, replace: bool = False) -> t.Callable[[F], F]:
import warnings
warnings.warn(
"'resultcallback' has been renamed to 'result_callback'."
" The old name will be removed in Click 8.1.",
DeprecationWarning,
stacklevel=2,
)
return self.result_callback(replace=replace)
def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Extra format methods for multi methods that adds all the commands """Extra format methods for multi methods that adds all the commands
after the options. after the options.
@ -1631,11 +1629,11 @@ class MultiCommand(Command):
if not ctx.protected_args: if not ctx.protected_args:
if self.invoke_without_command: if self.invoke_without_command:
# No subcommand was invoked, so the result callback is # No subcommand was invoked, so the result callback is
# invoked with None for regular groups, or an empty list # invoked with the group return value for regular
# for chained groups. # groups, or an empty list for chained groups.
with ctx: with ctx:
super().invoke(ctx) rv = super().invoke(ctx)
return _process_result([] if self.chain else None) return _process_result([] if self.chain else rv)
ctx.fail(_("Missing command.")) ctx.fail(_("Missing command."))
# Fetch args back out # Fetch args back out
@ -1811,9 +1809,19 @@ class Group(MultiCommand):
_check_multicommand(self, name, cmd, register=True) _check_multicommand(self, name, cmd, register=True)
self.commands[name] = cmd self.commands[name] = cmd
@t.overload
def command(self, __func: t.Callable[..., t.Any]) -> Command:
...
@t.overload
def command( def command(
self, *args: t.Any, **kwargs: t.Any 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
) -> t.Union[t.Callable[[t.Callable[..., t.Any]], Command], Command]:
"""A shortcut decorator for declaring and attaching a command to """A shortcut decorator for declaring and attaching a command to
the group. This takes the same arguments as :func:`command` and the group. This takes the same arguments as :func:`command` and
immediately registers the created command with this group by immediately registers the created command with this group by
@ -1822,24 +1830,49 @@ class Group(MultiCommand):
To customize the command class used, set the To customize the command class used, set the
:attr:`command_class` attribute. :attr:`command_class` attribute.
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
.. versionchanged:: 8.0 .. versionchanged:: 8.0
Added the :attr:`command_class` attribute. Added the :attr:`command_class` attribute.
""" """
from .decorators import command from .decorators import command
if self.command_class is not None and "cls" not in kwargs: if self.command_class and kwargs.get("cls") is None:
kwargs["cls"] = self.command_class kwargs["cls"] = self.command_class
func: t.Optional[t.Callable] = None
if args and callable(args[0]):
assert (
len(args) == 1 and not kwargs
), "Use 'command(**kwargs)(callable)' to provide arguments."
(func,) = args
args = ()
def decorator(f: t.Callable[..., t.Any]) -> Command: def decorator(f: t.Callable[..., t.Any]) -> Command:
cmd = command(*args, **kwargs)(f) cmd: Command = command(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
return cmd return cmd
if func is not None:
return decorator(func)
return decorator return decorator
@t.overload
def group(self, __func: t.Callable[..., t.Any]) -> "Group":
...
@t.overload
def group( def group(
self, *args: t.Any, **kwargs: t.Any 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
) -> t.Union[t.Callable[[t.Callable[..., t.Any]], "Group"], "Group"]:
"""A shortcut decorator for declaring and attaching a group to """A shortcut decorator for declaring and attaching a group to
the group. This takes the same arguments as :func:`group` and the group. This takes the same arguments as :func:`group` and
immediately registers the created group with this group by immediately registers the created group with this group by
@ -1848,22 +1881,37 @@ class Group(MultiCommand):
To customize the group class used, set the :attr:`group_class` To customize the group class used, set the :attr:`group_class`
attribute. attribute.
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
.. versionchanged:: 8.0 .. versionchanged:: 8.0
Added the :attr:`group_class` attribute. Added the :attr:`group_class` attribute.
""" """
from .decorators import group from .decorators import group
if self.group_class is not None and "cls" not in kwargs: func: t.Optional[t.Callable] = None
if args and callable(args[0]):
assert (
len(args) == 1 and not kwargs
), "Use 'group(**kwargs)(callable)' to provide arguments."
(func,) = args
args = ()
if self.group_class is not None and kwargs.get("cls") is None:
if self.group_class is type: if self.group_class is type:
kwargs["cls"] = type(self) kwargs["cls"] = type(self)
else: else:
kwargs["cls"] = self.group_class kwargs["cls"] = self.group_class
def decorator(f: t.Callable[..., t.Any]) -> "Group": def decorator(f: t.Callable[..., t.Any]) -> "Group":
cmd = group(*args, **kwargs)(f) cmd: Group = group(*args, **kwargs)(f)
self.add_command(cmd) self.add_command(cmd)
return cmd return cmd
if func is not None:
return decorator(func)
return decorator return decorator
def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]: def get_command(self, ctx: Context, cmd_name: str) -> t.Optional[Command]:
@ -2020,11 +2068,6 @@ class Parameter:
t.Union[t.List["CompletionItem"], t.List[str]], t.Union[t.List["CompletionItem"], t.List[str]],
] ]
] = None, ] = None,
autocompletion: t.Optional[
t.Callable[
[Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]]
]
] = None,
) -> None: ) -> None:
self.name, self.opts, self.secondary_opts = self._parse_decls( self.name, self.opts, self.secondary_opts = self._parse_decls(
param_decls or (), expose_value param_decls or (), expose_value
@ -2048,36 +2091,6 @@ class Parameter:
self.is_eager = is_eager self.is_eager = is_eager
self.metavar = metavar self.metavar = metavar
self.envvar = envvar self.envvar = envvar
if autocompletion is not None:
import warnings
warnings.warn(
"'autocompletion' is renamed to 'shell_complete'. The old name is"
" deprecated and will be removed in Click 8.1. See the docs about"
" 'Parameter' for information about new behavior.",
DeprecationWarning,
stacklevel=2,
)
def shell_complete(
ctx: Context, param: "Parameter", incomplete: str
) -> t.List["CompletionItem"]:
from click.shell_completion import CompletionItem
out = []
for c in autocompletion(ctx, [], incomplete): # type: ignore
if isinstance(c, tuple):
c = CompletionItem(c[0], help=c[1])
elif isinstance(c, str):
c = CompletionItem(c)
if c.value.startswith(incomplete):
out.append(c)
return out
self._custom_shell_complete = shell_complete self._custom_shell_complete = shell_complete
if __debug__: if __debug__:
@ -2172,13 +2185,13 @@ class Parameter:
return metavar return metavar
@typing.overload @t.overload
def get_default( def get_default(
self, ctx: Context, call: "te.Literal[True]" = True self, ctx: Context, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]: ) -> t.Optional[t.Any]:
... ...
@typing.overload @t.overload
def get_default( def get_default(
self, ctx: Context, call: bool = ... 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]]]:
@ -2399,25 +2412,27 @@ class Option(Parameter):
All other parameters are passed onwards to the parameter constructor. All other parameters are passed onwards to the parameter constructor.
:param show_default: controls if the default value should be shown on the :param show_default: Show the default value for this option in its
help page. Normally, defaults are not shown. If this help text. Values are not shown by default, unless
value is a string, it shows the string instead of the :attr:`Context.show_default` is ``True``. If this value is a
value. This is particularly useful for dynamic options. string, it shows that string in parentheses instead of the
:param show_envvar: controls if an environment variable should be shown on actual value. This is particularly useful for dynamic options.
the help page. Normally, environment variables For single option boolean flags, the default remains hidden if
are not shown. its value is ``False``.
:param prompt: if set to `True` or a non empty string then the user will be :param show_envvar: Controls if an environment variable should be
prompted for input. If set to `True` the prompt will be the shown on the help page. Normally, environment variables are not
option name capitalized. shown.
:param prompt: If set to ``True`` or a non empty string then the
user will be prompted for input. If set to ``True`` the prompt
will be the option name capitalized.
:param confirmation_prompt: Prompt a second time to confirm the :param confirmation_prompt: Prompt a second time to confirm the
value if it was prompted for. Can be set to a string instead of value if it was prompted for. Can be set to a string instead of
``True`` to customize the message. ``True`` to customize the message.
:param prompt_required: If set to ``False``, the user will be :param prompt_required: If set to ``False``, the user will be
prompted for input only when the option was specified as a flag prompted for input only when the option was specified as a flag
without a value. without a value.
:param hide_input: if this is `True` then the input on the prompt will be :param hide_input: If this is ``True`` then the input on the prompt
hidden from the user. This is useful for password will be hidden from the user. This is useful for password input.
input.
:param is_flag: forces this option to act as a flag. The default is :param is_flag: forces this option to act as a flag. The default is
auto detection. auto detection.
:param flag_value: which value should be used for this flag if it's :param flag_value: which value should be used for this flag if it's
@ -2435,6 +2450,18 @@ class Option(Parameter):
:param help: the help string. :param help: the help string.
:param hidden: hide this option from help outputs. :param hidden: hide this option from help outputs.
.. versionchanged:: 8.1.0
Help text indentation is cleaned here instead of only in the
``@option`` decorator.
.. versionchanged:: 8.1.0
The ``show_default`` parameter overrides
``Context.show_default``.
.. versionchanged:: 8.1.0
The default of a single option boolean flag is not shown if the
default value is ``False``.
.. versionchanged:: 8.0.1 .. versionchanged:: 8.0.1
``type`` is detected from ``flag_value`` if given. ``type`` is detected from ``flag_value`` if given.
""" """
@ -2444,7 +2471,7 @@ class Option(Parameter):
def __init__( def __init__(
self, self,
param_decls: t.Optional[t.Sequence[str]] = None, param_decls: t.Optional[t.Sequence[str]] = None,
show_default: t.Union[bool, str] = False, show_default: t.Union[bool, str, None] = None,
prompt: t.Union[bool, str] = False, prompt: t.Union[bool, str] = False,
confirmation_prompt: t.Union[bool, str] = False, confirmation_prompt: t.Union[bool, str] = False,
prompt_required: bool = True, prompt_required: bool = True,
@ -2461,6 +2488,9 @@ class Option(Parameter):
show_envvar: bool = False, show_envvar: bool = False,
**attrs: t.Any, **attrs: t.Any,
) -> None: ) -> None:
if help:
help = inspect.cleandoc(help)
default_is_missing = "default" not in attrs default_is_missing = "default" not in attrs
super().__init__(param_decls, type=type, multiple=multiple, **attrs) super().__init__(param_decls, type=type, multiple=multiple, **attrs)
@ -2472,7 +2502,7 @@ class Option(Parameter):
elif prompt is False: elif prompt is False:
prompt_text = None prompt_text = None
else: else:
prompt_text = t.cast(str, prompt) prompt_text = prompt
self.prompt = prompt_text self.prompt = prompt_text
self.confirmation_prompt = confirmation_prompt self.confirmation_prompt = confirmation_prompt
@ -2499,7 +2529,7 @@ class Option(Parameter):
# flag if flag_value is set. # flag if flag_value is set.
self._flag_needs_value = flag_value is not None self._flag_needs_value = flag_value is not None
if is_flag and default_is_missing: if is_flag and default_is_missing and not self.required:
self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False self.default: t.Union[t.Any, t.Callable[[], t.Any]] = False
if flag_value is None: if flag_value is None:
@ -2550,6 +2580,9 @@ class Option(Parameter):
if self.is_flag: if self.is_flag:
raise TypeError("'count' is not valid with 'is_flag'.") raise TypeError("'count' is not valid with 'is_flag'.")
if self.multiple and self.is_flag:
raise TypeError("'multiple' is not valid with 'is_flag', use 'count'.")
def to_info_dict(self) -> t.Dict[str, t.Any]: def to_info_dict(self) -> t.Dict[str, t.Any]:
info_dict = super().to_info_dict() info_dict = super().to_info_dict()
info_dict.update( info_dict.update(
@ -2711,16 +2744,23 @@ class Option(Parameter):
finally: finally:
ctx.resilient_parsing = resilient ctx.resilient_parsing = resilient
show_default_is_str = isinstance(self.show_default, str) show_default = False
show_default_is_str = False
if show_default_is_str or ( if self.show_default is not None:
default_value is not None and (self.show_default or ctx.show_default) if isinstance(self.show_default, str):
): show_default_is_str = show_default = True
else:
show_default = self.show_default
elif ctx.show_default is not None:
show_default = ctx.show_default
if show_default_is_str or (show_default and (default_value is not None)):
if show_default_is_str: if show_default_is_str:
default_string = f"({self.show_default})" default_string = f"({self.show_default})"
elif isinstance(default_value, (list, tuple)): elif isinstance(default_value, (list, tuple)):
default_string = ", ".join(str(d) for d in default_value) default_string = ", ".join(str(d) for d in default_value)
elif callable(default_value): elif inspect.isfunction(default_value):
default_string = _("(dynamic)") default_string = _("(dynamic)")
elif self.is_bool_flag and self.secondary_opts: elif self.is_bool_flag and self.secondary_opts:
# For boolean flags that have distinct True/False opts, # For boolean flags that have distinct True/False opts,
@ -2728,6 +2768,8 @@ class Option(Parameter):
default_string = split_opt( default_string = split_opt(
(self.opts if self.default else self.secondary_opts)[0] (self.opts if self.default else self.secondary_opts)[0]
)[1] )[1]
elif self.is_bool_flag and not self.secondary_opts and not default_value:
default_string = ""
else: else:
default_string = str(default_value) default_string = str(default_value)
@ -2753,13 +2795,13 @@ class Option(Parameter):
return ("; " if any_prefix_is_slash else " / ").join(rv), help return ("; " if any_prefix_is_slash else " / ").join(rv), help
@typing.overload @t.overload
def get_default( def get_default(
self, ctx: Context, call: "te.Literal[True]" = True self, ctx: Context, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]: ) -> t.Optional[t.Any]:
... ...
@typing.overload @t.overload
def get_default( def get_default(
self, ctx: Context, call: bool = ... 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]]]:
@ -2770,7 +2812,7 @@ class Option(Parameter):
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]: ) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
# If we're a non boolean flag our default is more complex because # If we're a non boolean flag our default is more complex because
# we need to look at all flags in the same group to figure out # we need to look at all flags in the same group to figure out
# if we're the the default one in which case we return the flag # if we're the default one in which case we return the flag
# value as default. # value as default.
if self.is_flag and not self.is_bool_flag: if self.is_flag and not self.is_bool_flag:
for param in ctx.command.params: for param in ctx.command.params:
@ -2821,7 +2863,10 @@ class Option(Parameter):
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
rv = os.environ.get(envvar) rv = os.environ.get(envvar)
return rv if rv:
return rv
return None
def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]: def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx) rv: t.Optional[t.Any] = self.resolve_envvar_value(ctx)

View file

@ -14,7 +14,7 @@ from .globals import get_current_context
from .utils import echo from .utils import echo
F = t.TypeVar("F", bound=t.Callable[..., t.Any]) F = t.TypeVar("F", bound=t.Callable[..., t.Any])
FC = t.TypeVar("FC", t.Callable[..., t.Any], Command) FC = t.TypeVar("FC", bound=t.Union[t.Callable[..., t.Any], Command])
def pass_context(f: F) -> F: def pass_context(f: F) -> F:
@ -121,43 +121,38 @@ def pass_meta_key(
return decorator return decorator
def _make_command( CmdType = t.TypeVar("CmdType", bound=Command)
f: F,
name: t.Optional[str],
attrs: t.MutableMapping[str, t.Any], @t.overload
cls: t.Type[Command], def command(
__func: t.Callable[..., t.Any],
) -> Command: ) -> Command:
if isinstance(f, Command): ...
raise TypeError("Attempted to convert a callback into a command twice.")
try:
params = f.__click_params__ # type: ignore
params.reverse()
del f.__click_params__ # type: ignore
except AttributeError:
params = []
help = attrs.get("help") @t.overload
def command(
name: t.Optional[str] = None,
**attrs: t.Any,
) -> t.Callable[..., Command]:
...
if help is None:
help = inspect.getdoc(f)
else:
help = inspect.cleandoc(help)
attrs["help"] = help @t.overload
return cls( def command(
name=name or f.__name__.lower().replace("_", "-"), name: t.Optional[str] = None,
callback=f, cls: t.Type[CmdType] = ...,
params=params, **attrs: t.Any,
**attrs, ) -> t.Callable[..., CmdType]:
) ...
def command( def command(
name: t.Optional[str] = None, name: t.Union[str, t.Callable[..., t.Any], None] = None,
cls: t.Optional[t.Type[Command]] = None, cls: t.Optional[t.Type[Command]] = None,
**attrs: t.Any, **attrs: t.Any,
) -> t.Callable[[F], Command]: ) -> t.Union[Command, t.Callable[..., Command]]:
r"""Creates a new :class:`Command` and uses the decorated function as r"""Creates a new :class:`Command` and uses the decorated function as
callback. This will also automatically attach all decorated callback. This will also automatically attach all decorated
:func:`option`\s and :func:`argument`\s as parameters to the command. :func:`option`\s and :func:`argument`\s as parameters to the command.
@ -167,6 +162,8 @@ def command(
pass the intended name as the first argument. pass the intended name as the first argument.
All keyword arguments are forwarded to the underlying command class. All keyword arguments are forwarded to the underlying command class.
For the ``params`` argument, any decorated params are appended to
the end of the list.
Once decorated the function turns into a :class:`Command` instance Once decorated the function turns into a :class:`Command` instance
that can be invoked as a command line utility or be attached to a that can be invoked as a command line utility or be attached to a
@ -176,24 +173,91 @@ def command(
name with underscores replaced by dashes. name with underscores replaced by dashes.
:param cls: the command class to instantiate. This defaults to :param cls: the command class to instantiate. This defaults to
:class:`Command`. :class:`Command`.
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
.. versionchanged:: 8.1
The ``params`` argument can be used. Decorated params are
appended to the end of the list.
""" """
func: t.Optional[t.Callable[..., t.Any]] = None
if callable(name):
func = name
name = None
assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class."
assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments."
if cls is None: if cls is None:
cls = Command cls = Command
def decorator(f: t.Callable[..., t.Any]) -> Command: def decorator(f: t.Callable[..., t.Any]) -> Command:
cmd = _make_command(f, name, attrs, cls) # type: ignore if isinstance(f, Command):
raise TypeError("Attempted to convert a callback into a command twice.")
attr_params = attrs.pop("params", None)
params = attr_params if attr_params is not None else []
try:
decorator_params = f.__click_params__ # type: ignore
except AttributeError:
pass
else:
del f.__click_params__ # type: ignore
params.extend(reversed(decorator_params))
if attrs.get("help") is None:
attrs["help"] = f.__doc__
cmd = cls( # type: ignore[misc]
name=name or f.__name__.lower().replace("_", "-"), # type: ignore[arg-type]
callback=f,
params=params,
**attrs,
)
cmd.__doc__ = f.__doc__ cmd.__doc__ = f.__doc__
return cmd return cmd
if func is not None:
return decorator(func)
return decorator return decorator
def group(name: t.Optional[str] = None, **attrs: t.Any) -> t.Callable[[F], Group]: @t.overload
def group(
__func: t.Callable[..., t.Any],
) -> Group:
...
@t.overload
def group(
name: t.Optional[str] = None,
**attrs: t.Any,
) -> t.Callable[[F], Group]:
...
def group(
name: t.Union[str, t.Callable[..., t.Any], None] = None, **attrs: t.Any
) -> t.Union[Group, t.Callable[[F], Group]]:
"""Creates a new :class:`Group` with a function as callback. This """Creates a new :class:`Group` with a function as callback. This
works otherwise the same as :func:`command` just that the `cls` works otherwise the same as :func:`command` just that the `cls`
parameter is set to :class:`Group`. parameter is set to :class:`Group`.
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
""" """
attrs.setdefault("cls", Group) if attrs.get("cls") is None:
attrs["cls"] = Group
if callable(name):
grp: t.Callable[[F], Group] = t.cast(Group, command(**attrs))
return grp(name)
return t.cast(Group, command(name, **attrs)) return t.cast(Group, command(name, **attrs))
@ -219,7 +283,7 @@ def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
""" """
def decorator(f: FC) -> FC: def decorator(f: FC) -> FC:
ArgumentClass = attrs.pop("cls", Argument) ArgumentClass = attrs.pop("cls", None) or Argument
_param_memo(f, ArgumentClass(param_decls, **attrs)) _param_memo(f, ArgumentClass(param_decls, **attrs))
return f return f
@ -240,10 +304,7 @@ def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
def decorator(f: FC) -> FC: def decorator(f: FC) -> FC:
# Issue 926, copy attrs, so pre-defined options can re-use the same cls= # Issue 926, copy attrs, so pre-defined options can re-use the same cls=
option_attrs = attrs.copy() option_attrs = attrs.copy()
OptionClass = option_attrs.pop("cls", None) or Option
if "help" in option_attrs:
option_attrs["help"] = inspect.cleandoc(option_attrs["help"])
OptionClass = option_attrs.pop("cls", Option)
_param_memo(f, OptionClass(param_decls, **option_attrs)) _param_memo(f, OptionClass(param_decls, **option_attrs))
return f return f

View file

@ -1,4 +1,3 @@
import typing
import typing as t import typing as t
from threading import local from threading import local
@ -9,12 +8,12 @@ if t.TYPE_CHECKING:
_local = local() _local = local()
@typing.overload @t.overload
def get_current_context(silent: "te.Literal[False]" = False) -> "Context": def get_current_context(silent: "te.Literal[False]" = False) -> "Context":
... ...
@typing.overload @t.overload
def get_current_context(silent: bool = ...) -> t.Optional["Context"]: def get_current_context(silent: bool = ...) -> t.Optional["Context"]:
... ...

View file

@ -102,10 +102,10 @@ _SOURCE_BASH = """\
IFS=',' read type value <<< "$completion" IFS=',' read type value <<< "$completion"
if [[ $type == 'dir' ]]; then if [[ $type == 'dir' ]]; then
COMREPLY=() COMPREPLY=()
compopt -o dirnames compopt -o dirnames
elif [[ $type == 'file' ]]; then elif [[ $type == 'file' ]]; then
COMREPLY=() COMPREPLY=()
compopt -o default compopt -o default
elif [[ $type == 'plain' ]]; then elif [[ $type == 'plain' ]]; then
COMPREPLY+=($value) COMPREPLY+=($value)
@ -448,17 +448,16 @@ def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool:
) )
def _start_of_option(value: str) -> bool: def _start_of_option(ctx: Context, value: str) -> bool:
"""Check if the value looks like the start of an option.""" """Check if the value looks like the start of an option."""
if not value: if not value:
return False return False
c = value[0] c = value[0]
# Allow "/" since that starts a path. return c in ctx._opt_prefixes
return not c.isalnum() and c != "/"
def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool: def _is_incomplete_option(ctx: Context, args: t.List[str], param: Parameter) -> bool:
"""Determine if the given parameter is an option that needs a value. """Determine if the given parameter is an option that needs a value.
:param args: List of complete args before the incomplete value. :param args: List of complete args before the incomplete value.
@ -467,7 +466,7 @@ def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool:
if not isinstance(param, Option): if not isinstance(param, Option):
return False return False
if param.is_flag: if param.is_flag or param.count:
return False return False
last_option = None last_option = None
@ -476,7 +475,7 @@ def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool:
if index + 1 > param.nargs: if index + 1 > param.nargs:
break break
if _start_of_option(arg): if _start_of_option(ctx, arg):
last_option = arg last_option = arg
return last_option is not None and last_option in param.opts return last_option is not None and last_option in param.opts
@ -551,7 +550,7 @@ def _resolve_incomplete(
# split and discard the "=" to make completion easier. # split and discard the "=" to make completion easier.
if incomplete == "=": if incomplete == "=":
incomplete = "" incomplete = ""
elif "=" in incomplete and _start_of_option(incomplete): elif "=" in incomplete and _start_of_option(ctx, incomplete):
name, _, incomplete = incomplete.partition("=") name, _, incomplete = incomplete.partition("=")
args.append(name) args.append(name)
@ -559,7 +558,7 @@ def _resolve_incomplete(
# even if they start with the option character. If it hasn't been # even if they start with the option character. If it hasn't been
# given and the incomplete arg looks like an option, the current # given and the incomplete arg looks like an option, the current
# command will provide option name completions. # command will provide option name completions.
if "--" not in args and _start_of_option(incomplete): if "--" not in args and _start_of_option(ctx, incomplete):
return ctx.command, incomplete return ctx.command, incomplete
params = ctx.command.get_params(ctx) params = ctx.command.get_params(ctx)
@ -567,7 +566,7 @@ def _resolve_incomplete(
# If the last complete arg is an option name with an incomplete # If the last complete arg is an option name with an incomplete
# value, the option will provide value completions. # value, the option will provide value completions.
for param in params: for param in params:
if _is_incomplete_option(args, param): if _is_incomplete_option(ctx, args, param):
return param, incomplete return param, incomplete
# It's not an option name or value. The first argument without a # It's not an option name or value. The first argument without a

View file

@ -3,7 +3,6 @@ import io
import itertools import itertools
import os import os
import sys import sys
import typing
import typing as t import typing as t
from gettext import gettext as _ from gettext import gettext as _
@ -94,7 +93,7 @@ def prompt(
"""Prompts a user for input. This is a convenience function that can """Prompts a user for input. This is a convenience function that can
be used to prompt a user for input later. be used to prompt a user for input later.
If the user aborts the input by sending a interrupt signal, this If the user aborts the input by sending an interrupt signal, this
function will catch it and raise a :exc:`Abort` exception. function will catch it and raise a :exc:`Abort` exception.
:param text: the text to show for the prompt. :param text: the text to show for the prompt.
@ -160,7 +159,6 @@ def prompt(
if confirmation_prompt is True: if confirmation_prompt is True:
confirmation_prompt = _("Repeat for confirmation") confirmation_prompt = _("Repeat for confirmation")
confirmation_prompt = t.cast(str, confirmation_prompt)
confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
while True: while True:
@ -182,9 +180,9 @@ def prompt(
if not confirmation_prompt: if not confirmation_prompt:
return result return result
while True: while True:
confirmation_prompt = t.cast(str, confirmation_prompt)
value2 = prompt_func(confirmation_prompt) value2 = prompt_func(confirmation_prompt)
if value2: is_empty = not value and not value2
if value2 or is_empty:
break break
if value == value2: if value == value2:
return result return result
@ -252,26 +250,6 @@ def confirm(
return rv return rv
def get_terminal_size() -> os.terminal_size:
"""Returns the current size of the terminal as tuple in the form
``(width, height)`` in columns and rows.
.. deprecated:: 8.0
Will be removed in Click 8.1. Use
:func:`shutil.get_terminal_size` instead.
"""
import shutil
import warnings
warnings.warn(
"'click.get_terminal_size()' is deprecated and will be removed"
" in Click 8.1. Use 'shutil.get_terminal_size()' instead.",
DeprecationWarning,
stacklevel=2,
)
return shutil.get_terminal_size()
def echo_via_pager( def echo_via_pager(
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str], text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
color: t.Optional[bool] = None, color: t.Optional[bool] = None,
@ -627,7 +605,7 @@ def unstyle(text: str) -> str:
def secho( def secho(
message: t.Optional[t.Any] = None, message: t.Optional[t.Any] = None,
file: t.Optional[t.IO] = None, file: t.Optional[t.IO[t.AnyStr]] = None,
nl: bool = True, nl: bool = True,
err: bool = False, err: bool = False,
color: t.Optional[bool] = None, color: t.Optional[bool] = None,

View file

@ -464,16 +464,16 @@ class CliRunner:
Added the ``temp_dir`` parameter. Added the ``temp_dir`` parameter.
""" """
cwd = os.getcwd() cwd = os.getcwd()
t = tempfile.mkdtemp(dir=temp_dir) dt = tempfile.mkdtemp(dir=temp_dir) # type: ignore[type-var]
os.chdir(t) os.chdir(dt)
try: try:
yield t yield t.cast(str, dt)
finally: finally:
os.chdir(cwd) os.chdir(cwd)
if temp_dir is None: if temp_dir is None:
try: try:
shutil.rmtree(t) shutil.rmtree(dt)
except OSError: # noqa: B014 except OSError: # noqa: B014
pass pass

View file

@ -63,7 +63,14 @@ class ParamType:
# The class name without the "ParamType" suffix. # The class name without the "ParamType" suffix.
param_type = type(self).__name__.partition("ParamType")[0] param_type = type(self).__name__.partition("ParamType")[0]
param_type = param_type.partition("ParameterType")[0] param_type = param_type.partition("ParameterType")[0]
return {"param_type": param_type, "name": self.name}
# Custom subclasses might not remember to set a name.
if hasattr(self, "name"):
name = self.name
else:
name = param_type
return {"param_type": param_type, "name": name}
def __call__( def __call__(
self, self,
@ -724,7 +731,7 @@ class File(ParamType):
return f return f
except OSError as e: # noqa: B014 except OSError as e: # noqa: B014
self.fail(f"{os.fsdecode(value)!r}: {e.strerror}", param, ctx) self.fail(f"'{os.fsdecode(value)}': {e.strerror}", param, ctx)
def shell_complete( def shell_complete(
self, ctx: "Context", param: "Parameter", incomplete: str self, ctx: "Context", param: "Parameter", incomplete: str
@ -744,30 +751,31 @@ class File(ParamType):
class Path(ParamType): class Path(ParamType):
"""The path type is similar to the :class:`File` type but it performs """The ``Path`` type is similar to the :class:`File` type, but
different checks. First of all, instead of returning an open file returns the filename instead of an open file. Various checks can be
handle it returns just the filename. Secondly, it can perform various enabled to validate the type of file and permissions.
basic checks about what the file or directory should be.
:param exists: if set to true, the file or directory needs to exist for :param exists: The file or directory needs to exist for the value to
this value to be valid. If this is not required and a be valid. If this is not set to ``True``, and the file does not
file does indeed not exist, then all further checks are exist, then all further checks are silently skipped.
silently skipped. :param file_okay: Allow a file as a value.
:param file_okay: controls if a file is a possible value. :param dir_okay: Allow a directory as a value.
:param dir_okay: controls if a directory is a possible value.
:param writable: if true, a writable check is performed.
:param readable: if true, a readable check is performed. :param readable: if true, a readable check is performed.
:param resolve_path: if this is true, then the path is fully resolved :param writable: if true, a writable check is performed.
before the value is passed onwards. This means :param executable: if true, an executable check is performed.
that it's absolute and symlinks are resolved. It :param resolve_path: Make the value absolute and resolve any
will not expand a tilde-prefix, as this is symlinks. A ``~`` is not expanded, as this is supposed to be
supposed to be done by the shell only. done by the shell only.
:param allow_dash: If this is set to `True`, a single dash to indicate :param allow_dash: Allow a single dash as a value, which indicates
standard streams is permitted. a standard stream (but does not open it). Use
:func:`~click.open_file` to handle opening this value.
:param path_type: Convert the incoming path value to this type. If :param path_type: Convert the incoming path value to this type. If
``None``, keep Python's default, which is ``str``. Useful to ``None``, keep Python's default, which is ``str``. Useful to
convert to :class:`pathlib.Path`. convert to :class:`pathlib.Path`.
.. versionchanged:: 8.1
Added the ``executable`` parameter.
.. versionchanged:: 8.0 .. versionchanged:: 8.0
Allow passing ``type=pathlib.Path``. Allow passing ``type=pathlib.Path``.
@ -787,12 +795,14 @@ class Path(ParamType):
resolve_path: bool = False, resolve_path: bool = False,
allow_dash: bool = False, allow_dash: bool = False,
path_type: t.Optional[t.Type] = None, path_type: t.Optional[t.Type] = None,
executable: bool = False,
): ):
self.exists = exists self.exists = exists
self.file_okay = file_okay self.file_okay = file_okay
self.dir_okay = dir_okay self.dir_okay = dir_okay
self.writable = writable
self.readable = readable self.readable = readable
self.writable = writable
self.executable = executable
self.resolve_path = resolve_path self.resolve_path = resolve_path
self.allow_dash = allow_dash self.allow_dash = allow_dash
self.type = path_type self.type = path_type
@ -865,12 +875,22 @@ class Path(ParamType):
) )
if not self.dir_okay and stat.S_ISDIR(st.st_mode): if not self.dir_okay and stat.S_ISDIR(st.st_mode):
self.fail( self.fail(
_("{name} {filename!r} is a directory.").format( _("{name} '{filename}' is a directory.").format(
name=self.name.title(), filename=os.fsdecode(value) name=self.name.title(), filename=os.fsdecode(value)
), ),
param, param,
ctx, ctx,
) )
if self.readable and not os.access(rv, os.R_OK):
self.fail(
_("{name} {filename!r} is not readable.").format(
name=self.name.title(), filename=os.fsdecode(value)
),
param,
ctx,
)
if self.writable and not os.access(rv, os.W_OK): if self.writable and not os.access(rv, os.W_OK):
self.fail( self.fail(
_("{name} {filename!r} is not writable.").format( _("{name} {filename!r} is not writable.").format(
@ -879,9 +899,10 @@ class Path(ParamType):
param, param,
ctx, ctx,
) )
if self.readable and not os.access(rv, os.R_OK):
if self.executable and not os.access(value, os.X_OK):
self.fail( self.fail(
_("{name} {filename!r} is not readable.").format( _("{name} {filename!r} is not executable.").format(
name=self.name.title(), filename=os.fsdecode(value) name=self.name.title(), filename=os.fsdecode(value)
), ),
param, param,

View file

@ -1,4 +1,5 @@
import os import os
import re
import sys import sys
import typing as t import typing as t
from functools import update_wrapper from functools import update_wrapper
@ -203,7 +204,7 @@ class KeepOpenFile:
def echo( def echo(
message: t.Optional[t.Any] = None, message: t.Optional[t.Any] = None,
file: t.Optional[t.IO] = None, file: t.Optional[t.IO[t.Any]] = None,
nl: bool = True, nl: bool = True,
err: bool = False, err: bool = False,
color: t.Optional[bool] = None, color: t.Optional[bool] = None,
@ -340,55 +341,45 @@ def open_file(
lazy: bool = False, lazy: bool = False,
atomic: bool = False, atomic: bool = False,
) -> t.IO: ) -> t.IO:
"""This is similar to how the :class:`File` works but for manual """Open a file, with extra behavior to handle ``'-'`` to indicate
usage. Files are opened non lazy by default. This can open regular a standard stream, lazy open on write, and atomic write. Similar to
files as well as stdin/stdout if ``'-'`` is passed. the behavior of the :class:`~click.File` param type.
If stdin/stdout is returned the stream is wrapped so that the context If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is
manager will not close the stream accidentally. This makes it possible wrapped so that using it in a context manager will not close it.
to always use the function like this without having to worry to This makes it possible to use the function without accidentally
accidentally close a standard stream:: closing a standard stream:
.. code-block:: python
with open_file(filename) as f: with open_file(filename) as f:
... ...
.. versionadded:: 3.0 :param filename: The name 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
text mode.
:param errors: The error handling mode.
:param lazy: Wait to open the file until it is accessed. For read
mode, the file is temporarily opened to raise access errors
early, then closed until it is read again.
:param atomic: Write to a temporary file and replace the given file
on close.
:param filename: the name of the file to open (or ``'-'`` for stdin/stdout). .. versionadded:: 3.0
:param mode: the mode in which to open the file.
:param encoding: the encoding to use.
:param errors: the error handling for this file.
:param lazy: can be flipped to true to open the file lazily.
:param atomic: in atomic mode writes go into a temporary file and it's
moved on close.
""" """
if lazy: if lazy:
return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic)) return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic))
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
if not should_close: if not should_close:
f = t.cast(t.IO, KeepOpenFile(f)) f = t.cast(t.IO, KeepOpenFile(f))
return f return f
def get_os_args() -> t.Sequence[str]:
"""Returns the argument part of ``sys.argv``, removing the first
value which is the name of the script.
.. deprecated:: 8.0
Will be removed in Click 8.1. Access ``sys.argv[1:]`` directly
instead.
"""
import warnings
warnings.warn(
"'get_os_args' is deprecated and will be removed in Click 8.1."
" Access 'sys.argv[1:]' directly instead.",
DeprecationWarning,
stacklevel=2,
)
return sys.argv[1:]
def format_filename( def format_filename(
filename: t.Union[str, bytes, os.PathLike], shorten: bool = False filename: t.Union[str, bytes, os.PathLike], shorten: bool = False
) -> str: ) -> str:
@ -484,7 +475,7 @@ class PacifyFlushWrapper:
def _detect_program_name( def _detect_program_name(
path: t.Optional[str] = None, _main: ModuleType = sys.modules["__main__"] path: t.Optional[str] = None, _main: t.Optional[ModuleType] = None
) -> str: ) -> str:
"""Determine the command used to run the program, for use in help """Determine the command used to run the program, for use in help
text. If a file or entry point was executed, the file name is text. If a file or entry point was executed, the file name is
@ -506,6 +497,9 @@ def _detect_program_name(
:meta private: :meta private:
""" """
if _main is None:
_main = sys.modules["__main__"]
if not path: if not path:
path = sys.argv[0] path = sys.argv[0]
@ -546,7 +540,7 @@ def _expand_args(
See :func:`glob.glob`, :func:`os.path.expanduser`, and See :func:`glob.glob`, :func:`os.path.expanduser`, and
:func:`os.path.expandvars`. :func:`os.path.expandvars`.
This intended for use on Windows, where the shell does not do any This is intended for use on Windows, where the shell does not do any
expansion. It may not exactly match what a Unix shell would do. expansion. It may not exactly match what a Unix shell would do.
:param args: List of command line arguments to expand. :param args: List of command line arguments to expand.
@ -554,6 +548,10 @@ def _expand_args(
:param env: Expand environment variables. :param env: Expand environment variables.
:param glob_recursive: ``**`` matches directories recursively. :param glob_recursive: ``**`` matches directories recursively.
.. versionchanged:: 8.1
Invalid glob patterns are treated as empty expansions rather
than raising an error.
.. versionadded:: 8.0 .. versionadded:: 8.0
:meta private: :meta private:
@ -569,7 +567,10 @@ def _expand_args(
if env: if env:
arg = os.path.expandvars(arg) arg = os.path.expandvars(arg)
matches = glob(arg, recursive=glob_recursive) try:
matches = glob(arg, recursive=glob_recursive)
except re.error:
matches = []
if not matches: if not matches:
out.append(arg) out.append(arg)

View file

@ -41,25 +41,25 @@ def test_nargs_tup(runner):
assert result.output.splitlines() == ["name=peter", "point=1/2"] assert result.output.splitlines() == ["name=peter", "point=1/2"]
def test_nargs_tup_composite(runner): @pytest.mark.parametrize(
variations = [ "opts",
[
dict(type=(str, int)), dict(type=(str, int)),
dict(type=click.Tuple([str, int])), dict(type=click.Tuple([str, int])),
dict(nargs=2, type=click.Tuple([str, int])), dict(nargs=2, type=click.Tuple([str, int])),
dict(nargs=2, type=(str, int)), dict(nargs=2, type=(str, int)),
] ],
)
def test_nargs_tup_composite(runner, opts):
@click.command()
@click.argument("item", **opts)
def copy(item):
name, id = item
click.echo(f"name={name} id={id:d}")
for opts in variations: result = runner.invoke(copy, ["peter", "1"])
assert result.exception is None
@click.command() assert result.output.splitlines() == ["name=peter id=1"]
@click.argument("item", **opts)
def copy(item):
name, id = item
click.echo(f"name={name} id={id:d}")
result = runner.invoke(copy, ["peter", "1"])
assert not result.exception
assert result.output.splitlines() == ["name=peter id=1"]
def test_nargs_err(runner): def test_nargs_err(runner):
@ -120,9 +120,9 @@ def test_file_args(runner):
assert result.exit_code == 0 assert result.exit_code == 0
def test_path_args(runner): def test_path_allow_dash(runner):
@click.command() @click.command()
@click.argument("input", type=click.Path(dir_okay=False, allow_dash=True)) @click.argument("input", type=click.Path(allow_dash=True))
def foo(input): def foo(input):
click.echo(input) click.echo(input)

View file

@ -1,5 +1,4 @@
import os import os
import uuid
from itertools import chain from itertools import chain
import pytest import pytest
@ -104,125 +103,135 @@ def test_group_from_list(runner):
assert result.output == "sub" assert result.output == "sub"
def test_basic_option(runner): @pytest.mark.parametrize(
("args", "expect"),
[
([], "S:[no value]"),
(["--s=42"], "S:[42]"),
(["--s"], "Error: Option '--s' requires an argument."),
(["--s="], "S:[]"),
(["--s=\N{SNOWMAN}"], "S:[\N{SNOWMAN}]"),
],
)
def test_string_option(runner, args, expect):
@click.command() @click.command()
@click.option("--foo", default="no value") @click.option("--s", default="no value")
def cli(foo): def cli(s):
click.echo(f"FOO:[{foo}]") click.echo(f"S:[{s}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, args)
assert not result.exception assert expect in result.output
assert "FOO:[no value]" in result.output
result = runner.invoke(cli, ["--foo=42"]) if expect.startswith("Error:"):
assert not result.exception assert result.exception is not None
assert "FOO:[42]" in result.output else:
assert result.exception is None
result = runner.invoke(cli, ["--foo"])
assert result.exception
assert "Option '--foo' requires an argument." in result.output
result = runner.invoke(cli, ["--foo="])
assert not result.exception
assert "FOO:[]" in result.output
result = runner.invoke(cli, ["--foo=\N{SNOWMAN}"])
assert not result.exception
assert "FOO:[\N{SNOWMAN}]" in result.output
def test_int_option(runner): @pytest.mark.parametrize(
("args", "expect"),
[
([], "I:[84]"),
(["--i=23"], "I:[46]"),
(["--i=x"], "Error: Invalid value for '--i': 'x' is not a valid integer."),
],
)
def test_int_option(runner, args, expect):
@click.command() @click.command()
@click.option("--foo", default=42) @click.option("--i", default=42)
def cli(foo): def cli(i):
click.echo(f"FOO:[{foo * 2}]") click.echo(f"I:[{i * 2}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, args)
assert not result.exception assert expect in result.output
assert "FOO:[84]" in result.output
result = runner.invoke(cli, ["--foo=23"]) if expect.startswith("Error:"):
assert not result.exception assert result.exception is not None
assert "FOO:[46]" in result.output else:
assert result.exception is None
result = runner.invoke(cli, ["--foo=bar"])
assert result.exception
assert "Invalid value for '--foo': 'bar' is not a valid integer." in result.output
def test_uuid_option(runner): @pytest.mark.parametrize(
("args", "expect"),
[
([], "U:[ba122011-349f-423b-873b-9d6a79c688ab]"),
(
["--u=821592c1-c50e-4971-9cd6-e89dc6832f86"],
"U:[821592c1-c50e-4971-9cd6-e89dc6832f86]",
),
(["--u=x"], "Error: Invalid value for '--u': 'x' is not a valid UUID."),
],
)
def test_uuid_option(runner, args, expect):
@click.command() @click.command()
@click.option( @click.option(
"--u", default="ba122011-349f-423b-873b-9d6a79c688ab", type=click.UUID "--u", default="ba122011-349f-423b-873b-9d6a79c688ab", type=click.UUID
) )
def cli(u): def cli(u):
assert type(u) is uuid.UUID
click.echo(f"U:[{u}]") click.echo(f"U:[{u}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, args)
assert not result.exception assert expect in result.output
assert "U:[ba122011-349f-423b-873b-9d6a79c688ab]" in result.output
result = runner.invoke(cli, ["--u=821592c1-c50e-4971-9cd6-e89dc6832f86"]) if expect.startswith("Error:"):
assert not result.exception assert result.exception is not None
assert "U:[821592c1-c50e-4971-9cd6-e89dc6832f86]" in result.output else:
assert result.exception is None
result = runner.invoke(cli, ["--u=bar"])
assert result.exception
assert "Invalid value for '--u': 'bar' is not a valid UUID." in result.output
def test_float_option(runner): @pytest.mark.parametrize(
("args", "expect"),
[
([], "F:[42.0]"),
("--f=23.5", "F:[23.5]"),
("--f=x", "Error: Invalid value for '--f': 'x' is not a valid float."),
],
)
def test_float_option(runner, args, expect):
@click.command() @click.command()
@click.option("--foo", default=42, type=click.FLOAT) @click.option("--f", default=42.0)
def cli(foo): def cli(f):
assert type(foo) is float click.echo(f"F:[{f}]")
click.echo(f"FOO:[{foo}]")
result = runner.invoke(cli, []) result = runner.invoke(cli, args)
assert not result.exception assert expect in result.output
assert "FOO:[42.0]" in result.output
result = runner.invoke(cli, ["--foo=23.5"]) if expect.startswith("Error:"):
assert not result.exception assert result.exception is not None
assert "FOO:[23.5]" in result.output else:
assert result.exception is None
result = runner.invoke(cli, ["--foo=bar"])
assert result.exception
assert "Invalid value for '--foo': 'bar' is not a valid float." in result.output
def test_boolean_option(runner): @pytest.mark.parametrize("default", [True, False])
for default in True, False: @pytest.mark.parametrize(
("args", "expect"), [(["--on"], True), (["--off"], False), ([], None)]
)
def test_boolean_switch(runner, default, args, expect):
@click.command()
@click.option("--on/--off", default=default)
def cli(on):
return on
@click.command() if expect is None:
@click.option("--with-foo/--without-foo", default=default) expect = default
def cli(with_foo):
click.echo(with_foo)
result = runner.invoke(cli, ["--with-foo"]) result = runner.invoke(cli, args, standalone_mode=False)
assert not result.exception assert result.return_value is expect
assert result.output == "True\n"
result = runner.invoke(cli, ["--without-foo"])
assert not result.exception
assert result.output == "False\n"
result = runner.invoke(cli, [])
assert not result.exception
assert result.output == f"{default}\n"
for default in True, False:
@click.command() @pytest.mark.parametrize("default", [True, False])
@click.option("--flag", is_flag=True, default=default) @pytest.mark.parametrize(("args", "expect"), [(["--f"], True), ([], False)])
def cli(flag): def test_boolean_flag(runner, default, args, expect):
click.echo(flag) @click.command()
@click.option("--f", is_flag=True, default=default)
def cli(f):
return f
result = runner.invoke(cli, ["--flag"]) if default:
assert not result.exception expect = not expect
assert result.output == f"{not default}\n"
result = runner.invoke(cli, []) result = runner.invoke(cli, args, standalone_mode=False)
assert not result.exception assert result.return_value is expect
assert result.output == f"{default}\n"
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -34,7 +34,17 @@ def test_basic_chaining(runner):
] ]
def test_chaining_help(runner): @pytest.mark.parametrize(
("args", "expect"),
[
(["--help"], "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..."),
(["--help"], "ROOT HELP"),
(["sdist", "--help"], "SDIST HELP"),
(["bdist", "--help"], "BDIST HELP"),
(["bdist", "sdist", "--help"], "SDIST HELP"),
],
)
def test_chaining_help(runner, args, expect):
@click.group(chain=True) @click.group(chain=True)
def cli(): def cli():
"""ROOT HELP""" """ROOT HELP"""
@ -50,22 +60,9 @@ def test_chaining_help(runner):
"""BDIST HELP""" """BDIST HELP"""
click.echo("bdist called") click.echo("bdist called")
result = runner.invoke(cli, ["--help"]) result = runner.invoke(cli, args)
assert not result.exception assert not result.exception
assert "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." in result.output assert expect in result.output
assert "ROOT HELP" in result.output
result = runner.invoke(cli, ["sdist", "--help"])
assert not result.exception
assert "SDIST HELP" in result.output
result = runner.invoke(cli, ["bdist", "--help"])
assert not result.exception
assert "BDIST HELP" in result.output
result = runner.invoke(cli, ["bdist", "sdist", "--help"])
assert not result.exception
assert "SDIST HELP" in result.output
def test_chaining_with_options(runner): def test_chaining_with_options(runner):
@ -88,20 +85,20 @@ def test_chaining_with_options(runner):
assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
@pytest.mark.parametrize(("chain", "expect"), [(False, "None"), (True, "[]")]) @pytest.mark.parametrize(("chain", "expect"), [(False, "1"), (True, "[]")])
def test_no_command_result_callback(runner, chain, expect): def test_no_command_result_callback(runner, chain, expect):
"""When a group has ``invoke_without_command=True``, the result """When a group has ``invoke_without_command=True``, the result
callback is always invoked. A regular group invokes it with callback is always invoked. A regular group invokes it with
``None``, a chained group with ``[]``. its return value, a chained group with ``[]``.
""" """
@click.group(invoke_without_command=True, chain=chain) @click.group(invoke_without_command=True, chain=chain)
def cli(): def cli():
pass return 1
@cli.result_callback() @cli.result_callback()
def process_result(result): def process_result(result):
click.echo(str(result), nl=False) click.echo(result, nl=False)
result = runner.invoke(cli, []) result = runner.invoke(cli, [])
assert result.output == expect assert result.output == expect
@ -127,15 +124,23 @@ def test_chaining_with_arguments(runner):
assert result.output.splitlines() == ["bdist called 1", "sdist called 2"] assert result.output.splitlines() == ["bdist called 1", "sdist called 2"]
def test_pipeline(runner): @pytest.mark.parametrize(
("args", "input", "expect"),
[
(["-f", "-"], "foo\nbar", ["foo", "bar"]),
(["-f", "-", "strip"], "foo \n bar", ["foo", "bar"]),
(["-f", "-", "strip", "uppercase"], "foo \n bar", ["FOO", "BAR"]),
],
)
def test_pipeline(runner, args, input, expect):
@click.group(chain=True, invoke_without_command=True) @click.group(chain=True, invoke_without_command=True)
@click.option("-i", "--input", type=click.File("r")) @click.option("-f", type=click.File("r"))
def cli(input): def cli(f):
pass pass
@cli.result_callback() @cli.result_callback()
def process_pipeline(processors, input): def process_pipeline(processors, f):
iterator = (x.rstrip("\r\n") for x in input) iterator = (x.rstrip("\r\n") for x in f)
for processor in processors: for processor in processors:
iterator = processor(iterator) iterator = processor(iterator)
for item in iterator: for item in iterator:
@ -157,17 +162,9 @@ def test_pipeline(runner):
return processor return processor
result = runner.invoke(cli, ["-i", "-"], input="foo\nbar") result = runner.invoke(cli, args, input=input)
assert not result.exception assert not result.exception
assert result.output.splitlines() == ["foo", "bar"] assert result.output.splitlines() == expect
result = runner.invoke(cli, ["-i", "-", "strip"], input="foo \n bar")
assert not result.exception
assert result.output.splitlines() == ["foo", "bar"]
result = runner.invoke(cli, ["-i", "-", "strip", "uppercase"], input="foo \n bar")
assert not result.exception
assert result.output.splitlines() == ["FOO", "BAR"]
def test_args_and_chain(runner): def test_args_and_chain(runner):

View file

@ -0,0 +1,51 @@
import click
def test_command_no_parens(runner):
@click.command
def cli():
click.echo("hello")
result = runner.invoke(cli)
assert result.exception is None
assert result.output == "hello\n"
def test_group_no_parens(runner):
@click.group
def grp():
click.echo("grp1")
@grp.command
def cmd1():
click.echo("cmd1")
@grp.group
def grp2():
click.echo("grp2")
@grp2.command
def cmd2():
click.echo("cmd2")
result = runner.invoke(grp, ["cmd1"])
assert result.exception is None
assert result.output == "grp1\ncmd1\n"
result = runner.invoke(grp, ["grp2", "cmd2"])
assert result.exception is None
assert result.output == "grp1\ngrp2\ncmd2\n"
def test_params_argument(runner):
opt = click.Argument(["a"])
@click.command(params=[opt])
@click.argument("b")
def cli(a, b):
click.echo(f"{a} {b}")
assert cli.params[0].name == "a"
assert cli.params[1].name == "b"
result = runner.invoke(cli, ["1", "2"])
assert result.output == "1 2\n"

View file

@ -1,5 +1,7 @@
import re import re
import pytest
import click import click
@ -93,6 +95,20 @@ def test_auto_shorthelp(runner):
) )
def test_help_truncation(runner):
@click.command()
def cli():
"""This is a command with truncated help.
\f
This text should be truncated.
"""
result = runner.invoke(cli, ["--help"])
assert result.exit_code == 0
assert "This is a command with truncated help." in result.output
def test_no_args_is_help(runner): def test_no_args_is_help(runner):
@click.command(no_args_is_help=True) @click.command(no_args_is_help=True)
def cli(): def cli():
@ -119,7 +135,16 @@ def test_default_maps(runner):
assert result.output == "changed\n" assert result.output == "changed\n"
def test_group_with_args(runner): @pytest.mark.parametrize(
("args", "exit_code", "expect"),
[
(["obj1"], 2, "Error: Missing command."),
(["obj1", "--help"], 0, "Show this message and exit."),
(["obj1", "move"], 0, "obj=obj1\nmove\n"),
([], 0, "Show this message and exit."),
],
)
def test_group_with_args(runner, args, exit_code, expect):
@click.group() @click.group()
@click.argument("obj") @click.argument("obj")
def cli(obj): def cli(obj):
@ -129,21 +154,9 @@ def test_group_with_args(runner):
def move(): def move():
click.echo("move") click.echo("move")
result = runner.invoke(cli, []) result = runner.invoke(cli, args)
assert result.exit_code == 0 assert result.exit_code == exit_code
assert "Show this message and exit." in result.output assert expect in result.output
result = runner.invoke(cli, ["obj1"])
assert result.exit_code == 2
assert "Error: Missing command." in result.output
result = runner.invoke(cli, ["obj1", "--help"])
assert result.exit_code == 0
assert "Show this message and exit." in result.output
result = runner.invoke(cli, ["obj1", "move"])
assert result.exit_code == 0
assert result.output == "obj=obj1\nmove\n"
def test_base_command(runner): def test_base_command(runner):
@ -196,18 +209,12 @@ def test_base_command(runner):
cli.add_command(OptParseCommand("test", parser, test_callback)) cli.add_command(OptParseCommand("test", parser, test_callback))
result = runner.invoke( result = runner.invoke(cli, ["test", "-f", "f.txt", "-q", "q1.txt", "q2.txt"])
cli, ["test", "-f", "test.txt", "-q", "whatever.txt", "whateverelse.txt"] assert result.exception is None
) assert result.output.splitlines() == ["q1.txt q2.txt", "f.txt", "False"]
assert not result.exception
assert result.output.splitlines() == [
"whatever.txt whateverelse.txt",
"test.txt",
"False",
]
result = runner.invoke(cli, ["test", "--help"]) result = runner.invoke(cli, ["test", "--help"])
assert not result.exception assert result.exception is None
assert result.output.splitlines() == [ assert result.output.splitlines() == [
"Usage: foo test [OPTIONS]", "Usage: foo test [OPTIONS]",
"", "",
@ -328,20 +335,13 @@ def test_unprocessed_options(runner):
] ]
def test_deprecated_in_help_messages(runner): @pytest.mark.parametrize("doc", ["CLI HELP", None])
@click.command(deprecated=True) def test_deprecated_in_help_messages(runner, doc):
def cmd_with_help(): @click.command(deprecated=True, help=doc)
"""CLI HELP""" def cli():
pass pass
result = runner.invoke(cmd_with_help, ["--help"]) result = runner.invoke(cli, ["--help"])
assert "(Deprecated)" in result.output
@click.command(deprecated=True)
def cmd_without_help():
pass
result = runner.invoke(cmd_without_help, ["--help"])
assert "(Deprecated)" in result.output assert "(Deprecated)" in result.output
@ -352,3 +352,62 @@ def test_deprecated_in_invocation(runner):
result = runner.invoke(deprecated_cmd) result = runner.invoke(deprecated_cmd)
assert "DeprecationWarning:" in result.output assert "DeprecationWarning:" in result.output
def test_command_parse_args_collects_option_prefixes():
@click.command()
@click.option("+p", is_flag=True)
@click.option("!e", is_flag=True)
def test(p, e):
pass
ctx = click.Context(test)
test.parse_args(ctx, [])
assert ctx._opt_prefixes == {"-", "--", "+", "!"}
def test_group_parse_args_collects_base_option_prefixes():
@click.group()
@click.option("~t", is_flag=True)
def group(t):
pass
@group.command()
@click.option("+p", is_flag=True)
def command1(p):
pass
@group.command()
@click.option("!e", is_flag=True)
def command2(e):
pass
ctx = click.Context(group)
group.parse_args(ctx, ["command1", "+p"])
assert ctx._opt_prefixes == {"-", "--", "~"}
def test_group_invoke_collects_used_option_prefixes(runner):
opt_prefixes = set()
@click.group()
@click.option("~t", is_flag=True)
def group(t):
pass
@group.command()
@click.option("+p", is_flag=True)
@click.pass_context
def command1(ctx, p):
nonlocal opt_prefixes
opt_prefixes = ctx._opt_prefixes
@group.command()
@click.option("!e", is_flag=True)
def command2(e):
pass
runner.invoke(group, ["command1"])
assert opt_prefixes == {"-", "--", "~", "+"}

View file

@ -365,3 +365,11 @@ def test_parameter_source(runner, option_args, invoke_args, expect):
rv = runner.invoke(cli, standalone_mode=False, **invoke_args) rv = runner.invoke(cli, standalone_mode=False, **invoke_args)
assert rv.return_value == expect assert rv.return_value == expect
def test_propagate_opt_prefixes():
parent = click.Context(click.Command("test"))
parent._opt_prefixes = {"-", "--", "!"}
ctx = click.Context(click.Command("test2"), parent=parent)
assert ctx._opt_prefixes == {"-", "--", "!"}

View file

@ -322,12 +322,13 @@ def test_global_show_default(runner):
pass pass
result = runner.invoke(cli, ["--help"]) result = runner.invoke(cli, ["--help"])
# the default to "--help" is not shown because it is False
assert result.output.splitlines() == [ assert result.output.splitlines() == [
"Usage: cli [OPTIONS]", "Usage: cli [OPTIONS]",
"", "",
"Options:", "Options:",
" -f TEXT Output file name [default: out.txt]", " -f TEXT Output file name [default: out.txt]",
" --help Show this message and exit. [default: False]", " --help Show this message and exit.",
] ]

View file

@ -266,3 +266,10 @@ def test_context():
"ignore_unknown_options": False, "ignore_unknown_options": False,
"auto_envvar_prefix": None, "auto_envvar_prefix": None,
} }
def test_paramtype_no_name():
class TestType(click.ParamType):
pass
assert TestType().to_info_dict()["name"] == "TestType"

View file

@ -153,14 +153,15 @@ def test_init_bad_default_list(runner, multiple, nargs, default):
click.Option(["-a"], type=type, multiple=multiple, nargs=nargs, default=default) click.Option(["-a"], type=type, multiple=multiple, nargs=nargs, default=default)
def test_empty_envvar(runner): @pytest.mark.parametrize("env_key", ["MYPATH", "AUTO_MYPATH"])
def test_empty_envvar(runner, env_key):
@click.command() @click.command()
@click.option("--mypath", type=click.Path(exists=True), envvar="MYPATH") @click.option("--mypath", type=click.Path(exists=True), envvar="MYPATH")
def cli(mypath): def cli(mypath):
click.echo(f"mypath: {mypath}") click.echo(f"mypath: {mypath}")
result = runner.invoke(cli, [], env={"MYPATH": ""}) result = runner.invoke(cli, env={env_key: ""}, auto_envvar_prefix="AUTO")
assert result.exit_code == 0 assert result.exception is None
assert result.output == "mypath: None\n" assert result.output == "mypath: None\n"
@ -305,6 +306,19 @@ def test_dynamic_default_help_text(runner):
assert "(current user)" in result.output assert "(current user)" in result.output
def test_dynamic_default_help_special_method(runner):
class Value:
def __call__(self):
return 42
def __str__(self):
return "special value"
opt = click.Option(["-a"], default=Value(), show_default=True)
ctx = click.Context(click.Command("cli"))
assert "special value" in opt.get_help_record(ctx)[1]
@pytest.mark.parametrize( @pytest.mark.parametrize(
("type", "expect"), ("type", "expect"),
[ [
@ -489,6 +503,15 @@ def test_missing_option_string_cast():
assert str(excinfo.value) == "Missing parameter: a" assert str(excinfo.value) == "Missing parameter: a"
def test_missing_required_flag(runner):
cli = click.Command(
"cli", params=[click.Option(["--on/--off"], is_flag=True, required=True)]
)
result = runner.invoke(cli)
assert result.exit_code == 2
assert "Error: Missing option '--on'." in result.output
def test_missing_choice(runner): def test_missing_choice(runner):
@click.command() @click.command()
@click.option("--foo", type=click.Choice(["foo", "bar"]), required=True) @click.option("--foo", type=click.Choice(["foo", "bar"]), required=True)
@ -630,7 +653,6 @@ def test_option_custom_class_reusable(runner):
# Both of the commands should have the --help option now. # Both of the commands should have the --help option now.
for cmd in (cmd1, cmd2): for cmd in (cmd1, cmd2):
result = runner.invoke(cmd, ["--help"]) result = runner.invoke(cmd, ["--help"])
assert "I am a help text" in result.output assert "I am a help text" in result.output
assert "you wont see me" not in result.output assert "you wont see me" not in result.output
@ -723,16 +745,37 @@ def test_show_default_boolean_flag_name(runner, default, expect):
assert f"[default: {expect}]" in message assert f"[default: {expect}]" in message
def test_show_default_boolean_flag_value(runner): def test_show_true_default_boolean_flag_value(runner):
"""When a boolean flag only has one opt, it will show the default """When a boolean flag only has one opt and its default is True,
value, not the opt name. it will show the default value, not the opt name.
""" """
opt = click.Option( opt = click.Option(
("--cache",), is_flag=True, show_default=True, help="Enable the cache." ("--cache",),
is_flag=True,
show_default=True,
default=True,
help="Enable the cache.",
) )
ctx = click.Context(click.Command("test")) ctx = click.Context(click.Command("test"))
message = opt.get_help_record(ctx)[1] message = opt.get_help_record(ctx)[1]
assert "[default: False]" in message assert "[default: True]" in message
@pytest.mark.parametrize("default", [False, None])
def test_hide_false_default_boolean_flag_value(runner, default):
"""When a boolean flag only has one opt and its default is False or
None, it will not show the default
"""
opt = click.Option(
("--cache",),
is_flag=True,
show_default=True,
default=default,
help="Enable the cache.",
)
ctx = click.Context(click.Command("test"))
message = opt.get_help_record(ctx)[1]
assert "[default: " not in message
def test_show_default_string(runner): def test_show_default_string(runner):
@ -761,6 +804,28 @@ def test_do_not_show_default_empty_multiple():
assert message == "values" assert message == "values"
@pytest.mark.parametrize(
("ctx_value", "opt_value", "expect"),
[
(None, None, False),
(None, False, False),
(None, True, True),
(False, None, False),
(False, False, False),
(False, True, True),
(True, None, True),
(True, False, False),
(True, True, True),
(False, "one", True),
],
)
def test_show_default_precedence(ctx_value, opt_value, expect):
ctx = click.Context(click.Command("test"), show_default=ctx_value)
opt = click.Option("-a", default=1, help="value", show_default=opt_value)
help = opt.get_help_record(ctx)[1]
assert ("default:" in help) is expect
@pytest.mark.parametrize( @pytest.mark.parametrize(
("args", "expect"), ("args", "expect"),
[ [
@ -839,3 +904,21 @@ def test_type_from_flag_value():
) )
def test_is_bool_flag_is_correctly_set(option, expected): def test_is_bool_flag_is_correctly_set(option, expected):
assert option.is_bool_flag is expected assert option.is_bool_flag is expected
@pytest.mark.parametrize(
("kwargs", "message"),
[
({"count": True, "multiple": True}, "'count' is not valid with 'multiple'."),
({"count": True, "is_flag": True}, "'count' is not valid with 'is_flag'."),
(
{"multiple": True, "is_flag": True},
"'multiple' is not valid with 'is_flag', use 'count'.",
),
],
)
def test_invalid_flag_combinations(runner, kwargs, message):
with pytest.raises(TypeError) as e:
click.Option(["-a"], **kwargs)
assert message in str(e.value)

View file

@ -1,5 +1,7 @@
import pytest import pytest
import click
from click.parser import OptionParser
from click.parser import split_arg_string from click.parser import split_arg_string
@ -15,3 +17,16 @@ from click.parser import split_arg_string
) )
def test_split_arg_string(value, expect): def test_split_arg_string(value, expect):
assert split_arg_string(value) == expect assert split_arg_string(value) == expect
def test_parser_default_prefixes():
parser = OptionParser()
assert parser._opt_prefixes == {"-", "--"}
def test_parser_collects_prefixes():
ctx = click.Context(click.Command("test"))
parser = OptionParser(ctx)
click.Option("+p", is_flag=True).add_to_parser(parser, ctx)
click.Option("!e", is_flag=True).add_to_parser(parser, ctx)
assert parser._opt_prefixes == {"-", "--", "+", "!"}

View file

@ -110,6 +110,45 @@ def test_type_choice():
assert _get_words(cli, ["-c"], "a2") == ["a2"] assert _get_words(cli, ["-c"], "a2") == ["a2"]
def test_choice_special_characters():
cli = Command("cli", params=[Option(["-c"], type=Choice(["!1", "!2", "+3"]))])
assert _get_words(cli, ["-c"], "") == ["!1", "!2", "+3"]
assert _get_words(cli, ["-c"], "!") == ["!1", "!2"]
assert _get_words(cli, ["-c"], "!2") == ["!2"]
def test_choice_conflicting_prefix():
cli = Command(
"cli",
params=[
Option(["-c"], type=Choice(["!1", "!2", "+3"])),
Option(["+p"], is_flag=True),
],
)
assert _get_words(cli, ["-c"], "") == ["!1", "!2", "+3"]
assert _get_words(cli, ["-c"], "+") == ["+p"]
def test_option_count():
cli = Command("cli", params=[Option(["-c"], count=True)])
assert _get_words(cli, ["-c"], "") == []
assert _get_words(cli, ["-c"], "-") == ["--help"]
def test_option_optional():
cli = Command(
"cli",
add_help_option=False,
params=[
Option(["--name"], is_flag=False, flag_value="value"),
Option(["--flag"], is_flag=True),
],
)
assert _get_words(cli, ["--name"], "") == []
assert _get_words(cli, ["--name"], "-") == ["--flag"]
assert _get_words(cli, ["--name", "--flag"], "-") == []
@pytest.mark.parametrize( @pytest.mark.parametrize(
("type", "expect"), ("type", "expect"),
[(File(), "file"), (Path(), "file"), (Path(file_okay=False), "dir")], [(File(), "file"), (Path(), "file"), (Path(file_okay=False), "dir")],
@ -161,20 +200,6 @@ def test_option_custom():
assert _get_words(cli, ["a", "b"], "c") == ["C"] assert _get_words(cli, ["a", "b"], "c") == ["C"]
def test_autocompletion_deprecated():
# old function takes args and not param, returns all values, can mix
# strings and tuples
def custom(ctx, args, incomplete):
assert isinstance(args, list)
return [("art", "x"), "bat", "cat"]
with pytest.deprecated_call():
cli = Command("cli", params=[Argument(["x"], autocompletion=custom)])
assert _get_words(cli, [], "") == ["art", "bat", "cat"]
assert _get_words(cli, [], "c") == ["cat"]
def test_option_multiple(): def test_option_multiple():
cli = Command( cli = Command(
"type", "type",

View file

@ -420,17 +420,22 @@ def test_prompt_required_false(runner, args, expect):
@pytest.mark.parametrize( @pytest.mark.parametrize(
("prompt", "input", "expect"), ("prompt", "input", "default", "expect"),
[ [
(True, "password\npassword", "password"), (True, "password\npassword", None, "password"),
("Confirm Password", "password\npassword\n", "password"), ("Confirm Password", "password\npassword\n", None, "password"),
(False, None, None), (True, "", "", ""),
(False, None, None, None),
], ],
) )
def test_confirmation_prompt(runner, prompt, input, expect): def test_confirmation_prompt(runner, prompt, input, default, expect):
@click.command() @click.command()
@click.option( @click.option(
"--password", prompt=prompt, hide_input=True, confirmation_prompt=prompt "--password",
prompt=prompt,
hide_input=True,
default=default,
confirmation_prompt=prompt,
) )
def cli(password): def cli(password):
return password return password

View file

@ -320,6 +320,22 @@ def test_open_file(runner):
assert result.output == "foobar\nmeep\n" assert result.output == "foobar\nmeep\n"
def test_open_file_pathlib_dash(runner):
@click.command()
@click.argument(
"filename", type=click.Path(allow_dash=True, path_type=pathlib.Path)
)
def cli(filename):
click.echo(str(type(filename)))
with click.open_file(filename) as f:
click.echo(f.read())
result = runner.invoke(cli, ["-"], input="value")
assert result.exception is None
assert result.output == "pathlib.Path\nvalue\n"
def test_open_file_ignore_errors_stdin(runner): def test_open_file_ignore_errors_stdin(runner):
@click.command() @click.command()
@click.argument("filename") @click.argument("filename")
@ -428,20 +444,12 @@ class MockMain:
("example.py", None, "example.py"), ("example.py", None, "example.py"),
(str(pathlib.Path("/foo/bar/example.py")), None, "example.py"), (str(pathlib.Path("/foo/bar/example.py")), None, "example.py"),
("example", None, "example"), ("example", None, "example"),
( (str(pathlib.Path("example/__main__.py")), "example", "python -m example"),
str(pathlib.Path("example/__main__.py")), (str(pathlib.Path("example/cli.py")), "example", "python -m example.cli"),
MockMain(".example"),
"python -m example",
),
(
str(pathlib.Path("example/cli.py")),
MockMain(".example"),
"python -m example.cli",
),
], ],
) )
def test_detect_program_name(path, main, expected): def test_detect_program_name(path, main, expected):
assert click.utils._detect_program_name(path, _main=main) == expected assert click.utils._detect_program_name(path, _main=MockMain(main)) == expected
def test_expand_args(monkeypatch): def test_expand_args(monkeypatch):
@ -452,6 +460,8 @@ def test_expand_args(monkeypatch):
assert "setup.cfg" in click.utils._expand_args(["*.cfg"]) assert "setup.cfg" in click.utils._expand_args(["*.cfg"])
assert os.path.join("docs", "conf.py") in click.utils._expand_args(["**/conf.py"]) assert os.path.join("docs", "conf.py") in click.utils._expand_args(["**/conf.py"])
assert "*.not-found" in click.utils._expand_args(["*.not-found"]) assert "*.not-found" in click.utils._expand_args(["*.not-found"])
# a bad glob pattern, such as a pytest identifier, should return itself
assert click.utils._expand_args(["test.py::test_bad"])[0] == "test.py::test_bad"
@pytest.mark.parametrize( @pytest.mark.parametrize(

View file

@ -1,6 +1,6 @@
[tox] [tox]
envlist = envlist =
py{39,38,37,36,py3} py3{11,10,9,8,7},pypy3{8,7}
style style
typing typing
docs docs