leverage mypy to check language implementations

This commit is contained in:
Anthony Sottile
2020-01-12 09:06:06 -08:00
parent 327ed924a3
commit 4eea90c26c
4 changed files with 69 additions and 114 deletions

View File

@@ -1,5 +1,9 @@
from typing import Any
from typing import Dict
from typing import Callable
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import TYPE_CHECKING
from pre_commit.languages import conda
from pre_commit.languages import docker
@@ -15,58 +19,43 @@ from pre_commit.languages import rust
from pre_commit.languages import script
from pre_commit.languages import swift
from pre_commit.languages import system
from pre_commit.prefix import Prefix
if TYPE_CHECKING:
from pre_commit.repository import Hook
# A language implements the following constant and functions in its module:
#
# # Use None for no environment
# ENVIRONMENT_DIR = 'foo_env'
#
# def get_default_version():
# """Return a value to replace the 'default' value for language_version.
#
# return 'default' if there is no better option.
# """
#
# def healthy(prefix, language_version):
# """Return whether or not the environment is considered functional."""
#
# def install_environment(prefix, version, additional_dependencies):
# """Installs a repository in the given repository. Note that the current
# working directory will already be inside the repository.
#
# Args:
# prefix - `Prefix` bound to the repository.
# version - A version specified in the hook configuration or 'default'.
# """
#
# def run_hook(hook, file_args, color):
# """Runs a hook and returns the returncode and output of running that
# hook.
#
# Args:
# hook - `Hook`
# file_args - The files to be run
# color - whether the hook should be given a pty (when supported)
#
# Returns:
# (returncode, output)
# """
class Language(NamedTuple):
name: str
# Use `None` for no installation / environment
ENVIRONMENT_DIR: Optional[str]
# return a value to replace `'default` for `language_version`
get_default_version: Callable[[], str]
# return whether the environment is healthy (or should be rebuilt)
healthy: Callable[[Prefix, str], bool]
# install a repository for the given language and language_version
install_environment: Callable[[Prefix, str, Sequence[str]], None]
# execute a hook and return the exit code and output
run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]'
languages: Dict[str, Any] = {
'conda': conda,
'docker': docker,
'docker_image': docker_image,
'fail': fail,
'golang': golang,
'node': node,
'pygrep': pygrep,
'python': python,
'python_venv': python_venv,
'ruby': ruby,
'rust': rust,
'script': script,
'swift': swift,
'system': system,
# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018
languages = {
# BEGIN GENERATED (testing/gen-languages-all)
'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501
'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501
'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501
'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501
'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501
'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501
'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501
'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501
'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501
# END GENERATED
}
all_languages = sorted(languages)

View File

@@ -113,6 +113,7 @@ class Hook(NamedTuple):
logger.info('This may take a few minutes...')
lang = languages[self.language]
assert lang.ENVIRONMENT_DIR is not None
venv = environment_dir(lang.ENVIRONMENT_DIR, self.language_version)
# There's potentially incomplete cleanup from previous runs

27
testing/gen-languages-all Executable file
View File

@@ -0,0 +1,27 @@
#!/usr/bin/env python3
import sys
LANGUAGES = [
'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'pygrep',
'python', 'python_venv', 'ruby', 'rust', 'script', 'swift', 'system',
]
FIELDS = [
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',
'run_hook',
]
def main() -> int:
print(f' # BEGIN GENERATED ({sys.argv[0]})')
for lang in LANGUAGES:
parts = [f' {lang!r}: Language(name={lang!r}']
for k in FIELDS:
parts.append(f', {k}={lang}.{k}')
parts.append('), # noqa: E501')
print(''.join(parts))
print(' # END GENERATED')
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -1,62 +0,0 @@
import inspect
from typing import Sequence
from typing import Tuple
import pytest
from pre_commit.languages.all import all_languages
from pre_commit.languages.all import languages
from pre_commit.prefix import Prefix
def _argspec(annotations):
args = [k for k in annotations if k != 'return']
return inspect.FullArgSpec(
args=args, annotations=annotations,
varargs=None, varkw=None, defaults=None,
kwonlyargs=[], kwonlydefaults=None,
)
@pytest.mark.parametrize('language', all_languages)
def test_install_environment_argspec(language):
expected_argspec = _argspec({
'return': None,
'prefix': Prefix,
'version': str,
'additional_dependencies': Sequence[str],
})
argspec = inspect.getfullargspec(languages[language].install_environment)
assert argspec == expected_argspec
@pytest.mark.parametrize('language', all_languages)
def test_ENVIRONMENT_DIR(language):
assert hasattr(languages[language], 'ENVIRONMENT_DIR')
@pytest.mark.parametrize('language', all_languages)
def test_run_hook_argspec(language):
expected_argspec = _argspec({
'return': Tuple[int, bytes],
'hook': 'Hook', 'file_args': Sequence[str], 'color': bool,
})
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({'return': str})
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({
'return': bool,
'prefix': Prefix, 'language_version': str,
})
argspec = inspect.getfullargspec(languages[language].healthy)
assert argspec == expected_argspec