Add types to pre-commit

This commit is contained in:
Anthony Sottile 2020-01-10 23:32:28 -08:00
parent fa536a8693
commit 327ed924a3
62 changed files with 911 additions and 411 deletions

View file

@ -6,6 +6,16 @@ import stat
import subprocess
import sys
import tempfile
from types import TracebackType
from typing import Any
from typing import Callable
from typing import Dict
from typing import Generator
from typing import IO
from typing import Optional
from typing import Tuple
from typing import Type
from typing import Union
from pre_commit import five
from pre_commit import parse_shebang
@ -17,8 +27,10 @@ else: # pragma: no cover (<PY37)
from importlib_resources import open_binary
from importlib_resources import read_text
EnvironT = Union[Dict[str, str], 'os._Environ']
def mkdirp(path):
def mkdirp(path: str) -> None:
try:
os.makedirs(path)
except OSError:
@ -27,7 +39,7 @@ def mkdirp(path):
@contextlib.contextmanager
def clean_path_on_failure(path):
def clean_path_on_failure(path: str) -> Generator[None, None, None]:
"""Cleans up the directory on an exceptional failure."""
try:
yield
@ -38,12 +50,12 @@ def clean_path_on_failure(path):
@contextlib.contextmanager
def noop_context():
def noop_context() -> Generator[None, None, None]:
yield
@contextlib.contextmanager
def tmpdir():
def tmpdir() -> Generator[str, None, None]:
"""Contextmanager to create a temporary directory. It will be cleaned up
afterwards.
"""
@ -54,15 +66,15 @@ def tmpdir():
rmtree(tempdir)
def resource_bytesio(filename):
def resource_bytesio(filename: str) -> IO[bytes]:
return open_binary('pre_commit.resources', filename)
def resource_text(filename):
def resource_text(filename: str) -> str:
return read_text('pre_commit.resources', filename)
def make_executable(filename):
def make_executable(filename: str) -> None:
original_mode = os.stat(filename).st_mode
os.chmod(
filename, original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH,
@ -70,18 +82,23 @@ def make_executable(filename):
class CalledProcessError(RuntimeError):
def __init__(self, returncode, cmd, expected_returncode, stdout, stderr):
super().__init__(
returncode, cmd, expected_returncode, stdout, stderr,
)
def __init__(
self,
returncode: int,
cmd: Tuple[str, ...],
expected_returncode: int,
stdout: bytes,
stderr: Optional[bytes],
) -> None:
super().__init__(returncode, cmd, expected_returncode, stdout, stderr)
self.returncode = returncode
self.cmd = cmd
self.expected_returncode = expected_returncode
self.stdout = stdout
self.stderr = stderr
def __bytes__(self):
def _indent_or_none(part):
def __bytes__(self) -> bytes:
def _indent_or_none(part: Optional[bytes]) -> bytes:
if part:
return b'\n ' + part.replace(b'\n', b'\n ')
else:
@ -97,11 +114,14 @@ class CalledProcessError(RuntimeError):
b'stderr:', _indent_or_none(self.stderr),
))
def __str__(self):
def __str__(self) -> str:
return self.__bytes__().decode('UTF-8')
def _cmd_kwargs(*cmd, **kwargs):
def _cmd_kwargs(
*cmd: str,
**kwargs: Any,
) -> Tuple[Tuple[str, ...], Dict[str, Any]]:
# py2/py3 on windows are more strict about the types here
cmd = tuple(five.n(arg) for arg in cmd)
kwargs['env'] = {
@ -113,7 +133,10 @@ def _cmd_kwargs(*cmd, **kwargs):
return cmd, kwargs
def cmd_output_b(*cmd, **kwargs):
def cmd_output_b(
*cmd: str,
**kwargs: Any,
) -> Tuple[int, bytes, Optional[bytes]]:
retcode = kwargs.pop('retcode', 0)
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)
@ -132,7 +155,7 @@ def cmd_output_b(*cmd, **kwargs):
return returncode, stdout_b, stderr_b
def cmd_output(*cmd, **kwargs):
def cmd_output(*cmd: str, **kwargs: Any) -> Tuple[int, str, Optional[str]]:
returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs)
stdout = stdout_b.decode('UTF-8') if stdout_b is not None else None
stderr = stderr_b.decode('UTF-8') if stderr_b is not None else None
@ -144,10 +167,11 @@ if os.name != 'nt': # pragma: windows no cover
import termios
class Pty:
def __init__(self):
self.r = self.w = None
def __init__(self) -> None:
self.r: Optional[int] = None
self.w: Optional[int] = None
def __enter__(self):
def __enter__(self) -> 'Pty':
self.r, self.w = openpty()
# tty flags normally change \n to \r\n
@ -158,21 +182,29 @@ if os.name != 'nt': # pragma: windows no cover
return self
def close_w(self):
def close_w(self) -> None:
if self.w is not None:
os.close(self.w)
self.w = None
def close_r(self):
def close_r(self) -> None:
assert self.r is not None
os.close(self.r)
self.r = None
def __exit__(self, exc_type, exc_value, traceback):
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
self.close_w()
self.close_r()
def cmd_output_p(*cmd, **kwargs):
def cmd_output_p(
*cmd: str,
**kwargs: Any,
) -> Tuple[int, bytes, Optional[bytes]]:
assert kwargs.pop('retcode') is None
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
cmd, kwargs = _cmd_kwargs(*cmd, **kwargs)
@ -183,6 +215,7 @@ if os.name != 'nt': # pragma: windows no cover
return e.to_output()
with open(os.devnull) as devnull, Pty() as pty:
assert pty.r is not None
kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w})
proc = subprocess.Popen(cmd, **kwargs)
pty.close_w()
@ -206,9 +239,13 @@ else: # pragma: no cover
cmd_output_p = cmd_output_b
def rmtree(path):
def rmtree(path: str) -> None:
"""On windows, rmtree fails for readonly dirs."""
def handle_remove_readonly(func, path, exc):
def handle_remove_readonly(
func: Callable[..., Any],
path: str,
exc: Tuple[Type[OSError], OSError, TracebackType],
) -> None:
excvalue = exc[1]
if (
func in (os.rmdir, os.remove, os.unlink) and
@ -222,6 +259,6 @@ def rmtree(path):
shutil.rmtree(path, ignore_errors=False, onerror=handle_remove_readonly)
def parse_version(s):
def parse_version(s: str) -> Tuple[int, ...]:
"""poor man's version comparison"""
return tuple(int(p) for p in s.split('.'))