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
updates:
- package-ecosystem: pip
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: monthly
time: "08:00"
open-pull-requests-limit: 99
interval: "monthly"
day: "monday"
time: "16:00"
timezone: "UTC"

View file

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

View file

@ -24,32 +24,27 @@ jobs:
fail-fast: false
matrix:
include:
- {name: Linux, python: '3.9', os: ubuntu-latest, tox: py39}
- {name: Windows, python: '3.9', os: windows-latest, tox: py39}
- {name: Mac, python: '3.9', os: macos-latest, tox: py39}
- {name: Linux, python: '3.10', os: ubuntu-latest, tox: py310}
- {name: Windows, python: '3.10', os: windows-latest, tox: py310}
- {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.7', python: '3.7', os: ubuntu-latest, tox: py37}
- {name: '3.6', python: '3.6', os: ubuntu-latest, tox: py36}
- {name: 'PyPy', python: pypy3, os: ubuntu-latest, tox: pypy3}
- {name: Typing, python: '3.9', os: ubuntu-latest, tox: typing}
- {name: 'PyPy', python: 'pypy-3.7', os: ubuntu-latest, tox: pypy37}
- {name: Typing, python: '3.10', os: ubuntu-latest, tox: typing}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python }}
cache: 'pip'
cache-dependency-path: 'requirements/*.txt'
- name: update pip
run: |
pip install -U wheel
pip install -U setuptools
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
uses: actions/cache@v2
with:

1
.gitignore vendored
View file

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

View file

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

View file

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

View file

@ -1,5 +1,115 @@
.. 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
-------------

BIN
docs/.DS_Store vendored

Binary file not shown.

View file

@ -63,8 +63,6 @@ Utilities
.. autofunction:: pause
.. autofunction:: get_terminal_size
.. autofunction:: get_binary_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'])
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
-------------------
@ -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
:ref:`callback-evaluation-order`.
A callback is a function that is invoked with two parameters: the current
:class:`Context` and the value. The context provides some useful features
such as quitting the application and gives access to other already
processed parameters.
A callback is a function that is invoked with three parameters: the
current :class:`Context`, the current :class:`Parameter`, and the value.
The context provides some useful features such as quitting the
application and gives access to other already processed parameters.
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.
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`.
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
class EnvVarType(ParamType):
name = "envvar"
def shell_complete(self, ctx, param, incomplete):
return [
CompletionItem(name)

View file

@ -9,4 +9,4 @@ Click Examples
through the wrong interpreter.
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 tests.in
-r typing.in
pip-tools
pip-compile-multi
pre-commit
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:
#
# pip-compile requirements/dev.in
# pip-compile-multi
#
alabaster==0.7.12
# via sphinx
attrs==21.2.0
# via pytest
babel==2.9.1
# via sphinx
backports.entry-points-selectable==1.1.0
# via virtualenv
certifi==2021.5.30
# via requests
-r docs.txt
-r tests.txt
-r typing.txt
cfgv==3.3.1
# via pre-commit
charset-normalizer==2.0.6
# via requests
click==8.0.1
# via pip-tools
distlib==0.3.3
# via virtualenv
docutils==0.16
click==8.1.2
# via
# sphinx
# sphinx-tabs
filelock==3.3.0
# pip-compile-multi
# pip-tools
distlib==0.3.4
# via virtualenv
filelock==3.6.0
# via
# tox
# virtualenv
identify==2.3.0
identify==2.5.0
# 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
# via pre-commit
packaging==21.0
# via
# pallets-sphinx-themes
# pytest
# sphinx
# tox
pallets-sphinx-themes==2.0.1
# via -r requirements/docs.in
pep517==0.11.0
pep517==0.12.0
# via pip-tools
pip-tools==6.3.0
pip-compile-multi==2.4.5
# via -r requirements/dev.in
platformdirs==2.4.0
pip-tools==6.6.0
# via pip-compile-multi
platformdirs==2.5.2
# via virtualenv
pluggy==1.0.0
# via
# pytest
# tox
pre-commit==2.15.0
pre-commit==2.18.1
# via -r requirements/dev.in
py==1.10.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
pyyaml==6.0
# via pre-commit
requests==2.26.0
# via sphinx
six==1.16.0
# via
# tox
# 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
# via
# mypy
# pre-commit
# pytest
# tox
tomli==1.2.1
# via pep517
tox==3.24.4
toposort==1.7
# via pip-compile-multi
tox==3.25.0
# via -r requirements/dev.in
typing-extensions==3.10.0.2
# via mypy
urllib3==1.26.7
# via requests
virtualenv==20.8.1
virtualenv==20.14.1
# via
# pre-commit
# tox
wheel==0.37.0
wheel==0.37.1
# via pip-tools
# 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:
#
# pip-compile requirements/docs.in
# pip-compile-multi
#
alabaster==0.7.12
# via sphinx
babel==2.9.1
babel==2.10.1
# via sphinx
certifi==2021.5.30
certifi==2021.10.8
# via requests
charset-normalizer==2.0.6
charset-normalizer==2.0.12
# via requests
docutils==0.16
docutils==0.17.1
# via
# sphinx
# sphinx-tabs
idna==3.2
idna==3.3
# via requests
imagesize==1.2.0
imagesize==1.3.0
# via sphinx
jinja2==3.0.2
jinja2==3.1.2
# via sphinx
markupsafe==2.0.1
markupsafe==2.1.1
# via jinja2
packaging==21.0
packaging==21.3
# via
# pallets-sphinx-themes
# sphinx
pallets-sphinx-themes==2.0.1
pallets-sphinx-themes==2.0.2
# via -r requirements/docs.in
pygments==2.10.0
pygments==2.12.0
# via
# sphinx
# sphinx-tabs
pyparsing==2.4.7
pyparsing==3.0.8
# via packaging
pytz==2021.3
pytz==2022.1
# via babel
requests==2.26.0
requests==2.27.1
# via sphinx
snowballstemmer==2.1.0
snowballstemmer==2.2.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
sphinx==4.5.0
# via
# -r requirements/docs.in
# pallets-sphinx-themes
# sphinx-issues
# sphinx-tabs
# 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
# via sphinx
sphinxcontrib-devhelp==1.0.2
@ -67,8 +68,5 @@ sphinxcontrib-qthelp==1.0.3
# via sphinx
sphinxcontrib-serializinghtml==1.1.5
# via sphinx
urllib3==1.26.7
urllib3==1.26.9
# 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:
#
# pip-compile requirements/tests.in
# pip-compile-multi
#
attrs==21.2.0
attrs==21.4.0
# via pytest
iniconfig==1.1.1
# via pytest
packaging==21.0
packaging==21.3
# via pytest
pluggy==1.0.0
# via pytest
py==1.10.0
py==1.11.0
# via pytest
pyparsing==2.4.7
pyparsing==3.0.8
# via packaging
pytest==6.2.5
pytest==7.1.2
# via -r requirements/tests.in
toml==0.10.2
tomli==2.0.1
# 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:
#
# pip-compile requirements/typing.in
# pip-compile-multi
#
mypy==0.950
# via -r requirements/typing.in
mypy-extensions==0.4.3
# via mypy
mypy==0.910
# via -r requirements/typing.in
toml==0.10.2
tomli==2.0.1
# via mypy
typing-extensions==3.10.0.2
typing-extensions==4.2.0
# via mypy

View file

@ -29,8 +29,8 @@ classifiers =
[options]
packages = find:
package_dir = = src
include_package_data = true
python_requires = >= 3.6
include_package_data = True
python_requires = >= 3.7
# Dependencies are in setup.py for GitHub's dependency graph.
[options.packages.find]
@ -42,14 +42,14 @@ filterwarnings =
error
[coverage:run]
branch = true
branch = True
source =
click
tests
[coverage:paths]
source =
click
src
*/site-packages
[flake8]
@ -57,7 +57,7 @@ source =
# E = pycodestyle errors
# F = flake8 pyflakes
# W = pycodestyle warnings
# B9 = bugbear opinions,
# B9 = bugbear opinions
# ISC = implicit str concat
select = B, E, F, W, B9, ISC
ignore =
@ -72,12 +72,13 @@ ignore =
# up to 88 allowed by bugbear B950
max-line-length = 80
per-file-ignores =
# __init__ module exports names
# __init__ exports names
src/click/__init__.py: F401
[mypy]
files = src/click
python_version = 3.6
python_version = 3.7
show_error_codes = True
disallow_subclassing_any = True
disallow_untyped_calls = 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 echo_via_pager as echo_via_pager
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 launch as launch
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 get_app_dir as get_app_dir
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 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]:
binary = "b" in mode
# Standard streams first. These are simple because they don't need
# special handling for the atomic flag. It's entirely ignored.
if filename == "-":
# Standard streams first. These are simple because they ignore the
# atomic flag. Use fsdecode to handle Path("-").
if os.fsdecode(filename) == "-":
if any(m in mode for m in ["w", "a", "x"]):
if binary:
return get_binary_stdout(), False
@ -561,7 +561,6 @@ if sys.platform.startswith("win") and WIN:
return rv
else:
def _get_argv_encoding() -> str:

View file

@ -675,7 +675,6 @@ if WIN:
_translate_ch_to_exc(rv)
return rv
else:
import tty
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 errno
import inspect
import os
import sys
import typing
import typing as t
from collections import abc
from contextlib import contextmanager
@ -14,7 +14,6 @@ from gettext import ngettext
from itertools import repeat
from . import types
from ._unicodefun import _verify_python_env
from .exceptions import Abort
from .exceptions import BadParameter
from .exceptions import ClickException
@ -224,9 +223,14 @@ class Context:
codes are used in texts that Click prints which is by
default not the case. This for instance would affect
help output.
:param show_default: Show defaults for all options. If not set,
defaults to the value from a parent context. Overrides an
option's ``show_default`` argument.
:param show_default: Show the default value for commands. If this
value is not set, it defaults to the value from the parent
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
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
#: to implement nested parsing.
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:
obj = parent.obj
@ -632,13 +638,13 @@ class Context:
self.obj = rv = object_type()
return rv
@typing.overload
@t.overload
def lookup_default(
self, name: str, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]:
...
@typing.overload
@t.overload
def lookup_default(
self, name: str, call: "te.Literal[False]" = ...
) -> t.Optional[t.Union[t.Any, t.Callable[[], t.Any]]]:
@ -956,7 +962,7 @@ class BaseCommand:
return results
@typing.overload
@t.overload
def main(
self,
args: t.Optional[t.Sequence[str]] = None,
@ -967,7 +973,7 @@ class BaseCommand:
) -> "te.NoReturn":
...
@typing.overload
@t.overload
def main(
self,
args: t.Optional[t.Sequence[str]] = None,
@ -1029,10 +1035,6 @@ class BaseCommand:
.. versionchanged:: 3.0
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:
args = sys.argv[1:]
@ -1133,13 +1135,6 @@ class Command(BaseCommand):
Click. A basic command handles command line parsing and might dispatch
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 context_settings: an optional dictionary with defaults that are
passed to the context object.
@ -1161,6 +1156,20 @@ class Command(BaseCommand):
:param deprecated: issues a message indicating that
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__(
@ -1186,12 +1195,6 @@ class Command(BaseCommand):
#: should show up in the help page and execute. Eager parameters
#: will automatically be handled before non eager ones.
self.params: t.List["Parameter"] = params or []
# 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.epilog = epilog
self.options_metavar = options_metavar
@ -1299,10 +1302,12 @@ class Command(BaseCommand):
"""Gets short help for the command or makes it by shortening the
long help string.
"""
text = self.short_help or ""
if not text and self.help:
if self.short_help:
text = inspect.cleandoc(self.short_help)
elif self.help:
text = make_default_short_help(self.help, limit)
else:
text = ""
if self.deprecated:
text = _("(Deprecated) {text}").format(text=text)
@ -1328,12 +1333,13 @@ class Command(BaseCommand):
def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None:
"""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:
text = _("(Deprecated) {text}").format(text=text)
if text:
text = inspect.cleandoc(text).partition("\f")[0]
formatter.write_paragraph()
with formatter.indentation():
@ -1354,9 +1360,11 @@ class Command(BaseCommand):
def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None:
"""Writes the epilog into the formatter if it exists."""
if self.epilog:
epilog = inspect.cleandoc(self.epilog)
formatter.write_paragraph()
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]:
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._opt_prefixes.update(parser._opt_prefixes)
return args
def invoke(self, ctx: Context) -> t.Any:
@ -1568,17 +1577,6 @@ class MultiCommand(Command):
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:
"""Extra format methods for multi methods that adds all the commands
after the options.
@ -1631,11 +1629,11 @@ class MultiCommand(Command):
if not ctx.protected_args:
if self.invoke_without_command:
# No subcommand was invoked, so the result callback is
# invoked with None for regular groups, or an empty list
# for chained groups.
# invoked with the group return value for regular
# groups, or an empty list for chained groups.
with ctx:
super().invoke(ctx)
return _process_result([] if self.chain else None)
rv = super().invoke(ctx)
return _process_result([] if self.chain else rv)
ctx.fail(_("Missing command."))
# Fetch args back out
@ -1811,9 +1809,19 @@ class Group(MultiCommand):
_check_multicommand(self, name, cmd, register=True)
self.commands[name] = cmd
@t.overload
def command(self, __func: t.Callable[..., t.Any]) -> Command:
...
@t.overload
def command(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], Command]:
...
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
the group. This takes the same arguments as :func:`command` and
immediately registers the created command with this group by
@ -1822,24 +1830,49 @@ class Group(MultiCommand):
To customize the command class used, set the
:attr:`command_class` attribute.
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
.. versionchanged:: 8.0
Added the :attr:`command_class` attribute.
"""
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
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:
cmd = command(*args, **kwargs)(f)
cmd: Command = command(*args, **kwargs)(f)
self.add_command(cmd)
return cmd
if func is not None:
return decorator(func)
return decorator
@t.overload
def group(self, __func: t.Callable[..., t.Any]) -> "Group":
...
@t.overload
def group(
self, *args: t.Any, **kwargs: t.Any
) -> t.Callable[[t.Callable[..., t.Any]], "Group"]:
...
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
the group. This takes the same arguments as :func:`group` and
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`
attribute.
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
.. versionchanged:: 8.0
Added the :attr:`group_class` attribute.
"""
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:
kwargs["cls"] = type(self)
else:
kwargs["cls"] = self.group_class
def decorator(f: t.Callable[..., t.Any]) -> "Group":
cmd = group(*args, **kwargs)(f)
cmd: Group = group(*args, **kwargs)(f)
self.add_command(cmd)
return cmd
if func is not None:
return decorator(func)
return decorator
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]],
]
] = None,
autocompletion: t.Optional[
t.Callable[
[Context, t.List[str], str], t.List[t.Union[t.Tuple[str, str], str]]
]
] = None,
) -> None:
self.name, self.opts, self.secondary_opts = self._parse_decls(
param_decls or (), expose_value
@ -2048,36 +2091,6 @@ class Parameter:
self.is_eager = is_eager
self.metavar = metavar
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
if __debug__:
@ -2172,13 +2185,13 @@ class Parameter:
return metavar
@typing.overload
@t.overload
def get_default(
self, ctx: Context, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]:
...
@typing.overload
@t.overload
def get_default(
self, ctx: Context, call: bool = ...
) -> 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.
:param show_default: controls if the default value should be shown on the
help page. Normally, defaults are not shown. If this
value is a string, it shows the string instead of the
value. This is particularly useful for dynamic options.
:param show_envvar: controls if an environment variable should be shown on
the help page. Normally, environment variables
are not 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 show_default: Show the default value for this option in its
help text. Values are not shown by default, unless
:attr:`Context.show_default` is ``True``. If this value is a
string, it shows that string in parentheses instead of the
actual value. This is particularly useful for dynamic options.
For single option boolean flags, the default remains hidden if
its value is ``False``.
:param show_envvar: Controls if an environment variable should be
shown on the help page. Normally, environment variables are not
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
value if it was prompted for. Can be set to a string instead of
``True`` to customize the message.
:param prompt_required: If set to ``False``, the user will be
prompted for input only when the option was specified as a flag
without a value.
:param hide_input: if this is `True` then the input on the prompt will be
hidden from the user. This is useful for password
input.
:param hide_input: If this is ``True`` then the input on the prompt
will be hidden from the user. This is useful for password input.
:param is_flag: forces this option to act as a flag. The default is
auto detection.
: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 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
``type`` is detected from ``flag_value`` if given.
"""
@ -2444,7 +2471,7 @@ class Option(Parameter):
def __init__(
self,
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,
confirmation_prompt: t.Union[bool, str] = False,
prompt_required: bool = True,
@ -2461,6 +2488,9 @@ class Option(Parameter):
show_envvar: bool = False,
**attrs: t.Any,
) -> None:
if help:
help = inspect.cleandoc(help)
default_is_missing = "default" not in attrs
super().__init__(param_decls, type=type, multiple=multiple, **attrs)
@ -2472,7 +2502,7 @@ class Option(Parameter):
elif prompt is False:
prompt_text = None
else:
prompt_text = t.cast(str, prompt)
prompt_text = prompt
self.prompt = prompt_text
self.confirmation_prompt = confirmation_prompt
@ -2499,7 +2529,7 @@ class Option(Parameter):
# flag if flag_value is set.
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
if flag_value is None:
@ -2550,6 +2580,9 @@ class Option(Parameter):
if self.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]:
info_dict = super().to_info_dict()
info_dict.update(
@ -2711,16 +2744,23 @@ class Option(Parameter):
finally:
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 (
default_value is not None and (self.show_default or ctx.show_default)
):
if self.show_default is not None:
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:
default_string = f"({self.show_default})"
elif isinstance(default_value, (list, tuple)):
default_string = ", ".join(str(d) for d in default_value)
elif callable(default_value):
elif inspect.isfunction(default_value):
default_string = _("(dynamic)")
elif self.is_bool_flag and self.secondary_opts:
# For boolean flags that have distinct True/False opts,
@ -2728,6 +2768,8 @@ class Option(Parameter):
default_string = split_opt(
(self.opts if self.default else self.secondary_opts)[0]
)[1]
elif self.is_bool_flag and not self.secondary_opts and not default_value:
default_string = ""
else:
default_string = str(default_value)
@ -2753,13 +2795,13 @@ class Option(Parameter):
return ("; " if any_prefix_is_slash else " / ").join(rv), help
@typing.overload
@t.overload
def get_default(
self, ctx: Context, call: "te.Literal[True]" = True
) -> t.Optional[t.Any]:
...
@typing.overload
@t.overload
def get_default(
self, ctx: Context, call: bool = ...
) -> 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]]]:
# 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
# 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.
if self.is_flag and not self.is_bool_flag:
for param in ctx.command.params:
@ -2821,7 +2863,10 @@ class Option(Parameter):
envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}"
rv = os.environ.get(envvar)
return rv
if rv:
return rv
return None
def value_from_envvar(self, ctx: Context) -> t.Optional[t.Any]:
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
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:
@ -121,43 +121,38 @@ def pass_meta_key(
return decorator
def _make_command(
f: F,
name: t.Optional[str],
attrs: t.MutableMapping[str, t.Any],
cls: t.Type[Command],
CmdType = t.TypeVar("CmdType", bound=Command)
@t.overload
def command(
__func: t.Callable[..., t.Any],
) -> 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
return cls(
name=name or f.__name__.lower().replace("_", "-"),
callback=f,
params=params,
**attrs,
)
@t.overload
def command(
name: t.Optional[str] = None,
cls: t.Type[CmdType] = ...,
**attrs: t.Any,
) -> t.Callable[..., CmdType]:
...
def command(
name: t.Optional[str] = None,
name: t.Union[str, t.Callable[..., t.Any], None] = None,
cls: t.Optional[t.Type[Command]] = None,
**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
callback. This will also automatically attach all decorated
: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.
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
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.
:param cls: the command class to instantiate. This defaults to
: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:
cls = 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__
return cmd
if func is not None:
return decorator(func)
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
works otherwise the same as :func:`command` just that the `cls`
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))
@ -219,7 +283,7 @@ def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[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))
return f
@ -240,10 +304,7 @@ def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
def decorator(f: FC) -> FC:
# Issue 926, copy attrs, so pre-defined options can re-use the same cls=
option_attrs = attrs.copy()
if "help" in option_attrs:
option_attrs["help"] = inspect.cleandoc(option_attrs["help"])
OptionClass = option_attrs.pop("cls", Option)
OptionClass = option_attrs.pop("cls", None) or Option
_param_memo(f, OptionClass(param_decls, **option_attrs))
return f

View file

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

View file

@ -102,10 +102,10 @@ _SOURCE_BASH = """\
IFS=',' read type value <<< "$completion"
if [[ $type == 'dir' ]]; then
COMREPLY=()
COMPREPLY=()
compopt -o dirnames
elif [[ $type == 'file' ]]; then
COMREPLY=()
COMPREPLY=()
compopt -o default
elif [[ $type == 'plain' ]]; then
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."""
if not value:
return False
c = value[0]
# Allow "/" since that starts a path.
return not c.isalnum() and c != "/"
return c in ctx._opt_prefixes
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.
: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):
return False
if param.is_flag:
if param.is_flag or param.count:
return False
last_option = None
@ -476,7 +475,7 @@ def _is_incomplete_option(args: t.List[str], param: Parameter) -> bool:
if index + 1 > param.nargs:
break
if _start_of_option(arg):
if _start_of_option(ctx, arg):
last_option = arg
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.
if incomplete == "=":
incomplete = ""
elif "=" in incomplete and _start_of_option(incomplete):
elif "=" in incomplete and _start_of_option(ctx, incomplete):
name, _, incomplete = incomplete.partition("=")
args.append(name)
@ -559,7 +558,7 @@ def _resolve_incomplete(
# even if they start with the option character. If it hasn't been
# given and the incomplete arg looks like an option, the current
# 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
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
# value, the option will provide value completions.
for param in params:
if _is_incomplete_option(args, param):
if _is_incomplete_option(ctx, args, param):
return param, incomplete
# It's not an option name or value. The first argument without a

View file

@ -3,7 +3,6 @@ import io
import itertools
import os
import sys
import typing
import typing as t
from gettext import gettext as _
@ -94,7 +93,7 @@ def prompt(
"""Prompts a user for input. This is a convenience function that can
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.
:param text: the text to show for the prompt.
@ -160,7 +159,6 @@ def prompt(
if confirmation_prompt is True:
confirmation_prompt = _("Repeat for confirmation")
confirmation_prompt = t.cast(str, confirmation_prompt)
confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix)
while True:
@ -182,9 +180,9 @@ def prompt(
if not confirmation_prompt:
return result
while True:
confirmation_prompt = t.cast(str, confirmation_prompt)
value2 = prompt_func(confirmation_prompt)
if value2:
is_empty = not value and not value2
if value2 or is_empty:
break
if value == value2:
return result
@ -252,26 +250,6 @@ def confirm(
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(
text_or_generator: t.Union[t.Iterable[str], t.Callable[[], t.Iterable[str]], str],
color: t.Optional[bool] = None,
@ -627,7 +605,7 @@ def unstyle(text: str) -> str:
def secho(
message: t.Optional[t.Any] = None,
file: t.Optional[t.IO] = None,
file: t.Optional[t.IO[t.AnyStr]] = None,
nl: bool = True,
err: bool = False,
color: t.Optional[bool] = None,

View file

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

View file

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

View file

@ -1,4 +1,5 @@
import os
import re
import sys
import typing as t
from functools import update_wrapper
@ -203,7 +204,7 @@ class KeepOpenFile:
def echo(
message: t.Optional[t.Any] = None,
file: t.Optional[t.IO] = None,
file: t.Optional[t.IO[t.Any]] = None,
nl: bool = True,
err: bool = False,
color: t.Optional[bool] = None,
@ -340,55 +341,45 @@ def open_file(
lazy: bool = False,
atomic: bool = False,
) -> t.IO:
"""This is similar to how the :class:`File` works but for manual
usage. Files are opened non lazy by default. This can open regular
files as well as stdin/stdout if ``'-'`` is passed.
"""Open a file, with extra behavior to handle ``'-'`` to indicate
a standard stream, lazy open on write, and atomic write. Similar to
the behavior of the :class:`~click.File` param type.
If stdin/stdout is returned the stream is wrapped so that the context
manager will not close the stream accidentally. This makes it possible
to always use the function like this without having to worry to
accidentally close a standard stream::
If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is
wrapped so that using it in a context manager will not close it.
This makes it possible to use the function without accidentally
closing a standard stream:
.. code-block:: python
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).
: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.
.. versionadded:: 3.0
"""
if lazy:
return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic))
f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic)
if not should_close:
f = t.cast(t.IO, KeepOpenFile(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(
filename: t.Union[str, bytes, os.PathLike], shorten: bool = False
) -> str:
@ -484,7 +475,7 @@ class PacifyFlushWrapper:
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:
"""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
@ -506,6 +497,9 @@ def _detect_program_name(
:meta private:
"""
if _main is None:
_main = sys.modules["__main__"]
if not path:
path = sys.argv[0]
@ -546,7 +540,7 @@ def _expand_args(
See :func:`glob.glob`, :func:`os.path.expanduser`, and
: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.
:param args: List of command line arguments to expand.
@ -554,6 +548,10 @@ def _expand_args(
:param env: Expand environment variables.
: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
:meta private:
@ -569,7 +567,10 @@ def _expand_args(
if env:
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:
out.append(arg)

View file

@ -41,25 +41,25 @@ def test_nargs_tup(runner):
assert result.output.splitlines() == ["name=peter", "point=1/2"]
def test_nargs_tup_composite(runner):
variations = [
@pytest.mark.parametrize(
"opts",
[
dict(type=(str, int)),
dict(type=click.Tuple([str, int])),
dict(nargs=2, type=click.Tuple([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:
@click.command()
@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"]
result = runner.invoke(copy, ["peter", "1"])
assert result.exception is None
assert result.output.splitlines() == ["name=peter id=1"]
def test_nargs_err(runner):
@ -120,9 +120,9 @@ def test_file_args(runner):
assert result.exit_code == 0
def test_path_args(runner):
def test_path_allow_dash(runner):
@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):
click.echo(input)

View file

@ -1,5 +1,4 @@
import os
import uuid
from itertools import chain
import pytest
@ -104,125 +103,135 @@ def test_group_from_list(runner):
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.option("--foo", default="no value")
def cli(foo):
click.echo(f"FOO:[{foo}]")
@click.option("--s", default="no value")
def cli(s):
click.echo(f"S:[{s}]")
result = runner.invoke(cli, [])
assert not result.exception
assert "FOO:[no value]" in result.output
result = runner.invoke(cli, args)
assert expect in result.output
result = runner.invoke(cli, ["--foo=42"])
assert not result.exception
assert "FOO:[42]" in result.output
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
if expect.startswith("Error:"):
assert result.exception is not None
else:
assert result.exception is None
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.option("--foo", default=42)
def cli(foo):
click.echo(f"FOO:[{foo * 2}]")
@click.option("--i", default=42)
def cli(i):
click.echo(f"I:[{i * 2}]")
result = runner.invoke(cli, [])
assert not result.exception
assert "FOO:[84]" in result.output
result = runner.invoke(cli, args)
assert expect in result.output
result = runner.invoke(cli, ["--foo=23"])
assert not result.exception
assert "FOO:[46]" in result.output
result = runner.invoke(cli, ["--foo=bar"])
assert result.exception
assert "Invalid value for '--foo': 'bar' is not a valid integer." in result.output
if expect.startswith("Error:"):
assert result.exception is not None
else:
assert result.exception is None
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.option(
"--u", default="ba122011-349f-423b-873b-9d6a79c688ab", type=click.UUID
)
def cli(u):
assert type(u) is uuid.UUID
click.echo(f"U:[{u}]")
result = runner.invoke(cli, [])
assert not result.exception
assert "U:[ba122011-349f-423b-873b-9d6a79c688ab]" in result.output
result = runner.invoke(cli, args)
assert expect in result.output
result = runner.invoke(cli, ["--u=821592c1-c50e-4971-9cd6-e89dc6832f86"])
assert not result.exception
assert "U:[821592c1-c50e-4971-9cd6-e89dc6832f86]" in result.output
result = runner.invoke(cli, ["--u=bar"])
assert result.exception
assert "Invalid value for '--u': 'bar' is not a valid UUID." in result.output
if expect.startswith("Error:"):
assert result.exception is not None
else:
assert result.exception is None
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.option("--foo", default=42, type=click.FLOAT)
def cli(foo):
assert type(foo) is float
click.echo(f"FOO:[{foo}]")
@click.option("--f", default=42.0)
def cli(f):
click.echo(f"F:[{f}]")
result = runner.invoke(cli, [])
assert not result.exception
assert "FOO:[42.0]" in result.output
result = runner.invoke(cli, args)
assert expect in result.output
result = runner.invoke(cli, ["--foo=23.5"])
assert not result.exception
assert "FOO:[23.5]" in result.output
result = runner.invoke(cli, ["--foo=bar"])
assert result.exception
assert "Invalid value for '--foo': 'bar' is not a valid float." in result.output
if expect.startswith("Error:"):
assert result.exception is not None
else:
assert result.exception is None
def test_boolean_option(runner):
for default in True, False:
@pytest.mark.parametrize("default", [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()
@click.option("--with-foo/--without-foo", default=default)
def cli(with_foo):
click.echo(with_foo)
if expect is None:
expect = default
result = runner.invoke(cli, ["--with-foo"])
assert not result.exception
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"
result = runner.invoke(cli, args, standalone_mode=False)
assert result.return_value is expect
for default in True, False:
@click.command()
@click.option("--flag", is_flag=True, default=default)
def cli(flag):
click.echo(flag)
@pytest.mark.parametrize("default", [True, False])
@pytest.mark.parametrize(("args", "expect"), [(["--f"], True), ([], False)])
def test_boolean_flag(runner, default, args, expect):
@click.command()
@click.option("--f", is_flag=True, default=default)
def cli(f):
return f
result = runner.invoke(cli, ["--flag"])
assert not result.exception
assert result.output == f"{not default}\n"
result = runner.invoke(cli, [])
assert not result.exception
assert result.output == f"{default}\n"
if default:
expect = not expect
result = runner.invoke(cli, args, standalone_mode=False)
assert result.return_value is expect
@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)
def cli():
"""ROOT HELP"""
@ -50,22 +60,9 @@ def test_chaining_help(runner):
"""BDIST HELP"""
click.echo("bdist called")
result = runner.invoke(cli, ["--help"])
result = runner.invoke(cli, args)
assert not result.exception
assert "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." 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
assert expect in result.output
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"]
@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):
"""When a group has ``invoke_without_command=True``, the result
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)
def cli():
pass
return 1
@cli.result_callback()
def process_result(result):
click.echo(str(result), nl=False)
click.echo(result, nl=False)
result = runner.invoke(cli, [])
assert result.output == expect
@ -127,15 +124,23 @@ def test_chaining_with_arguments(runner):
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.option("-i", "--input", type=click.File("r"))
def cli(input):
@click.option("-f", type=click.File("r"))
def cli(f):
pass
@cli.result_callback()
def process_pipeline(processors, input):
iterator = (x.rstrip("\r\n") for x in input)
def process_pipeline(processors, f):
iterator = (x.rstrip("\r\n") for x in f)
for processor in processors:
iterator = processor(iterator)
for item in iterator:
@ -157,17 +162,9 @@ def test_pipeline(runner):
return processor
result = runner.invoke(cli, ["-i", "-"], input="foo\nbar")
result = runner.invoke(cli, args, input=input)
assert not result.exception
assert result.output.splitlines() == ["foo", "bar"]
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"]
assert result.output.splitlines() == expect
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 pytest
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):
@click.command(no_args_is_help=True)
def cli():
@ -119,7 +135,16 @@ def test_default_maps(runner):
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.argument("obj")
def cli(obj):
@ -129,21 +154,9 @@ def test_group_with_args(runner):
def move():
click.echo("move")
result = runner.invoke(cli, [])
assert result.exit_code == 0
assert "Show this message and exit." 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"
result = runner.invoke(cli, args)
assert result.exit_code == exit_code
assert expect in result.output
def test_base_command(runner):
@ -196,18 +209,12 @@ def test_base_command(runner):
cli.add_command(OptParseCommand("test", parser, test_callback))
result = runner.invoke(
cli, ["test", "-f", "test.txt", "-q", "whatever.txt", "whateverelse.txt"]
)
assert not result.exception
assert result.output.splitlines() == [
"whatever.txt whateverelse.txt",
"test.txt",
"False",
]
result = runner.invoke(cli, ["test", "-f", "f.txt", "-q", "q1.txt", "q2.txt"])
assert result.exception is None
assert result.output.splitlines() == ["q1.txt q2.txt", "f.txt", "False"]
result = runner.invoke(cli, ["test", "--help"])
assert not result.exception
assert result.exception is None
assert result.output.splitlines() == [
"Usage: foo test [OPTIONS]",
"",
@ -328,20 +335,13 @@ def test_unprocessed_options(runner):
]
def test_deprecated_in_help_messages(runner):
@click.command(deprecated=True)
def cmd_with_help():
"""CLI HELP"""
@pytest.mark.parametrize("doc", ["CLI HELP", None])
def test_deprecated_in_help_messages(runner, doc):
@click.command(deprecated=True, help=doc)
def cli():
pass
result = runner.invoke(cmd_with_help, ["--help"])
assert "(Deprecated)" in result.output
@click.command(deprecated=True)
def cmd_without_help():
pass
result = runner.invoke(cmd_without_help, ["--help"])
result = runner.invoke(cli, ["--help"])
assert "(Deprecated)" in result.output
@ -352,3 +352,62 @@ def test_deprecated_in_invocation(runner):
result = runner.invoke(deprecated_cmd)
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)
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
result = runner.invoke(cli, ["--help"])
# the default to "--help" is not shown because it is False
assert result.output.splitlines() == [
"Usage: cli [OPTIONS]",
"",
"Options:",
" -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,
"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)
def test_empty_envvar(runner):
@pytest.mark.parametrize("env_key", ["MYPATH", "AUTO_MYPATH"])
def test_empty_envvar(runner, env_key):
@click.command()
@click.option("--mypath", type=click.Path(exists=True), envvar="MYPATH")
def cli(mypath):
click.echo(f"mypath: {mypath}")
result = runner.invoke(cli, [], env={"MYPATH": ""})
assert result.exit_code == 0
result = runner.invoke(cli, env={env_key: ""}, auto_envvar_prefix="AUTO")
assert result.exception is None
assert result.output == "mypath: None\n"
@ -305,6 +306,19 @@ def test_dynamic_default_help_text(runner):
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(
("type", "expect"),
[
@ -489,6 +503,15 @@ def test_missing_option_string_cast():
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):
@click.command()
@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.
for cmd in (cmd1, cmd2):
result = runner.invoke(cmd, ["--help"])
assert "I am a help text" 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
def test_show_default_boolean_flag_value(runner):
"""When a boolean flag only has one opt, it will show the default
value, not the opt name.
def test_show_true_default_boolean_flag_value(runner):
"""When a boolean flag only has one opt and its default is True,
it will show the default value, not the opt name.
"""
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"))
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):
@ -761,6 +804,28 @@ def test_do_not_show_default_empty_multiple():
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(
("args", "expect"),
[
@ -839,3 +904,21 @@ def test_type_from_flag_value():
)
def test_is_bool_flag_is_correctly_set(option, 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 click
from click.parser import OptionParser
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):
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"]
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(
("type", "expect"),
[(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"]
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():
cli = Command(
"type",

View file

@ -420,17 +420,22 @@ def test_prompt_required_false(runner, args, expect):
@pytest.mark.parametrize(
("prompt", "input", "expect"),
("prompt", "input", "default", "expect"),
[
(True, "password\npassword", "password"),
("Confirm Password", "password\npassword\n", "password"),
(False, None, None),
(True, "password\npassword", None, "password"),
("Confirm Password", "password\npassword\n", None, "password"),
(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.option(
"--password", prompt=prompt, hide_input=True, confirmation_prompt=prompt
"--password",
prompt=prompt,
hide_input=True,
default=default,
confirmation_prompt=prompt,
)
def cli(password):
return password

View file

@ -320,6 +320,22 @@ def test_open_file(runner):
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):
@click.command()
@click.argument("filename")
@ -428,20 +444,12 @@ class MockMain:
("example.py", None, "example.py"),
(str(pathlib.Path("/foo/bar/example.py")), None, "example.py"),
("example", None, "example"),
(
str(pathlib.Path("example/__main__.py")),
MockMain(".example"),
"python -m example",
),
(
str(pathlib.Path("example/cli.py")),
MockMain(".example"),
"python -m example.cli",
),
(str(pathlib.Path("example/__main__.py")), "example", "python -m example"),
(str(pathlib.Path("example/cli.py")), "example", "python -m example.cli"),
],
)
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):
@ -452,6 +460,8 @@ def test_expand_args(monkeypatch):
assert "setup.cfg" in click.utils._expand_args(["*.cfg"])
assert os.path.join("docs", "conf.py") in click.utils._expand_args(["**/conf.py"])
assert "*.not-found" in click.utils._expand_args(["*.not-found"])
# a bad glob pattern, such as a pytest identifier, should return itself
assert click.utils._expand_args(["test.py::test_bad"])[0] == "test.py::test_bad"
@pytest.mark.parametrize(

View file

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