summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Lord <davidism@gmail.com>2023-01-19 16:38:36 -0800
committerDavid Lord <davidism@gmail.com>2023-01-19 16:38:36 -0800
commitcba52fa76135af2edf46c154203b47106f898eb3 (patch)
tree24db6160c60d3b397989d42bf0dd2ad1c78786c1
parent725e3e4a8da6f45441d48ec9f6a526583b6e162c (diff)
parentc0092d2242479216ad633e12c87eabe33bacca2d (diff)
downloadclick-cba52fa76135af2edf46c154203b47106f898eb3.tar.gz
Merge branch '8.1.x'
-rw-r--r--CHANGES.rst9
-rw-r--r--setup.cfg4
-rw-r--r--src/click/_compat.py38
-rw-r--r--src/click/_termui_impl.py4
-rw-r--r--src/click/core.py36
-rw-r--r--src/click/decorators.py158
-rw-r--r--src/click/exceptions.py4
-rw-r--r--src/click/testing.py12
-rw-r--r--src/click/types.py17
-rw-r--r--src/click/utils.py33
10 files changed, 203 insertions, 112 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 3893438..fcc1ea1 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -6,6 +6,15 @@ Version 8.2.0
Unreleased
+Version 8.1.4
+-------------
+
+Unreleased
+
+- Improve type hinting for decorators and give all generic types parameters.
+ :issue:`2398`
+
+
Version 8.1.3
-------------
diff --git a/setup.cfg b/setup.cfg
index ea0a52c..0d9b7ac 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -71,6 +71,9 @@ ignore =
W503
# zip with strict=, requires python >= 3.10
B905
+ # string formatting opinion, B028 renamed to B907
+ B028
+ B907
# up to 88 allowed by bugbear B950
max-line-length = 80
per-file-ignores =
@@ -85,6 +88,7 @@ disallow_subclassing_any = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
+disallow_any_generics = True
check_untyped_defs = True
no_implicit_optional = True
local_partial_types = True
diff --git a/src/click/_compat.py b/src/click/_compat.py
index 766d286..e55a713 100644
--- a/src/click/_compat.py
+++ b/src/click/_compat.py
@@ -50,7 +50,7 @@ def is_ascii_encoding(encoding: str) -> bool:
return False
-def get_best_encoding(stream: t.IO) -> str:
+def get_best_encoding(stream: t.IO[t.Any]) -> str:
"""Returns the default stream encoding if not found."""
rv = getattr(stream, "encoding", None) or sys.getdefaultencoding()
if is_ascii_encoding(rv):
@@ -153,7 +153,7 @@ class _FixupStream:
return True
-def _is_binary_reader(stream: t.IO, default: bool = False) -> bool:
+def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool:
try:
return isinstance(stream.read(0), bytes)
except Exception:
@@ -162,7 +162,7 @@ def _is_binary_reader(stream: t.IO, default: bool = False) -> bool:
# closed. In this case, we assume the default.
-def _is_binary_writer(stream: t.IO, default: bool = False) -> bool:
+def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool:
try:
stream.write(b"")
except Exception:
@@ -175,7 +175,7 @@ def _is_binary_writer(stream: t.IO, default: bool = False) -> bool:
return True
-def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]:
+def _find_binary_reader(stream: t.IO[t.Any]) -> t.Optional[t.BinaryIO]:
# We need to figure out if the given stream is already binary.
# This can happen because the official docs recommend detaching
# the streams to get binary streams. Some code might do this, so
@@ -193,7 +193,7 @@ def _find_binary_reader(stream: t.IO) -> t.Optional[t.BinaryIO]:
return None
-def _find_binary_writer(stream: t.IO) -> t.Optional[t.BinaryIO]:
+def _find_binary_writer(stream: t.IO[t.Any]) -> t.Optional[t.BinaryIO]:
# We need to figure out if the given stream is already binary.
# This can happen because the official docs recommend detaching
# the streams to get binary streams. Some code might do this, so
@@ -241,11 +241,11 @@ def _is_compatible_text_stream(
def _force_correct_text_stream(
- text_stream: t.IO,
+ text_stream: t.IO[t.Any],
encoding: t.Optional[str],
errors: t.Optional[str],
- is_binary: t.Callable[[t.IO, bool], bool],
- find_binary: t.Callable[[t.IO], t.Optional[t.BinaryIO]],
+ is_binary: t.Callable[[t.IO[t.Any], bool], bool],
+ find_binary: t.Callable[[t.IO[t.Any]], t.Optional[t.BinaryIO]],
force_readable: bool = False,
force_writable: bool = False,
) -> t.TextIO:
@@ -287,7 +287,7 @@ def _force_correct_text_stream(
def _force_correct_text_reader(
- text_reader: t.IO,
+ text_reader: t.IO[t.Any],
encoding: t.Optional[str],
errors: t.Optional[str],
force_readable: bool = False,
@@ -303,7 +303,7 @@ def _force_correct_text_reader(
def _force_correct_text_writer(
- text_writer: t.IO,
+ text_writer: t.IO[t.Any],
encoding: t.Optional[str],
errors: t.Optional[str],
force_writable: bool = False,
@@ -367,11 +367,11 @@ def get_text_stderr(
def _wrap_io_open(
- file: t.Union[str, os.PathLike, int],
+ file: t.Union[str, "os.PathLike[t.AnyStr]", int],
mode: str,
encoding: t.Optional[str],
errors: t.Optional[str],
-) -> t.IO:
+) -> t.IO[t.Any]:
"""Handles not passing ``encoding`` and ``errors`` in binary mode."""
if "b" in mode:
return open(file, mode)
@@ -385,7 +385,7 @@ def open_stream(
encoding: t.Optional[str] = None,
errors: t.Optional[str] = "strict",
atomic: bool = False,
-) -> t.Tuple[t.IO, bool]:
+) -> t.Tuple[t.IO[t.Any], bool]:
binary = "b" in mode
# Standard streams first. These are simple because they ignore the
@@ -456,11 +456,11 @@ def open_stream(
f = _wrap_io_open(fd, mode, encoding, errors)
af = _AtomicFile(f, tmp_filename, os.path.realpath(filename))
- return t.cast(t.IO, af), True
+ return t.cast(t.IO[t.Any], af), True
class _AtomicFile:
- def __init__(self, f: t.IO, tmp_filename: str, real_filename: str) -> None:
+ def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None:
self._f = f
self._tmp_filename = tmp_filename
self._real_filename = real_filename
@@ -483,7 +483,7 @@ class _AtomicFile:
def __enter__(self) -> "_AtomicFile":
return self
- def __exit__(self, exc_type, exc_value, tb): # type: ignore
+ def __exit__(self, exc_type: t.Optional[t.Type[BaseException]], *_: t.Any) -> None:
self.close(delete=exc_type is not None)
def __repr__(self) -> str:
@@ -494,7 +494,7 @@ def strip_ansi(value: str) -> str:
return _ansi_re.sub("", value)
-def _is_jupyter_kernel_output(stream: t.IO) -> bool:
+def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool:
while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)):
stream = stream._stream
@@ -502,7 +502,7 @@ def _is_jupyter_kernel_output(stream: t.IO) -> bool:
def should_strip_ansi(
- stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
+ stream: t.Optional[t.IO[t.Any]] = None, color: t.Optional[bool] = None
) -> bool:
if color is None:
if stream is None:
@@ -576,7 +576,7 @@ def term_len(x: str) -> int:
return len(strip_ansi(x))
-def isatty(stream: t.IO) -> bool:
+def isatty(stream: t.IO[t.Any]) -> bool:
try:
return stream.isatty()
except Exception:
diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py
index 4b979bc..a050471 100644
--- a/src/click/_termui_impl.py
+++ b/src/click/_termui_impl.py
@@ -93,12 +93,12 @@ class ProgressBar(t.Generic[V]):
self.is_hidden = not isatty(self.file)
self._last_line: t.Optional[str] = None
- def __enter__(self) -> "ProgressBar":
+ def __enter__(self) -> "ProgressBar[V]":
self.entered = True
self.render_progress()
return self
- def __exit__(self, exc_type, exc_value, tb): # type: ignore
+ def __exit__(self, *_: t.Any) -> None:
self.render_finish()
def __iter__(self) -> t.Iterator[V]:
diff --git a/src/click/core.py b/src/click/core.py
index 5abfb0f..6164cf3 100644
--- a/src/click/core.py
+++ b/src/click/core.py
@@ -455,7 +455,7 @@ class Context:
push_context(self)
return self
- def __exit__(self, exc_type, exc_value, tb): # type: ignore
+ def __exit__(self, *_: t.Any) -> None:
self._depth -= 1
if self._depth == 0:
self.close()
@@ -706,12 +706,30 @@ class Context:
"""
return type(self)(command, info_name=command.name, parent=self)
+ @t.overload
+ def invoke(
+ __self, # noqa: B902
+ __callback: "t.Callable[..., V]",
+ *args: t.Any,
+ **kwargs: t.Any,
+ ) -> V:
+ ...
+
+ @t.overload
def invoke(
__self, # noqa: B902
- __callback: t.Union["Command", t.Callable[..., t.Any]],
+ __callback: "Command",
*args: t.Any,
**kwargs: t.Any,
) -> t.Any:
+ ...
+
+ def invoke(
+ __self, # noqa: B902
+ __callback: t.Union["Command", "t.Callable[..., V]"],
+ *args: t.Any,
+ **kwargs: t.Any,
+ ) -> t.Union[t.Any, V]:
"""Invokes a command callback in exactly the way it expects. There
are two ways to invoke this method:
@@ -739,7 +757,7 @@ class Context:
"The given command does not have a callback that can be invoked."
)
else:
- __callback = other_cmd.callback
+ __callback = t.cast("t.Callable[..., V]", other_cmd.callback)
ctx = __self._make_sub_context(other_cmd)
@@ -1841,7 +1859,7 @@ class Group(MultiCommand):
if self.command_class and kwargs.get("cls") is None:
kwargs["cls"] = self.command_class
- func: t.Optional[t.Callable] = None
+ func: t.Optional[t.Callable[..., t.Any]] = None
if args and callable(args[0]):
assert (
@@ -1889,7 +1907,7 @@ class Group(MultiCommand):
"""
from .decorators import group
- func: t.Optional[t.Callable] = None
+ func: t.Optional[t.Callable[..., t.Any]] = None
if args and callable(args[0]):
assert (
@@ -2260,7 +2278,7 @@ class Parameter:
if value is None:
return () if self.multiple or self.nargs == -1 else None
- def check_iter(value: t.Any) -> t.Iterator:
+ def check_iter(value: t.Any) -> t.Iterator[t.Any]:
try:
return _check_iter(value)
except TypeError:
@@ -2277,12 +2295,12 @@ class Parameter:
)
elif self.nargs == -1:
- def convert(value: t.Any) -> t.Tuple:
+ def convert(value: t.Any) -> t.Tuple[t.Any, ...]:
return tuple(self.type(x, self, ctx) for x in check_iter(value))
else: # nargs > 1
- def convert(value: t.Any) -> t.Tuple:
+ def convert(value: t.Any) -> t.Tuple[t.Any, ...]:
value = tuple(check_iter(value))
if len(value) != self.nargs:
@@ -2817,7 +2835,7 @@ class Option(Parameter):
if self.is_flag and not self.is_bool_flag:
for param in ctx.command.params:
if param.name == self.name and param.default:
- return param.flag_value # type: ignore
+ return t.cast(Option, param).flag_value
return None
diff --git a/src/click/decorators.py b/src/click/decorators.py
index 28618dc..b8b2731 100644
--- a/src/click/decorators.py
+++ b/src/click/decorators.py
@@ -13,36 +13,44 @@ from .core import Parameter
from .globals import get_current_context
from .utils import echo
-F = t.TypeVar("F", bound=t.Callable[..., t.Any])
-FC = t.TypeVar("FC", bound=t.Union[t.Callable[..., t.Any], Command])
+if t.TYPE_CHECKING:
+ import typing_extensions as te
+ P = te.ParamSpec("P")
-def pass_context(f: F) -> F:
+R = t.TypeVar("R")
+T = t.TypeVar("T")
+_AnyCallable = t.Callable[..., t.Any]
+_Decorator: "te.TypeAlias" = t.Callable[[T], T]
+FC = t.TypeVar("FC", bound=t.Union[_AnyCallable, Command])
+
+
+def pass_context(f: "t.Callable[te.Concatenate[Context, P], R]") -> "t.Callable[P, R]":
"""Marks a callback as wanting to receive the current context
object as first argument.
"""
- def new_func(*args, **kwargs): # type: ignore
+ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R":
return f(get_current_context(), *args, **kwargs)
- return update_wrapper(t.cast(F, new_func), f)
+ return update_wrapper(new_func, f)
-def pass_obj(f: F) -> F:
+def pass_obj(f: "t.Callable[te.Concatenate[t.Any, P], R]") -> "t.Callable[P, R]":
"""Similar to :func:`pass_context`, but only pass the object on the
context onwards (:attr:`Context.obj`). This is useful if that object
represents the state of a nested system.
"""
- def new_func(*args, **kwargs): # type: ignore
+ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R":
return f(get_current_context().obj, *args, **kwargs)
- return update_wrapper(t.cast(F, new_func), f)
+ return update_wrapper(new_func, f)
def make_pass_decorator(
- object_type: t.Type, ensure: bool = False
-) -> "t.Callable[[F], F]":
+ object_type: t.Type[T], ensure: bool = False
+) -> t.Callable[["t.Callable[te.Concatenate[T, P], R]"], "t.Callable[P, R]"]:
"""Given an object type this creates a decorator that will work
similar to :func:`pass_obj` but instead of passing the object of the
current context, it will find the innermost context of type
@@ -65,10 +73,11 @@ def make_pass_decorator(
remembered on the context if it's not there yet.
"""
- def decorator(f: F) -> F:
- def new_func(*args, **kwargs): # type: ignore
+ def decorator(f: "t.Callable[te.Concatenate[T, P], R]") -> "t.Callable[P, R]":
+ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> "R":
ctx = get_current_context()
+ obj: t.Optional[T]
if ensure:
obj = ctx.ensure_object(object_type)
else:
@@ -83,14 +92,14 @@ def make_pass_decorator(
return ctx.invoke(f, obj, *args, **kwargs)
- return update_wrapper(t.cast(F, new_func), f)
+ return update_wrapper(new_func, f)
return decorator
def pass_meta_key(
key: str, *, doc_description: t.Optional[str] = None
-) -> "t.Callable[[F], F]":
+) -> "t.Callable[[t.Callable[te.Concatenate[t.Any, P], R]], t.Callable[P, R]]":
"""Create a decorator that passes a key from
:attr:`click.Context.meta` as the first argument to the decorated
function.
@@ -103,13 +112,13 @@ def pass_meta_key(
.. versionadded:: 8.0
"""
- def decorator(f: F) -> F:
- def new_func(*args, **kwargs): # type: ignore
+ def decorator(f: "t.Callable[te.Concatenate[t.Any, P], R]") -> "t.Callable[P, R]":
+ def new_func(*args: "P.args", **kwargs: "P.kwargs") -> R:
ctx = get_current_context()
obj = ctx.meta[key]
return ctx.invoke(f, obj, *args, **kwargs)
- return update_wrapper(t.cast(F, new_func), f)
+ return update_wrapper(new_func, f)
if doc_description is None:
doc_description = f"the {key!r} key from :attr:`click.Context.meta`"
@@ -124,35 +133,51 @@ def pass_meta_key(
CmdType = t.TypeVar("CmdType", bound=Command)
+# variant: no call, directly as decorator for a function.
@t.overload
-def command(
- __func: t.Callable[..., t.Any],
-) -> Command:
+def command(name: _AnyCallable) -> Command:
...
+# variant: with positional name and with positional or keyword cls argument:
+# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...)
@t.overload
def command(
- name: t.Optional[str] = None,
+ name: t.Optional[str],
+ cls: t.Type[CmdType],
**attrs: t.Any,
-) -> t.Callable[..., Command]:
+) -> t.Callable[[_AnyCallable], CmdType]:
...
+# variant: name omitted, cls _must_ be a keyword argument, @command(cmd=CommandCls, ...)
+# The correct way to spell this overload is to use keyword-only argument syntax:
+# def command(*, cls: t.Type[CmdType], **attrs: t.Any) -> ...
+# However, mypy thinks this doesn't fit the overloaded function. Pyright does
+# accept that spelling, and the following work-around makes pyright issue a
+# warning that CmdType could be left unsolved, but mypy sees it as fine. *shrug*
@t.overload
def command(
- name: t.Optional[str] = None,
+ name: None = None,
cls: t.Type[CmdType] = ...,
**attrs: t.Any,
-) -> t.Callable[..., CmdType]:
+) -> t.Callable[[_AnyCallable], CmdType]:
+ ...
+
+
+# variant: with optional string name, no cls argument provided.
+@t.overload
+def command(
+ name: t.Optional[str] = ..., cls: None = None, **attrs: t.Any
+) -> t.Callable[[_AnyCallable], Command]:
...
def command(
- name: t.Union[str, t.Callable[..., t.Any], None] = None,
- cls: t.Optional[t.Type[Command]] = None,
+ name: t.Union[t.Optional[str], _AnyCallable] = None,
+ cls: t.Optional[t.Type[CmdType]] = None,
**attrs: t.Any,
-) -> t.Union[Command, t.Callable[..., Command]]:
+) -> t.Union[Command, t.Callable[[_AnyCallable], t.Union[Command, CmdType]]]:
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.
@@ -182,7 +207,7 @@ def command(
appended to the end of the list.
"""
- func: t.Optional[t.Callable[..., t.Any]] = None
+ func: t.Optional[t.Callable[[_AnyCallable], t.Any]] = None
if callable(name):
func = name
@@ -191,9 +216,9 @@ def command(
assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments."
if cls is None:
- cls = Command
+ cls = t.cast(t.Type[CmdType], Command)
- def decorator(f: t.Callable[..., t.Any]) -> Command:
+ def decorator(f: _AnyCallable) -> CmdType:
if isinstance(f, Command):
raise TypeError("Attempted to convert a callback into a command twice.")
@@ -211,8 +236,12 @@ def command(
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]
+ if t.TYPE_CHECKING:
+ assert cls is not None
+ assert not callable(name)
+
+ cmd = cls(
+ name=name or f.__name__.lower().replace("_", "-"),
callback=f,
params=params,
**attrs,
@@ -226,24 +255,54 @@ def command(
return decorator
+GrpType = t.TypeVar("GrpType", bound=Group)
+
+
+# variant: no call, directly as decorator for a function.
+@t.overload
+def group(name: _AnyCallable) -> Group:
+ ...
+
+
+# variant: with positional name and with positional or keyword cls argument:
+# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...)
@t.overload
def group(
- __func: t.Callable[..., t.Any],
-) -> Group:
+ name: t.Optional[str],
+ cls: t.Type[GrpType],
+ **attrs: t.Any,
+) -> t.Callable[[_AnyCallable], GrpType]:
...
+# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...)
+# The _correct_ way to spell this overload is to use keyword-only argument syntax:
+# def group(*, cls: t.Type[GrpType], **attrs: t.Any) -> ...
+# However, mypy thinks this doesn't fit the overloaded function. Pyright does
+# accept that spelling, and the following work-around makes pyright issue a
+# warning that GrpType could be left unsolved, but mypy sees it as fine. *shrug*
@t.overload
def group(
- name: t.Optional[str] = None,
+ name: None = None,
+ cls: t.Type[GrpType] = ...,
**attrs: t.Any,
-) -> t.Callable[[F], Group]:
+) -> t.Callable[[_AnyCallable], GrpType]:
...
+# variant: with optional string name, no cls argument provided.
+@t.overload
def group(
- name: t.Union[str, t.Callable[..., t.Any], None] = None, **attrs: t.Any
-) -> t.Union[Group, t.Callable[[F], Group]]:
+ name: t.Optional[str] = ..., cls: None = None, **attrs: t.Any
+) -> t.Callable[[_AnyCallable], Group]:
+ ...
+
+
+def group(
+ name: t.Union[str, _AnyCallable, None] = None,
+ cls: t.Optional[t.Type[GrpType]] = None,
+ **attrs: t.Any,
+) -> t.Union[Group, t.Callable[[_AnyCallable], t.Union[Group, GrpType]]]:
"""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`.
@@ -251,17 +310,16 @@ def group(
.. versionchanged:: 8.1
This decorator can be applied without parentheses.
"""
- if attrs.get("cls") is None:
- attrs["cls"] = Group
+ if cls is None:
+ cls = t.cast(t.Type[GrpType], Group)
if callable(name):
- grp: t.Callable[[F], Group] = t.cast(Group, command(**attrs))
- return grp(name)
+ return command(cls=cls, **attrs)(name)
- return t.cast(Group, command(name, **attrs))
+ return command(name, cls, **attrs)
-def _param_memo(f: FC, param: Parameter) -> None:
+def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None:
if isinstance(f, Command):
f.params.append(param)
else:
@@ -271,7 +329,7 @@ def _param_memo(f: FC, param: Parameter) -> None:
f.__click_params__.append(param) # type: ignore
-def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
+def argument(*param_decls: str, **attrs: t.Any) -> _Decorator[FC]:
"""Attaches an argument to the command. All positional arguments are
passed as parameter declarations to :class:`Argument`; all keyword
arguments are forwarded unchanged (except ``cls``).
@@ -290,7 +348,7 @@ def argument(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
return decorator
-def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
+def option(*param_decls: str, **attrs: t.Any) -> _Decorator[FC]:
"""Attaches an option to the command. All positional arguments are
passed as parameter declarations to :class:`Option`; all keyword
arguments are forwarded unchanged (except ``cls``).
@@ -311,7 +369,7 @@ def option(*param_decls: str, **attrs: t.Any) -> t.Callable[[FC], FC]:
return decorator
-def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
+def confirmation_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
"""Add a ``--yes`` option which shows a prompt before continuing if
not passed. If the prompt is declined, the program will exit.
@@ -335,7 +393,7 @@ def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC],
return option(*param_decls, **kwargs)
-def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
+def password_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
"""Add a ``--password`` option which prompts for a password, hiding
input and asking to enter the value again for confirmation.
@@ -359,7 +417,7 @@ def version_option(
prog_name: t.Optional[str] = None,
message: t.Optional[str] = None,
**kwargs: t.Any,
-) -> t.Callable[[FC], FC]:
+) -> _Decorator[FC]:
"""Add a ``--version`` option which immediately prints the version
number and exits the program.
@@ -466,7 +524,7 @@ def version_option(
return option(*param_decls, **kwargs)
-def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]:
+def help_option(*param_decls: str, **kwargs: t.Any) -> _Decorator[FC]:
"""Add a ``--help`` option which immediately prints the help page
and exits the program.
diff --git a/src/click/exceptions.py b/src/click/exceptions.py
index 9e20b3e..59b18c6 100644
--- a/src/click/exceptions.py
+++ b/src/click/exceptions.py
@@ -36,7 +36,7 @@ class ClickException(Exception):
def __str__(self) -> str:
return self.message
- def show(self, file: t.Optional[t.IO] = None) -> None:
+ def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None:
if file is None:
file = get_text_stderr()
@@ -59,7 +59,7 @@ class UsageError(ClickException):
self.ctx = ctx
self.cmd = self.ctx.command if self.ctx else None
- def show(self, file: t.Optional[t.IO] = None) -> None:
+ def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None:
if file is None:
file = get_text_stderr()
color = None
diff --git a/src/click/testing.py b/src/click/testing.py
index 244d326..7b6dd7f 100644
--- a/src/click/testing.py
+++ b/src/click/testing.py
@@ -79,11 +79,11 @@ class _NamedTextIOWrapper(io.TextIOWrapper):
def make_input_stream(
- input: t.Optional[t.Union[str, bytes, t.IO]], charset: str
+ input: t.Optional[t.Union[str, bytes, t.IO[t.Any]]], charset: str
) -> t.BinaryIO:
# Is already an input stream.
if hasattr(input, "read"):
- rv = _find_binary_reader(t.cast(t.IO, input))
+ rv = _find_binary_reader(t.cast(t.IO[t.Any], input))
if rv is not None:
return rv
@@ -206,7 +206,7 @@ class CliRunner:
@contextlib.contextmanager
def isolation(
self,
- input: t.Optional[t.Union[str, bytes, t.IO]] = None,
+ input: t.Optional[t.Union[str, bytes, t.IO[t.Any]]] = None,
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
color: bool = False,
) -> t.Iterator[t.Tuple[io.BytesIO, t.Optional[io.BytesIO]]]:
@@ -301,7 +301,7 @@ class CliRunner:
default_color = color
def should_strip_ansi(
- stream: t.Optional[t.IO] = None, color: t.Optional[bool] = None
+ stream: t.Optional[t.IO[t.Any]] = None, color: t.Optional[bool] = None
) -> bool:
if color is None:
return not default_color
@@ -350,7 +350,7 @@ class CliRunner:
self,
cli: "BaseCommand",
args: t.Optional[t.Union[str, t.Sequence[str]]] = None,
- input: t.Optional[t.Union[str, bytes, t.IO]] = None,
+ input: t.Optional[t.Union[str, bytes, t.IO[t.Any]]] = None,
env: t.Optional[t.Mapping[str, t.Optional[str]]] = None,
catch_exceptions: bool = True,
color: bool = False,
@@ -449,7 +449,7 @@ class CliRunner:
@contextlib.contextmanager
def isolated_filesystem(
- self, temp_dir: t.Optional[t.Union[str, os.PathLike]] = None
+ self, temp_dir: t.Optional[t.Union[str, "os.PathLike[str]"]] = None
) -> t.Iterator[str]:
"""A context manager that creates a temporary directory and
changes the current working directory to it. This isolates tests
diff --git a/src/click/types.py b/src/click/types.py
index d948c70..57866ec 100644
--- a/src/click/types.py
+++ b/src/click/types.py
@@ -397,7 +397,7 @@ class DateTime(ParamType):
class _NumberParamTypeBase(ParamType):
- _number_class: t.ClassVar[t.Type]
+ _number_class: t.ClassVar[t.Type[t.Any]]
def convert(
self, value: t.Any, param: t.Optional["Parameter"], ctx: t.Optional["Context"]
@@ -702,17 +702,14 @@ class File(ParamType):
lazy = self.resolve_lazy_flag(value)
if lazy:
- f: t.IO = t.cast(
- t.IO,
- LazyFile(
- value, self.mode, self.encoding, self.errors, atomic=self.atomic
- ),
+ lf = LazyFile(
+ value, self.mode, self.encoding, self.errors, atomic=self.atomic
)
if ctx is not None:
- ctx.call_on_close(f.close_intelligently) # type: ignore
+ ctx.call_on_close(lf.close_intelligently)
- return f
+ return t.cast(t.IO[t.Any], lf)
f, should_close = open_stream(
value, self.mode, self.encoding, self.errors, atomic=self.atomic
@@ -794,7 +791,7 @@ class Path(ParamType):
readable: bool = True,
resolve_path: bool = False,
allow_dash: bool = False,
- path_type: t.Optional[t.Type] = None,
+ path_type: t.Optional[t.Type[t.Any]] = None,
executable: bool = False,
):
self.exists = exists
@@ -944,7 +941,7 @@ class Tuple(CompositeParamType):
:param types: a list of types that should be used for the tuple items.
"""
- def __init__(self, types: t.Sequence[t.Union[t.Type, ParamType]]) -> None:
+ def __init__(self, types: t.Sequence[t.Union[t.Type[t.Any], ParamType]]) -> None:
self.types = [convert_type(ty) for ty in types]
def to_info_dict(self) -> t.Dict[str, t.Any]:
diff --git a/src/click/utils.py b/src/click/utils.py
index 8283788..e9310e5 100644
--- a/src/click/utils.py
+++ b/src/click/utils.py
@@ -21,23 +21,26 @@ from .globals import resolve_color_default
if t.TYPE_CHECKING:
import typing_extensions as te
-F = t.TypeVar("F", bound=t.Callable[..., t.Any])
+ P = te.ParamSpec("P")
+
+R = t.TypeVar("R")
def _posixify(name: str) -> str:
return "-".join(name.split()).lower()
-def safecall(func: F) -> F:
+def safecall(func: "t.Callable[P, R]") -> "t.Callable[P, t.Optional[R]]":
"""Wraps a function so that it swallows exceptions."""
- def wrapper(*args, **kwargs): # type: ignore
+ def wrapper(*args: "P.args", **kwargs: "P.kwargs") -> t.Optional[R]:
try:
return func(*args, **kwargs)
except Exception:
pass
+ return None
- return update_wrapper(t.cast(F, wrapper), func)
+ return update_wrapper(wrapper, func)
def make_str(value: t.Any) -> str:
@@ -120,7 +123,7 @@ class LazyFile:
self.encoding = encoding
self.errors = errors
self.atomic = atomic
- self._f: t.Optional[t.IO]
+ self._f: t.Optional[t.IO[t.Any]]
if filename == "-":
self._f, self.should_close = open_stream(filename, mode, encoding, errors)
@@ -141,7 +144,7 @@ class LazyFile:
return repr(self._f)
return f"<unopened file '{self.name}' {self.mode}>"
- def open(self) -> t.IO:
+ def open(self) -> t.IO[t.Any]:
"""Opens the file if it's not yet open. This call might fail with
a :exc:`FileError`. Not handling this error will produce an error
that Click shows.
@@ -174,7 +177,7 @@ class LazyFile:
def __enter__(self) -> "LazyFile":
return self
- def __exit__(self, exc_type, exc_value, tb): # type: ignore
+ def __exit__(self, *_: t.Any) -> None:
self.close_intelligently()
def __iter__(self) -> t.Iterator[t.AnyStr]:
@@ -183,7 +186,7 @@ class LazyFile:
class KeepOpenFile:
- def __init__(self, file: t.IO) -> None:
+ def __init__(self, file: t.IO[t.Any]) -> None:
self._file = file
def __getattr__(self, name: str) -> t.Any:
@@ -192,7 +195,7 @@ class KeepOpenFile:
def __enter__(self) -> "KeepOpenFile":
return self
- def __exit__(self, exc_type, exc_value, tb): # type: ignore
+ def __exit__(self, *_: t.Any) -> None:
pass
def __repr__(self) -> str:
@@ -340,7 +343,7 @@ def open_file(
errors: t.Optional[str] = "strict",
lazy: bool = False,
atomic: bool = False,
-) -> t.IO:
+) -> t.IO[t.Any]:
"""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.
@@ -370,18 +373,20 @@ def open_file(
.. versionadded:: 3.0
"""
if lazy:
- return t.cast(t.IO, LazyFile(filename, mode, encoding, errors, atomic=atomic))
+ return t.cast(
+ t.IO[t.Any], 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))
+ f = t.cast(t.IO[t.Any], KeepOpenFile(f))
return f
def format_filename(
- filename: t.Union[str, bytes, os.PathLike], shorten: bool = False
+ filename: t.Union[str, bytes, "os.PathLike[t.AnyStr]"], shorten: bool = False
) -> str:
"""Formats a filename for user display. The main purpose of this
function is to ensure that the filename can be displayed at all. This
@@ -458,7 +463,7 @@ class PacifyFlushWrapper:
pipe, all calls and attributes are proxied.
"""
- def __init__(self, wrapped: t.IO) -> None:
+ def __init__(self, wrapped: t.IO[t.Any]) -> None:
self.wrapped = wrapped
def flush(self) -> None: