diff options
-rw-r--r-- | .github/workflows/tests.yaml | 1 | ||||
-rw-r--r-- | requirements/dev.in | 1 | ||||
-rw-r--r-- | requirements/dev.txt | 8 | ||||
-rw-r--r-- | requirements/typing.in | 1 | ||||
-rw-r--r-- | requirements/typing.txt | 14 | ||||
-rw-r--r-- | setup.cfg | 20 | ||||
-rw-r--r-- | src/click/_compat.py | 15 | ||||
-rw-r--r-- | src/click/_winconsole.py | 69 | ||||
-rw-r--r-- | src/click/core.py | 6 | ||||
-rw-r--r-- | src/click/formatting.py | 3 | ||||
-rw-r--r-- | src/click/shell_completion.py | 11 | ||||
-rw-r--r-- | src/click/termui.py | 3 | ||||
-rw-r--r-- | src/click/types.py | 7 | ||||
-rw-r--r-- | tests/test_imports.py | 2 | ||||
-rw-r--r-- | tox.ini | 5 |
15 files changed, 111 insertions, 55 deletions
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2291f44..f7535c7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -23,6 +23,7 @@ jobs: - {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: Docs, python: '3.9', os: ubuntu-latest, tox: docs} steps: - uses: actions/checkout@v2 diff --git a/requirements/dev.in b/requirements/dev.in index c854000..2588467 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -1,5 +1,6 @@ -r docs.in -r tests.in +-r typing.in pip-tools pre-commit tox diff --git a/requirements/dev.txt b/requirements/dev.txt index cc6d59c..2f25e5c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -44,6 +44,10 @@ jinja2==2.11.3 # via sphinx markupsafe==1.1.1 # via jinja2 +mypy-extensions==0.4.3 + # via mypy +mypy==0.800 + # via -r requirements/typing.in nodeenv==1.5.0 # via pre-commit packaging==20.9 @@ -118,6 +122,10 @@ toml==0.10.2 # tox tox==3.21.4 # via -r requirements/dev.in +typed-ast==1.4.2 + # via mypy +typing-extensions==3.7.4.3 + # via mypy urllib3==1.26.3 # via requests virtualenv==20.4.2 diff --git a/requirements/typing.in b/requirements/typing.in new file mode 100644 index 0000000..f0aa93a --- /dev/null +++ b/requirements/typing.in @@ -0,0 +1 @@ +mypy diff --git a/requirements/typing.txt b/requirements/typing.txt new file mode 100644 index 0000000..2530301 --- /dev/null +++ b/requirements/typing.txt @@ -0,0 +1,14 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile requirements/typing.in +# +mypy-extensions==0.4.3 + # via mypy +mypy==0.800 + # via -r requirements/typing.in +typed-ast==1.4.2 + # via mypy +typing-extensions==3.7.4.3 + # via mypy @@ -70,3 +70,23 @@ max-line-length = 80 per-file-ignores = # __init__ module exports names src/click/__init__.py: F401 + +[mypy] +files = src/click +python_version = 3.6 +disallow_subclassing_any = True +# disallow_untyped_calls = True +# disallow_untyped_defs = True +disallow_incomplete_defs = True +no_implicit_optional = True +local_partial_types = True +# no_implicit_reexport = True +strict_equality = True +warn_redundant_casts = True +warn_unused_configs = True +warn_unused_ignores = True +warn_return_any = True +warn_unreachable = True + +[mypy-importlib_metadata.*] +ignore_missing_imports = True diff --git a/src/click/_compat.py b/src/click/_compat.py index 3cf5432..e0acca3 100644 --- a/src/click/_compat.py +++ b/src/click/_compat.py @@ -3,6 +3,7 @@ import io import os import re import sys +import typing as t from weakref import WeakKeyDictionary CYGWIN = sys.platform.startswith("cygwin") @@ -12,8 +13,7 @@ APP_ENGINE = "APPENGINE_RUNTIME" in os.environ and "Development/" in os.environ. "SERVER_SOFTWARE", "" ) WIN = sys.platform.startswith("win") and not APP_ENGINE and not MSYS2 -auto_wrap_for_ansi = None -colorama = None +auto_wrap_for_ansi: t.Optional[t.Callable[[t.TextIO], t.TextIO]] = None _ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") @@ -493,7 +493,8 @@ def should_strip_ansi(stream=None, color=None): # If we're on Windows, we provide transparent integration through # colorama. This will make ANSI colors through the echo function # work automatically. -if WIN: +# NOTE: double check is needed so mypy does not analyze this on Linux +if sys.platform.startswith("win") and WIN: from ._winconsole import _get_windows_console_stream def _get_argv_encoding(): @@ -506,9 +507,13 @@ if WIN: except ImportError: pass else: - _ansi_stream_wrappers = WeakKeyDictionary() + _ansi_stream_wrappers: t.MutableMapping[ + t.TextIO, t.TextIO + ] = WeakKeyDictionary() - def auto_wrap_for_ansi(stream, color=None): + def auto_wrap_for_ansi( + stream: t.TextIO, color: t.Optional[bool] = None + ) -> t.TextIO: """This function wraps a stream so that calls through colorama are issued to the win32 console API to recolor on demand. It also ensures to reset the colors if a write call is interrupted diff --git a/src/click/_winconsole.py b/src/click/_winconsole.py index 074dff7..cd5d012 100644 --- a/src/click/_winconsole.py +++ b/src/click/_winconsole.py @@ -6,8 +6,8 @@ # compared to the original patches as we do not need to patch # the entire interpreter but just work in our little world of # echo and prompt. -import ctypes import io +import sys import time from ctypes import byref from ctypes import c_char @@ -18,25 +18,18 @@ from ctypes import c_ulong from ctypes import c_void_p from ctypes import POINTER from ctypes import py_object -from ctypes import windll -from ctypes import WINFUNCTYPE +from ctypes import Structure from ctypes.wintypes import DWORD from ctypes.wintypes import HANDLE from ctypes.wintypes import LPCWSTR from ctypes.wintypes import LPWSTR -import msvcrt - from ._compat import _NonClosingTextIOWrapper -try: - from ctypes import pythonapi -except ImportError: - pythonapi = None -else: - PyObject_GetBuffer = pythonapi.PyObject_GetBuffer - PyBuffer_Release = pythonapi.PyBuffer_Release - +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 c_ssize_p = POINTER(c_ssize_t) @@ -50,16 +43,12 @@ GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( ("CommandLineToArgvW", windll.shell32) ) -LocalFree = WINFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p)( - ("LocalFree", windll.kernel32) -) - +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) STDIN_HANDLE = GetStdHandle(-10) STDOUT_HANDLE = GetStdHandle(-11) STDERR_HANDLE = GetStdHandle(-12) - PyBUF_SIMPLE = 0 PyBUF_WRITABLE = 1 @@ -74,33 +63,37 @@ STDERR_FILENO = 2 EOF = b"\x1a" MAX_BYTES_WRITTEN = 32767 - -class Py_buffer(ctypes.Structure): - _fields_ = [ - ("buf", c_void_p), - ("obj", py_object), - ("len", c_ssize_t), - ("itemsize", c_ssize_t), - ("readonly", c_int), - ("ndim", c_int), - ("format", c_char_p), - ("shape", c_ssize_p), - ("strides", c_ssize_p), - ("suboffsets", c_ssize_p), - ("internal", c_void_p), - ] - - -# On PyPy we cannot get buffers so our ability to operate here is -# severely limited. -if pythonapi is None: +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. get_buffer = None else: + class Py_buffer(Structure): + _fields_ = [ + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + def get_buffer(obj, writable=False): buf = Py_buffer() flags = PyBUF_WRITABLE if writable else PyBUF_SIMPLE PyObject_GetBuffer(py_object(obj), byref(buf), flags) + try: buffer_type = c_char * buf.len return buffer_type.from_address(buf.buf) diff --git a/src/click/core.py b/src/click/core.py index d04becb..7e78b57 100644 --- a/src/click/core.py +++ b/src/click/core.py @@ -2,6 +2,7 @@ import enum import errno import os import sys +import typing as t from contextlib import contextmanager from contextlib import ExitStack from functools import update_wrapper @@ -1625,7 +1626,7 @@ class Group(MultiCommand): #: subcommands use a custom command class. #: #: .. versionadded:: 8.0 - command_class = None + command_class: t.Optional[t.Type[BaseCommand]] = None #: If set, this is used by the group's :meth:`group` decorator #: as the default :class:`Group` class. This is useful to make all @@ -1637,7 +1638,8 @@ class Group(MultiCommand): #: custom groups. #: #: .. versionadded:: 8.0 - group_class = None + group_class: t.Optional[t.Union[t.Type["Group"], t.Type[type]]] = None + # Literal[type] isn't valid, so use Type[type] def __init__(self, name=None, commands=None, **attrs): super().__init__(name, **attrs) diff --git a/src/click/formatting.py b/src/click/formatting.py index 3edb09f..0d3c65e 100644 --- a/src/click/formatting.py +++ b/src/click/formatting.py @@ -1,10 +1,11 @@ +import typing as t from contextlib import contextmanager from ._compat import term_len from .parser import split_opt # Can force a width. This is used by the test system -FORCED_WIDTH = None +FORCED_WIDTH: t.Optional[int] = None def measure_table(rows): diff --git a/src/click/shell_completion.py b/src/click/shell_completion.py index 9b10e25..d87e0c8 100644 --- a/src/click/shell_completion.py +++ b/src/click/shell_completion.py @@ -1,5 +1,6 @@ import os import re +import typing as t from .core import Argument from .core import MultiCommand @@ -183,12 +184,12 @@ class ShellComplete: .. versionadded:: 8.0 """ - name = None + name: t.ClassVar[t.Optional[str]] = None """Name to register the shell as with :func:`add_completion_class`. This is used in completion instructions (``{name}_source`` and ``{name}_complete``). """ - source_template = None + source_template: t.ClassVar[t.Optional[str]] = None """Completion script template formatted by :meth:`source`. This must be provided by subclasses. """ @@ -312,7 +313,7 @@ class BashComplete(ShellComplete): return args, incomplete - def format_completion(self, item: CompletionItem): + def format_completion(self, item: CompletionItem) -> str: return f"{item.type},{item.value}" @@ -334,7 +335,7 @@ class ZshComplete(ShellComplete): return args, incomplete - def format_completion(self, item: CompletionItem): + def format_completion(self, item: CompletionItem) -> str: return f"{item.type}\n{item.value}\n{item.help if item.help else '_'}" @@ -356,7 +357,7 @@ class FishComplete(ShellComplete): return args, incomplete - def format_completion(self, item: CompletionItem): + def format_completion(self, item: CompletionItem) -> str: if item.help: return f"{item.type},{item.value}\t{item.help}" diff --git a/src/click/termui.py b/src/click/termui.py index c71711e..d3c61c0 100644 --- a/src/click/termui.py +++ b/src/click/termui.py @@ -3,6 +3,7 @@ import io import itertools import os import sys +import typing as t from ._compat import is_bytes from ._compat import isatty @@ -651,7 +652,7 @@ def launch(url, wait=False, locate=False): # If this is provided, getchar() calls into this instead. This is used # for unittesting purposes. -_getchar = None +_getchar: t.Optional[t.Callable[[bool], str]] = None def getchar(echo=False): diff --git a/src/click/types.py b/src/click/types.py index 626287d..6ca72c9 100644 --- a/src/click/types.py +++ b/src/click/types.py @@ -1,5 +1,6 @@ import os import stat +import typing as t from datetime import datetime from ._compat import _get_argv_encoding @@ -33,7 +34,7 @@ class ParamType: is_composite = False #: the descriptive name of this type - name = None + name: t.Optional[str] = None #: if a list of this type is expected and the value is pulled from a #: string environment variable, this is what splits it up. `None` @@ -41,7 +42,7 @@ class ParamType: #: whitespace splits them up. The exception are paths and files which #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on #: Windows). - envvar_list_splitter = None + envvar_list_splitter: t.ClassVar[t.Optional[str]] = None def to_info_dict(self): """Gather information that could be useful for a tool generating @@ -345,7 +346,7 @@ class DateTime(ParamType): class _NumberParamTypeBase(ParamType): - _number_class = None + _number_class: t.ClassVar[t.Optional[t.Type]] = None def convert(self, value, param, ctx): try: diff --git a/tests/test_imports.py b/tests/test_imports.py index 54d9559..d13b085 100644 --- a/tests/test_imports.py +++ b/tests/test_imports.py @@ -46,6 +46,8 @@ ALLOWED_IMPORTS = { "fcntl", "datetime", "enum", + "typing", + "types", } if WIN: @@ -2,6 +2,7 @@ envlist = py{39,38,37,36,py3} style + typing docs skip_missing_interpreters = true @@ -14,6 +15,10 @@ deps = pre-commit skip_install = true commands = pre-commit run --all-files --show-diff-on-failure +[testenv:typing] +deps = -r requirements/typing.txt +commands = mypy + [testenv:docs] deps = -r requirements/docs.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html |