from contextlib import contextmanager import filelock import os import pytest import re import subprocess from subprocess import PIPE import sys import tempfile from typing import Tuple, List, Generator import mypy.api from mypy.test.config import package_path, pip_lock, pip_timeout from mypy.util import try_find_python2_interpreter from mypy.test.data import DataDrivenTestCase, DataSuite from mypy.test.config import test_temp_dir from mypy.test.helpers import assert_string_arrays_equal, perform_file_operations # NOTE: options.use_builtins_fixtures should not be set in these # tests, otherwise mypy will ignore installed third-party packages. class PEP561Suite(DataSuite): files = [ 'pep561.test', ] base_path = '.' def run_case(self, test_case: DataDrivenTestCase) -> None: test_pep561(test_case) @contextmanager def virtualenv( python_executable: str = sys.executable ) -> Generator[Tuple[str, str], None, None]: """Context manager that creates a virtualenv in a temporary directory returns the path to the created Python executable""" # Sadly, we need virtualenv, as the Python 3 venv module does not support creating a venv # for Python 2, and Python 2 does not have its own venv. with tempfile.TemporaryDirectory() as venv_dir: proc = subprocess.run([sys.executable, '-m', 'virtualenv', f'-p{python_executable}', venv_dir], cwd=os.getcwd(), stdout=PIPE, stderr=PIPE) if proc.returncode != 0: err = proc.stdout.decode('utf-8') + proc.stderr.decode('utf-8') raise Exception("Failed to create venv. Do you have virtualenv installed?\n" + err) if sys.platform == 'win32': yield venv_dir, os.path.abspath(os.path.join(venv_dir, 'Scripts', 'python')) else: yield venv_dir, os.path.abspath(os.path.join(venv_dir, 'bin', 'python')) def install_package(pkg: str, python_executable: str = sys.executable, use_pip: bool = True, editable: bool = False) -> None: """Install a package from test-data/packages/pkg/""" working_dir = os.path.join(package_path, pkg) with tempfile.TemporaryDirectory() as dir: if use_pip: install_cmd = [python_executable, '-m', 'pip', 'install'] if editable: install_cmd.append('-e') install_cmd.append('.') else: install_cmd = [python_executable, 'setup.py'] if editable: install_cmd.append('develop') else: install_cmd.append('install') # Note that newer versions of pip (21.3+) don't # follow this env variable, but this is for compatibility env = {'PIP_BUILD': dir} # Inherit environment for Windows env.update(os.environ) try: with filelock.FileLock(pip_lock, timeout=pip_timeout): proc = subprocess.run(install_cmd, cwd=working_dir, stdout=PIPE, stderr=PIPE, env=env) except filelock.Timeout as err: raise Exception("Failed to acquire {}".format(pip_lock)) from err if proc.returncode != 0: raise Exception(proc.stdout.decode('utf-8') + proc.stderr.decode('utf-8')) def test_pep561(testcase: DataDrivenTestCase) -> None: """Test running mypy on files that depend on PEP 561 packages.""" assert testcase.old_cwd is not None, "test was not properly set up" if 'python2' in testcase.name.lower(): python = try_find_python2_interpreter() if python is None: pytest.skip() else: python = sys.executable assert python is not None, "Should be impossible" pkgs, pip_args = parse_pkgs(testcase.input[0]) mypy_args = parse_mypy_args(testcase.input[1]) use_pip = True editable = False for arg in pip_args: if arg == 'no-pip': use_pip = False elif arg == 'editable': editable = True assert pkgs != [], "No packages to install for PEP 561 test?" with virtualenv(python) as venv: venv_dir, python_executable = venv for pkg in pkgs: install_package(pkg, python_executable, use_pip, editable) cmd_line = list(mypy_args) has_program = not ('-p' in cmd_line or '--package' in cmd_line) if has_program: program = testcase.name + '.py' with open(program, 'w', encoding='utf-8') as f: for s in testcase.input: f.write(f'{s}\n') cmd_line.append(program) cmd_line.extend(['--no-error-summary']) if python_executable != sys.executable: cmd_line.append(f'--python-executable={python_executable}') steps = testcase.find_steps() if steps != [[]]: steps = [[]] + steps # type: ignore[operator,assignment] for i, operations in enumerate(steps): perform_file_operations(operations) output = [] # Type check the module out, err, returncode = mypy.api.run(cmd_line) # split lines, remove newlines, and remove directory of test case for line in (out + err).splitlines(): if line.startswith(test_temp_dir + os.sep): output.append(line[len(test_temp_dir + os.sep):].rstrip("\r\n")) else: # Normalize paths so that the output is the same on Windows and Linux/macOS. line = line.replace(test_temp_dir + os.sep, test_temp_dir + '/') output.append(line.rstrip("\r\n")) iter_count = '' if i == 0 else f' on iteration {i + 1}' expected = testcase.output if i == 0 else testcase.output2.get(i + 1, []) assert_string_arrays_equal(expected, output, 'Invalid output ({}, line {}){}'.format( testcase.file, testcase.line, iter_count)) if has_program: os.remove(program) def parse_pkgs(comment: str) -> Tuple[List[str], List[str]]: if not comment.startswith('# pkgs:'): return ([], []) else: pkgs_str, *args = comment[7:].split(';') return ([pkg.strip() for pkg in pkgs_str.split(',')], [arg.strip() for arg in args]) def parse_mypy_args(line: str) -> List[str]: m = re.match('# flags: (.*)$', line) if not m: return [] # No args; mypy will spit out an error. return m.group(1).split() def test_mypy_path_is_respected() -> None: assert False packages = 'packages' pkg_name = 'a' with tempfile.TemporaryDirectory() as temp_dir: old_dir = os.getcwd() os.chdir(temp_dir) try: # Create the pkg for files to go into full_pkg_name = os.path.join(temp_dir, packages, pkg_name) os.makedirs(full_pkg_name) # Create the empty __init__ file to declare a package pkg_init_name = os.path.join(temp_dir, packages, pkg_name, '__init__.py') open(pkg_init_name, 'w', encoding='utf8').close() mypy_config_path = os.path.join(temp_dir, 'mypy.ini') with open(mypy_config_path, 'w') as mypy_file: mypy_file.write('[mypy]\n') mypy_file.write(f'mypy_path = ./{packages}\n') with virtualenv() as venv: venv_dir, python_executable = venv cmd_line_args = [] if python_executable != sys.executable: cmd_line_args.append(f'--python-executable={python_executable}') cmd_line_args.extend(['--config-file', mypy_config_path, '--package', pkg_name]) out, err, returncode = mypy.api.run(cmd_line_args) assert returncode == 0 finally: os.chdir(old_dir)