summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/tests.yaml1
-rw-r--r--requirements/dev.in1
-rw-r--r--requirements/dev.txt8
-rw-r--r--requirements/typing.in1
-rw-r--r--requirements/typing.txt14
-rw-r--r--setup.cfg20
-rw-r--r--src/click/_compat.py15
-rw-r--r--src/click/_winconsole.py69
-rw-r--r--src/click/core.py6
-rw-r--r--src/click/formatting.py3
-rw-r--r--src/click/shell_completion.py11
-rw-r--r--src/click/termui.py3
-rw-r--r--src/click/types.py7
-rw-r--r--tests/test_imports.py2
-rw-r--r--tox.ini5
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
diff --git a/setup.cfg b/setup.cfg
index 5107553..1f98dc5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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:
diff --git a/tox.ini b/tox.ini
index 2ca13c9..de68730 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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