From a6c7ee060b02eaa62fd15264a669220914cfad4c Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 19 Jan 2023 16:33:15 -0800 Subject: start version 8.1.4 --- src/click/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/click/__init__.py b/src/click/__init__.py index e3ef423..ea214e3 100644 --- a/src/click/__init__.py +++ b/src/click/__init__.py @@ -70,4 +70,4 @@ from .utils import get_binary_stream as get_binary_stream from .utils import get_text_stream as get_text_stream from .utils import open_file as open_file -__version__ = "8.1.3" +__version__ = "8.1.4.dev0" -- cgit v1.2.1 From a1093bbe0dae00eea8342247a0c2739b07a6acd8 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Tue, 8 Nov 2022 19:44:29 +0000 Subject: Types: don't leave generic types without a parameter Enable `disallow_any_generics` and provide type information for missing parameters for type hints. --- src/click/_compat.py | 36 ++++++++++++++++++------------------ src/click/_termui_impl.py | 2 +- src/click/core.py | 10 +++++----- src/click/decorators.py | 2 +- src/click/exceptions.py | 4 ++-- src/click/testing.py | 12 ++++++------ src/click/types.py | 10 +++++----- src/click/utils.py | 18 ++++++++++-------- 8 files changed, 48 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/click/_compat.py b/src/click/_compat.py index 766d286..57faa91 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 @@ -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..1caaad8 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -93,7 +93,7 @@ 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 diff --git a/src/click/core.py b/src/click/core.py index 5abfb0f..9aef380 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -1841,7 +1841,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 +1889,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 +2260,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 +2277,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: diff --git a/src/click/decorators.py b/src/click/decorators.py index 28618dc..4f7ecbb 100644 --- a/src/click/decorators.py +++ b/src/click/decorators.py @@ -41,7 +41,7 @@ def pass_obj(f: F) -> F: def make_pass_decorator( - object_type: t.Type, ensure: bool = False + object_type: t.Type[t.Any], ensure: bool = False ) -> "t.Callable[[F], F]": """Given an object type this creates a decorator that will work similar to :func:`pass_obj` but instead of passing the object of the 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..1b04e37 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,8 +702,8 @@ class File(ParamType): lazy = self.resolve_lazy_flag(value) if lazy: - f: t.IO = t.cast( - t.IO, + f: t.IO[t.Any] = t.cast( + t.IO[t.Any], LazyFile( value, self.mode, self.encoding, self.errors, atomic=self.atomic ), @@ -794,7 +794,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 +944,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..fca3eba 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -120,7 +120,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 +141,7 @@ class LazyFile: return repr(self._f) return f"" - 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. @@ -183,7 +183,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: @@ -340,7 +340,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 +370,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 +460,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: -- cgit v1.2.1 From 085f414a046bd5f15dfa08bedd0aa50f25410520 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Wed, 9 Nov 2022 17:03:49 +0000 Subject: Type hinting: Low-hanging fruit improvements Clean out a series of ignores, either by specifying types or by reworking code slightly the ignore is no longer needed. --- src/click/_compat.py | 2 +- src/click/_termui_impl.py | 2 +- src/click/core.py | 4 ++-- src/click/types.py | 11 ++++------- src/click/utils.py | 4 ++-- 5 files changed, 10 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/click/_compat.py b/src/click/_compat.py index 57faa91..e55a713 100644 --- a/src/click/_compat.py +++ b/src/click/_compat.py @@ -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: diff --git a/src/click/_termui_impl.py b/src/click/_termui_impl.py index 1caaad8..a050471 100644 --- a/src/click/_termui_impl.py +++ b/src/click/_termui_impl.py @@ -98,7 +98,7 @@ class ProgressBar(t.Generic[V]): 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 9aef380..1a85bab 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() @@ -2817,7 +2817,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/types.py b/src/click/types.py index 1b04e37..57866ec 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -702,17 +702,14 @@ class File(ParamType): lazy = self.resolve_lazy_flag(value) if lazy: - f: t.IO[t.Any] = t.cast( - t.IO[t.Any], - 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 diff --git a/src/click/utils.py b/src/click/utils.py index fca3eba..8f3fb57 100644 --- a/src/click/utils.py +++ b/src/click/utils.py @@ -174,7 +174,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]: @@ -192,7 +192,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: -- cgit v1.2.1 From a63679e77f9be2eb99e2f0884d617f9635a485e2 Mon Sep 17 00:00:00 2001 From: Martijn Pieters Date: Wed, 9 Nov 2022 17:09:59 +0000 Subject: Type hinting: improve decorator annotations A combination of overloads, TypeVar, ParamSpec and Concatenate make it possible to tell the type checker more about what kinds of callables are expected and what is being returned. --- src/click/core.py | 22 ++++++- src/click/decorators.py | 158 +++++++++++++++++++++++++++++++++--------------- src/click/utils.py | 11 ++-- 3 files changed, 135 insertions(+), 56 deletions(-) (limited to 'src') diff --git a/src/click/core.py b/src/click/core.py index 1a85bab..6164cf3 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -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) diff --git a/src/click/decorators.py b/src/click/decorators.py index 4f7ecbb..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[t.Any], 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/utils.py b/src/click/utils.py index 8f3fb57..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: -- cgit v1.2.1