mypy passes with check_untyped_defs

This commit is contained in:
Anthony Sottile 2020-01-10 19:12:56 -08:00
parent ab19b94811
commit fa536a8693
25 changed files with 161 additions and 89 deletions

16
.gitignore vendored
View file

@ -1,14 +1,8 @@
*.egg-info
*.iml
*.py[co]
.*.sw[a-z]
.coverage
.idea
.project
.pydevproject
.tox
.venv.touch
/.coverage
/.mypy_cache
/.pytest_cache
/.tox
/dist
/venv*
coverage-html
dist
.pytest_cache

View file

@ -42,6 +42,11 @@ repos:
rev: v1.6.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.761
hooks:
- id: mypy
exclude: ^testing/resources/
- repo: meta
hooks:
- id: check-hooks-apply

View file

@ -2,7 +2,7 @@ import os
import sys
terminal_supports_color = True
if os.name == 'nt': # pragma: no cover (windows)
if sys.platform == 'win32': # pragma: no cover (windows)
from pre_commit.color_windows import enable_virtual_terminal_processing
try:
enable_virtual_terminal_processing()

View file

@ -1,10 +1,14 @@
from ctypes import POINTER
from ctypes import windll
from ctypes import WinError
from ctypes import WINFUNCTYPE
from ctypes.wintypes import BOOL
from ctypes.wintypes import DWORD
from ctypes.wintypes import HANDLE
import sys
assert sys.platform == 'win32'
from ctypes import POINTER # noqa: E402
from ctypes import windll # noqa: E402
from ctypes import WinError # noqa: E402
from ctypes import WINFUNCTYPE # noqa: E402
from ctypes.wintypes import BOOL # noqa: E402
from ctypes.wintypes import DWORD # noqa: E402
from ctypes.wintypes import HANDLE # noqa: E402
STD_OUTPUT_HANDLE = -11
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4

View file

@ -1,6 +1,8 @@
import collections
import os.path
import re
from typing import List
from typing import Optional
from aspy.yaml import ordered_dump
from aspy.yaml import ordered_load
@ -121,7 +123,7 @@ def autoupdate(config_file, store, tags_only, freeze, repos=()):
"""Auto-update the pre-commit config to the latest versions of repos."""
migrate_config(config_file, quiet=True)
retv = 0
rev_infos = []
rev_infos: List[Optional[RevInfo]] = []
changed = False
config = load_config(config_file)

View file

@ -1,13 +1,26 @@
import collections
import contextlib
import enum
import os
from typing import NamedTuple
from typing import Tuple
from typing import Union
UNSET = collections.namedtuple('UNSET', ())()
class _Unset(enum.Enum):
UNSET = 1
Var = collections.namedtuple('Var', ('name', 'default'))
Var.__new__.__defaults__ = ('',)
UNSET = _Unset.UNSET
class Var(NamedTuple):
name: str
default: str = ''
SubstitutionT = Tuple[Union[str, Var], ...]
ValueT = Union[str, _Unset, SubstitutionT]
PatchesT = Tuple[Tuple[str, ValueT], ...]
def format_env(parts, env):

View file

@ -2,6 +2,7 @@ import contextlib
import os.path
import sys
import traceback
from typing import Union
import pre_commit.constants as C
from pre_commit import five
@ -32,8 +33,8 @@ def _log_and_exit(msg, exc, formatted):
output.write_line(f'Check the log at {log_path}')
with open(log_path, 'wb') as log:
def _log_line(*s): # type: (*str) -> None
output.write_line(*s, stream=log)
def _log_line(s: Union[None, str, bytes] = None) -> None:
output.write_line(s, stream=log)
_log_line('### version information')
_log_line()

View file

@ -1,8 +1,9 @@
import contextlib
import errno
import os
try: # pragma: no cover (windows)
if os.name == 'nt': # pragma: no cover (windows)
import msvcrt
# https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/locking
@ -14,12 +15,14 @@ try: # pragma: no cover (windows)
@contextlib.contextmanager
def _locked(fileno, blocked_cb):
try:
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region)
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_NBLCK, _region) # type: ignore
except OSError:
blocked_cb()
while True:
try:
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region)
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_LOCK, _region) # type: ignore # noqa: E501
except OSError as e:
# Locking violation. Returned when the _LK_LOCK or _LK_RLCK
# flag is specified and the file cannot be locked after 10
@ -37,8 +40,9 @@ try: # pragma: no cover (windows)
# The documentation however states:
# "Regions should be locked only briefly and should be unlocked
# before closing a file or exiting the program."
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region)
except ImportError: # pragma: windows no cover
# TODO: https://github.com/python/typeshed/pull/3607
msvcrt.locking(fileno, msvcrt.LK_UNLCK, _region) # type: ignore
else: # pramga: windows no cover
import fcntl
@contextlib.contextmanager

View file

@ -1,3 +1,6 @@
from typing import Any
from typing import Dict
from pre_commit.languages import conda
from pre_commit.languages import docker
from pre_commit.languages import docker_image
@ -13,6 +16,7 @@ from pre_commit.languages import script
from pre_commit.languages import swift
from pre_commit.languages import system
# A language implements the following constant and functions in its module:
#
# # Use None for no environment
@ -49,7 +53,7 @@ from pre_commit.languages import system
# (returncode, output)
# """
languages = {
languages: Dict[str, Any] = {
'conda': conda,
'docker': docker,
'docker_image': docker_image,

View file

@ -2,6 +2,7 @@ import contextlib
import os
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import SubstitutionT
from pre_commit.envcontext import UNSET
from pre_commit.envcontext import Var
from pre_commit.languages import helpers
@ -18,7 +19,7 @@ def get_env_patch(env):
# they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin,
# $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
# seems to be used for python.exe.
path = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
if os.name == 'nt': # pragma: no cover (platform specific)
path = (env, os.pathsep) + path
path = (os.path.join(env, 'Scripts'), os.pathsep) + path

View file

@ -1,5 +1,6 @@
import hashlib
import os
from typing import Tuple
import pre_commit.constants as C
from pre_commit import five
@ -42,7 +43,7 @@ def assert_docker_available(): # pragma: windows no cover
def build_docker_image(prefix, **kwargs): # pragma: windows no cover
pull = kwargs.pop('pull')
assert not kwargs, kwargs
cmd = (
cmd: Tuple[str, ...] = (
'docker', 'build',
'--tag', docker_tag(prefix),
'--label', PRE_COMMIT_LABEL,

View file

@ -1,4 +1,5 @@
import contextlib
import functools
import os
import sys
@ -64,7 +65,8 @@ def _find_by_sys_executable():
return None
def _get_default_version(): # pragma: no cover (platform dependent)
@functools.lru_cache(maxsize=1)
def get_default_version(): # pragma: no cover (platform dependent)
# First attempt from `sys.executable` (or the realpath)
exe = _find_by_sys_executable()
if exe:
@ -86,15 +88,6 @@ def _get_default_version(): # pragma: no cover (platform dependent)
return C.DEFAULT
def get_default_version():
# TODO: when dropping python2, use `functools.lru_cache(maxsize=1)`
try:
return get_default_version.cached_version
except AttributeError:
get_default_version.cached_version = _get_default_version()
return get_default_version()
def _sys_executable_matches(version):
if version == 'python':
return True

View file

@ -5,6 +5,7 @@ import tarfile
import pre_commit.constants as C
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import PatchesT
from pre_commit.envcontext import Var
from pre_commit.languages import helpers
from pre_commit.util import CalledProcessError
@ -18,7 +19,7 @@ healthy = helpers.basic_healthy
def get_env_patch(venv, language_version): # pragma: windows no cover
patches = (
patches: PatchesT = (
('GEM_HOME', os.path.join(venv, 'gems')),
('RBENV_ROOT', venv),
('BUNDLE_IGNORE_CONFIG', '1'),

View file

@ -1,5 +1,7 @@
import contextlib
import os.path
from typing import Set
from typing import Tuple
import toml
@ -71,7 +73,7 @@ def install_environment(prefix, version, additional_dependencies):
_add_dependencies(prefix.path('Cargo.toml'), lib_deps)
with clean_path_on_failure(directory):
packages_to_install = {('--path', '.')}
packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')}
for cli_dep in cli_deps:
cli_dep = cli_dep[len('cli:'):]
package, _, version = cli_dep.partition(':')

View file

@ -1,8 +1,8 @@
import contextlib
import sys
from pre_commit import color
from pre_commit import five
from pre_commit.util import noop_context
def get_hook_message(
@ -71,14 +71,12 @@ def write(s, stream=stdout_byte_stream):
def write_line(s=None, stream=stdout_byte_stream, logfile_name=None):
output_streams = [stream]
if logfile_name:
ctx = open(logfile_name, 'ab')
output_streams.append(ctx)
else:
ctx = noop_context()
with contextlib.ExitStack() as exit_stack:
output_streams = [stream]
if logfile_name:
stream = exit_stack.enter_context(open(logfile_name, 'ab'))
output_streams.append(stream)
with ctx:
for output_stream in output_streams:
if s is not None:
output_stream.write(five.to_bytes(s))

View file

@ -1,8 +1,10 @@
import collections
import json
import logging
import os
import shlex
from typing import NamedTuple
from typing import Sequence
from typing import Set
import pre_commit.constants as C
from pre_commit import five
@ -49,8 +51,29 @@ def _write_state(prefix, venv, state):
_KEYS = tuple(item.key for item in MANIFEST_HOOK_DICT.items)
class Hook(collections.namedtuple('Hook', ('src', 'prefix') + _KEYS)):
__slots__ = ()
class Hook(NamedTuple):
src: str
prefix: Prefix
id: str
name: str
entry: str
language: str
alias: str
files: str
exclude: str
types: Sequence[str]
exclude_types: Sequence[str]
additional_dependencies: Sequence[str]
args: Sequence[str]
always_run: bool
pass_filenames: bool
description: str
language_version: str
log_file: str
minimum_pre_commit_version: str
require_serial: bool
stages: Sequence[str]
verbose: bool
@property
def cmd(self):
@ -201,7 +224,7 @@ def _repository_hooks(repo_config, store, root_config):
def install_hook_envs(hooks, store):
def _need_installed():
seen = set()
seen: Set[Hook] = set()
ret = []
for hook in hooks:
if hook.install_key not in seen and not hook.installed():

View file

@ -4,6 +4,7 @@ import distutils.spawn
import os
import subprocess
import sys
from typing import Tuple
# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)
@ -12,10 +13,10 @@ HERE = os.path.dirname(os.path.abspath(__file__))
Z40 = '0' * 40
ID_HASH = '138fd403232d2ddd5efb44317e38bf03'
# start templated
CONFIG = None
HOOK_TYPE = None
INSTALL_PYTHON = None
SKIP_ON_MISSING_CONFIG = None
CONFIG = ''
HOOK_TYPE = ''
INSTALL_PYTHON = ''
SKIP_ON_MISSING_CONFIG = False
# end templated
@ -123,7 +124,7 @@ def _rev_exists(rev):
def _pre_push(stdin):
remote = sys.argv[1]
opts = ()
opts: Tuple[str, ...] = ()
for line in stdin.decode('UTF-8').splitlines():
_, local_sha, _, remote_sha = line.split()
if local_sha == Z40:
@ -146,8 +147,8 @@ def _pre_push(stdin):
# pushing the whole tree including root commit
opts = ('--all-files',)
else:
cmd = ('git', 'rev-parse', f'{first_ancestor}^')
source = subprocess.check_output(cmd).decode().strip()
rev_cmd = ('git', 'rev-parse', f'{first_ancestor}^')
source = subprocess.check_output(rev_cmd).decode().strip()
opts = ('--origin', local_sha, '--source', source)
if opts:

View file

@ -152,6 +152,7 @@ if os.name != 'nt': # pragma: windows no cover
# tty flags normally change \n to \r\n
attrs = termios.tcgetattr(self.r)
assert isinstance(attrs[1], int)
attrs[1] &= ~(termios.ONLCR | termios.OPOST)
termios.tcsetattr(self.r, termios.TCSANOW, attrs)

View file

@ -4,6 +4,7 @@ import math
import os
import subprocess
import sys
from typing import List
from pre_commit import parse_shebang
from pre_commit.util import cmd_output_b
@ -56,7 +57,7 @@ def partition(cmd, varargs, target_concurrency, _max_length=None):
cmd = tuple(cmd)
ret = []
ret_cmd = []
ret_cmd: List[str] = []
# Reversed so arguments are in order
varargs = list(reversed(varargs))

View file

@ -52,3 +52,15 @@ exclude =
[bdist_wheel]
universal = True
[mypy]
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
no_implicit_optional = true
[mypy-testing.*]
disallow_untyped_defs = false
[mypy-tests.*]
disallow_untyped_defs = false

View file

@ -18,7 +18,7 @@ def test_install_environment_argspec(language):
expected_argspec = ArgSpec(
args=['prefix', 'version', 'additional_dependencies'],
)
argspec = inspect.getfullargpsec(languages[language].install_environment)
argspec = inspect.getfullargspec(languages[language].install_environment)
assert argspec == expected_argspec
@ -28,21 +28,21 @@ def test_ENVIRONMENT_DIR(language):
@pytest.mark.parametrize('language', all_languages)
def test_run_hook_argpsec(language):
def test_run_hook_argspec(language):
expected_argspec = ArgSpec(args=['hook', 'file_args', 'color'])
argspec = inspect.getfullargpsec(languages[language].run_hook)
argspec = inspect.getfullargspec(languages[language].run_hook)
assert argspec == expected_argspec
@pytest.mark.parametrize('language', all_languages)
def test_get_default_version_argspec(language):
expected_argspec = ArgSpec(args=[])
argspec = inspect.getfullargpsec(languages[language].get_default_version)
argspec = inspect.getfullargspec(languages[language].get_default_version)
assert argspec == expected_argspec
@pytest.mark.parametrize('language', all_languages)
def test_healthy_argspec(language):
expected_argspec = ArgSpec(args=['prefix', 'language_version'])
argspec = inspect.getfullargpsec(languages[language].healthy)
argspec = inspect.getfullargspec(languages[language].healthy)
assert argspec == expected_argspec

View file

@ -1,5 +1,8 @@
import argparse
import os.path
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from unittest import mock
import pytest
@ -24,11 +27,11 @@ def test_append_replace_default(argv, expected):
assert parser.parse_args(argv).f == expected
class Args:
def __init__(self, **kwargs):
kwargs.setdefault('command', 'help')
kwargs.setdefault('config', C.CONFIG_FILE)
self.__dict__.update(kwargs)
class Args(NamedTuple):
command: str = 'help'
config: str = C.CONFIG_FILE
files: Sequence[str] = []
repo: Optional[str] = None
def test_adjust_args_and_chdir_not_in_git_dir(in_tmpdir):
@ -73,6 +76,7 @@ def test_adjust_args_try_repo_repo_relative(in_git_dir):
in_git_dir.join('foo').ensure_dir().chdir()
args = Args(command='try-repo', repo='../foo', files=[])
assert args.repo is not None
assert os.path.exists(args.repo)
main._adjust_args_and_chdir(args)
assert os.getcwd() == in_git_dir

View file

@ -11,6 +11,12 @@ from pre_commit.envcontext import Var
from pre_commit.util import make_executable
def _echo_exe() -> str:
exe = distutils.spawn.find_executable('echo')
assert exe is not None
return exe
def test_file_doesnt_exist():
assert parse_shebang.parse_filename('herp derp derp') == ()
@ -27,8 +33,7 @@ def test_find_executable_full_path():
def test_find_executable_on_path():
expected = distutils.spawn.find_executable('echo')
assert parse_shebang.find_executable('echo') == expected
assert parse_shebang.find_executable('echo') == _echo_exe()
def test_find_executable_not_found_none():
@ -110,30 +115,29 @@ def test_normexe_already_full_path():
def test_normexe_gives_full_path():
expected = distutils.spawn.find_executable('echo')
assert parse_shebang.normexe('echo') == expected
assert os.sep in expected
assert parse_shebang.normexe('echo') == _echo_exe()
assert os.sep in _echo_exe()
def test_normalize_cmd_trivial():
cmd = (distutils.spawn.find_executable('echo'), 'hi')
cmd = (_echo_exe(), 'hi')
assert parse_shebang.normalize_cmd(cmd) == cmd
def test_normalize_cmd_PATH():
cmd = ('echo', '--version')
expected = (distutils.spawn.find_executable('echo'), '--version')
expected = (_echo_exe(), '--version')
assert parse_shebang.normalize_cmd(cmd) == expected
def test_normalize_cmd_shebang(in_tmpdir):
echo = distutils.spawn.find_executable('echo').replace(os.sep, '/')
echo = _echo_exe().replace(os.sep, '/')
path = write_executable(echo)
assert parse_shebang.normalize_cmd((path,)) == (echo, path)
def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir):
echo = distutils.spawn.find_executable('echo').replace(os.sep, '/')
echo = _echo_exe().replace(os.sep, '/')
path = write_executable(echo)
with bin_on_path():
ret = parse_shebang.normalize_cmd(('run',))
@ -141,7 +145,7 @@ def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir):
def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir):
echo = distutils.spawn.find_executable('echo')
echo = _echo_exe()
path = write_executable('/usr/bin/env echo')
with bin_on_path():
ret = parse_shebang.normalize_cmd(('run',))

View file

@ -2,6 +2,8 @@ import os.path
import re
import shutil
import sys
from typing import Any
from typing import Dict
from unittest import mock
import cfgv
@ -763,7 +765,7 @@ def test_local_python_repo(store, local_python_config):
def test_default_language_version(store, local_python_config):
config = {
config: Dict[str, Any] = {
'default_language_version': {'python': 'fake'},
'default_stages': ['commit'],
'repos': [local_python_config],
@ -780,7 +782,7 @@ def test_default_language_version(store, local_python_config):
def test_default_stages(store, local_python_config):
config = {
config: Dict[str, Any] = {
'default_language_version': {'python': C.DEFAULT},
'default_stages': ['commit'],
'repos': [local_python_config],

View file

@ -24,7 +24,8 @@ def patch_dir(tempdir_factory):
def get_short_git_status():
git_status = cmd_output('git', 'status', '-s')[1]
return dict(reversed(line.split()) for line in git_status.splitlines())
line_parts = [line.split() for line in git_status.splitlines()]
return {v: k for k, v in line_parts}
@pytest.fixture