Skip to content

Commit 8671fc5

Browse files
authored
regr_test.py: Allow non-types dependencies (#9382)
1 parent 4379a6a commit 8671fc5

File tree

5 files changed

+252
-104
lines changed

5 files changed

+252
-104
lines changed

.github/workflows/tests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
cache: pip
101101
cache-dependency-path: requirements-tests.txt
102102
- run: pip install -r requirements-tests.txt
103-
- run: python ./tests/regr_test.py --all --quiet
103+
- run: python ./tests/regr_test.py --all --verbosity QUIET
104104

105105
pyright:
106106
name: Test typeshed with pyright

tests/mypy_test.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ def add_third_party_files(
274274
seen_dists.add(distribution)
275275

276276
stubs_dir = Path("stubs")
277-
dependencies = get_recursive_requirements(distribution)
277+
dependencies = get_recursive_requirements(distribution).typeshed_pkgs
278278

279279
for dependency in dependencies:
280280
if dependency in seen_dists:

tests/regr_test.py

+135-60
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,30 @@
1010
import subprocess
1111
import sys
1212
import tempfile
13+
from enum import IntEnum
1314
from itertools import product
1415
from pathlib import Path
1516
from typing_extensions import TypeAlias
1617

1718
from utils import (
1819
PackageInfo,
20+
VenvInfo,
1921
colored,
2022
get_all_testcase_directories,
23+
get_mypy_req,
2124
get_recursive_requirements,
25+
make_venv,
2226
print_error,
2327
print_success_msg,
2428
testcase_dir_from_package_name,
2529
)
2630

2731
ReturnCode: TypeAlias = int
2832

33+
TEST_CASES = "test_cases"
34+
VENV_DIR = ".venv"
35+
TYPESHED = "typeshed"
36+
2937
SUPPORTED_PLATFORMS = ["linux", "darwin", "win32"]
3038
SUPPORTED_VERSIONS = ["3.11", "3.10", "3.9", "3.8", "3.7"]
3139

@@ -34,7 +42,7 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
3442
"""Helper function for argument-parsing"""
3543

3644
if package_name == "stdlib":
37-
return PackageInfo("stdlib", Path("test_cases"))
45+
return PackageInfo("stdlib", Path(TEST_CASES))
3846
test_case_dir = testcase_dir_from_package_name(package_name)
3947
if test_case_dir.is_dir():
4048
if not os.listdir(test_case_dir):
@@ -43,6 +51,12 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
4351
raise argparse.ArgumentTypeError(f"No test cases found for {package_name!r}!")
4452

4553

54+
class Verbosity(IntEnum):
55+
QUIET = 0
56+
NORMAL = 1
57+
VERBOSE = 2
58+
59+
4660
parser = argparse.ArgumentParser(description="Script to run mypy against various test cases for typeshed's stubs")
4761
parser.add_argument(
4862
"packages_to_test",
@@ -59,7 +73,12 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
5973
"Note that this cannot be specified if --platform and/or --python-version are specified."
6074
),
6175
)
62-
parser.add_argument("--quiet", action="store_true", help="Print less output to the terminal")
76+
parser.add_argument(
77+
"--verbosity",
78+
choices=[member.name for member in Verbosity],
79+
default=Verbosity.NORMAL.name,
80+
help="Control how much output to print to the terminal",
81+
)
6382
parser.add_argument(
6483
"--platform",
6584
dest="platforms_to_test",
@@ -85,16 +104,64 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
85104
)
86105

87106

88-
def test_testcase_directory(package: PackageInfo, version: str, platform: str, quiet: bool) -> ReturnCode:
89-
package_name, test_case_directory = package
90-
is_stdlib = package_name == "stdlib"
107+
def verbose_log(msg: str) -> None:
108+
print(colored("\n" + msg, "blue"))
91109

92-
msg = f"Running mypy --platform {platform} --python-version {version} on the "
93-
msg += "standard library test cases..." if is_stdlib else f"test cases for {package_name!r}..."
94-
if not quiet:
95-
print(msg, end=" ")
96110

97-
# "--enable-error-code ignore-without-code" is purposefully ommited. See https://github.com/python/typeshed/pull/8083
111+
def setup_testcase_dir(package: PackageInfo, tempdir: Path, new_test_case_dir: Path, verbosity: Verbosity) -> None:
112+
if verbosity is verbosity.VERBOSE:
113+
verbose_log(f"Setting up testcase dir in {tempdir}")
114+
# --warn-unused-ignores doesn't work for files inside typeshed.
115+
# SO, to work around this, we copy the test_cases directory into a TemporaryDirectory,
116+
# and run the test cases inside of that.
117+
shutil.copytree(package.test_case_directory, new_test_case_dir)
118+
if package.is_stdlib:
119+
return
120+
121+
# HACK: we want to run these test cases in an isolated environment --
122+
# we want mypy to see all stub packages listed in the "requires" field of METADATA.toml
123+
# (and all stub packages required by those stub packages, etc. etc.),
124+
# but none of the other stubs in typeshed.
125+
#
126+
# The best way of doing that without stopping --warn-unused-ignore from working
127+
# seems to be to create a "new typeshed" directory in a tempdir
128+
# that has only the required stubs copied over.
129+
new_typeshed = tempdir / TYPESHED
130+
new_typeshed.mkdir()
131+
shutil.copytree(Path("stdlib"), new_typeshed / "stdlib")
132+
requirements = get_recursive_requirements(package.name)
133+
# mypy refuses to consider a directory a "valid typeshed directory"
134+
# unless there's a stubs/mypy-extensions path inside it,
135+
# so add that to the list of stubs to copy over to the new directory
136+
for requirement in {package.name, *requirements.typeshed_pkgs, "mypy-extensions"}:
137+
shutil.copytree(Path("stubs", requirement), new_typeshed / "stubs" / requirement)
138+
139+
if requirements.external_pkgs:
140+
if verbosity is Verbosity.VERBOSE:
141+
verbose_log(f"Setting up venv in {tempdir / VENV_DIR}")
142+
pip_exe = make_venv(tempdir / VENV_DIR).pip_exe
143+
pip_command = [pip_exe, "install", get_mypy_req(), *requirements.external_pkgs]
144+
if verbosity is Verbosity.VERBOSE:
145+
verbose_log(f"{pip_command=}")
146+
try:
147+
subprocess.run(pip_command, check=True, capture_output=True, text=True)
148+
except subprocess.CalledProcessError as e:
149+
print(e.stderr)
150+
raise
151+
152+
153+
def run_testcases(
154+
package: PackageInfo, version: str, platform: str, *, tempdir: Path, verbosity: Verbosity
155+
) -> subprocess.CompletedProcess[str]:
156+
env_vars = dict(os.environ)
157+
new_test_case_dir = tempdir / TEST_CASES
158+
testcasedir_already_setup = new_test_case_dir.exists() and new_test_case_dir.is_dir()
159+
160+
if not testcasedir_already_setup:
161+
setup_testcase_dir(package, tempdir=tempdir, new_test_case_dir=new_test_case_dir, verbosity=verbosity)
162+
163+
# "--enable-error-code ignore-without-code" is purposefully ommited.
164+
# See https://github.com/python/typeshed/pull/8083
98165
flags = [
99166
"--python-version",
100167
version,
@@ -103,67 +170,70 @@ def test_testcase_directory(package: PackageInfo, version: str, platform: str, q
103170
"--no-error-summary",
104171
"--platform",
105172
platform,
106-
"--no-site-packages",
107173
"--strict",
108174
"--pretty",
109175
]
110176

111-
# --warn-unused-ignores doesn't work for files inside typeshed.
112-
# SO, to work around this, we copy the test_cases directory into a TemporaryDirectory.
113-
with tempfile.TemporaryDirectory() as td:
114-
td_path = Path(td)
115-
new_test_case_dir = td_path / "test_cases"
116-
shutil.copytree(test_case_directory, new_test_case_dir)
117-
env_vars = dict(os.environ)
118-
if is_stdlib:
119-
flags.extend(["--custom-typeshed-dir", str(Path(__file__).parent.parent)])
177+
if package.is_stdlib:
178+
python_exe = sys.executable
179+
custom_typeshed = Path(__file__).parent.parent
180+
flags.append("--no-site-packages")
181+
else:
182+
custom_typeshed = tempdir / TYPESHED
183+
env_vars["MYPYPATH"] = os.pathsep.join(map(str, custom_typeshed.glob("stubs/*")))
184+
has_non_types_dependencies = (tempdir / VENV_DIR).exists()
185+
if has_non_types_dependencies:
186+
python_exe = VenvInfo.of_existing_venv(tempdir / VENV_DIR).python_exe
120187
else:
121-
# HACK: we want to run these test cases in an isolated environment --
122-
# we want mypy to see all stub packages listed in the "requires" field of METADATA.toml
123-
# (and all stub packages required by those stub packages, etc. etc.),
124-
# but none of the other stubs in typeshed.
125-
#
126-
# The best way of doing that without stopping --warn-unused-ignore from working
127-
# seems to be to create a "new typeshed" directory in a tempdir
128-
# that has only the required stubs copied over.
129-
new_typeshed = td_path / "typeshed"
130-
os.mkdir(new_typeshed)
131-
shutil.copytree(Path("stdlib"), new_typeshed / "stdlib")
132-
requirements = get_recursive_requirements(package_name)
133-
# mypy refuses to consider a directory a "valid typeshed directory"
134-
# unless there's a stubs/mypy-extensions path inside it,
135-
# so add that to the list of stubs to copy over to the new directory
136-
for requirement in requirements + ["mypy-extensions"]:
137-
shutil.copytree(Path("stubs", requirement), new_typeshed / "stubs" / requirement)
138-
env_vars["MYPYPATH"] = os.pathsep.join(map(str, new_typeshed.glob("stubs/*")))
139-
flags.extend(["--custom-typeshed-dir", str(td_path / "typeshed")])
140-
141-
# If the test-case filename ends with -py39,
142-
# only run the test if --python-version was set to 3.9 or higher (for example)
143-
for path in new_test_case_dir.rglob("*.py"):
144-
if match := re.fullmatch(r".*-py3(\d{1,2})", path.stem):
145-
minor_version_required = int(match[1])
146-
assert f"3.{minor_version_required}" in SUPPORTED_VERSIONS
147-
if minor_version_required <= int(version.split(".")[1]):
148-
flags.append(str(path))
149-
else:
150-
flags.append(str(path))
151-
152-
result = subprocess.run([sys.executable, "-m", "mypy", *flags], capture_output=True, env=env_vars)
188+
python_exe = sys.executable
189+
flags.append("--no-site-packages")
190+
191+
flags.extend(["--custom-typeshed-dir", str(custom_typeshed)])
192+
193+
# If the test-case filename ends with -py39,
194+
# only run the test if --python-version was set to 3.9 or higher (for example)
195+
for path in new_test_case_dir.rglob("*.py"):
196+
if match := re.fullmatch(r".*-py3(\d{1,2})", path.stem):
197+
minor_version_required = int(match[1])
198+
assert f"3.{minor_version_required}" in SUPPORTED_VERSIONS
199+
python_minor_version = int(version.split(".")[1])
200+
if minor_version_required > python_minor_version:
201+
continue
202+
flags.append(str(path))
203+
204+
mypy_command = [python_exe, "-m", "mypy"] + flags
205+
if verbosity is Verbosity.VERBOSE:
206+
verbose_log(f"{mypy_command=}")
207+
if "MYPYPATH" in env_vars:
208+
verbose_log(f"{env_vars['MYPYPATH']=}")
209+
else:
210+
verbose_log("MYPYPATH not set")
211+
return subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars)
212+
213+
214+
def test_testcase_directory(
215+
package: PackageInfo, version: str, platform: str, *, verbosity: Verbosity, tempdir: Path
216+
) -> ReturnCode:
217+
msg = f"Running mypy --platform {platform} --python-version {version} on the "
218+
msg += "standard library test cases..." if package.is_stdlib else f"test cases for {package.name!r}..."
219+
if verbosity > Verbosity.QUIET:
220+
print(msg, end=" ", flush=True)
221+
222+
result = run_testcases(package=package, version=version, platform=platform, tempdir=tempdir, verbosity=verbosity)
153223

154224
if result.returncode:
155-
if quiet:
225+
if verbosity > Verbosity.QUIET:
156226
# We'll already have printed this if --quiet wasn't passed.
157-
# If--quiet was passed, only print this if there were errors.
227+
# If --quiet was passed, only print this if there were errors.
158228
# If there are errors, the output is inscrutable if this isn't printed.
159229
print(msg, end=" ")
160230
print_error("failure\n")
161-
replacements = (str(new_test_case_dir), str(test_case_directory))
231+
replacements = (str(tempdir / TEST_CASES), str(package.test_case_directory))
162232
if result.stderr:
163-
print_error(result.stderr.decode(), fix_path=replacements)
233+
print_error(result.stderr, fix_path=replacements)
164234
if result.stdout:
165-
print_error(result.stdout.decode(), fix_path=replacements)
166-
elif not quiet:
235+
print_error(result.stdout, fix_path=replacements)
236+
elif verbosity > Verbosity.QUIET:
167237
print_success_msg()
168238
return result.returncode
169239

@@ -172,6 +242,7 @@ def main() -> ReturnCode:
172242
args = parser.parse_args()
173243

174244
testcase_directories = args.packages_to_test or get_all_testcase_directories()
245+
verbosity = Verbosity[args.verbosity]
175246
if args.all:
176247
if args.platforms_to_test:
177248
parser.error("Cannot specify both --platform and --all")
@@ -183,8 +254,12 @@ def main() -> ReturnCode:
183254
versions_to_test = args.versions_to_test or [f"3.{sys.version_info[1]}"]
184255

185256
code = 0
186-
for platform, version, directory in product(platforms_to_test, versions_to_test, testcase_directories):
187-
code = max(code, test_testcase_directory(directory, version, platform, args.quiet))
257+
for testcase_dir in testcase_directories:
258+
with tempfile.TemporaryDirectory() as td:
259+
tempdir = Path(td)
260+
for platform, version in product(platforms_to_test, versions_to_test):
261+
this_code = test_testcase_directory(testcase_dir, version, platform, verbosity=verbosity, tempdir=tempdir)
262+
code = max(code, this_code)
188263
if code:
189264
print_error("\nTest completed with errors")
190265
else:

tests/stubtest_third_party.py

+4-27
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@
44
from __future__ import annotations
55

66
import argparse
7-
import functools
87
import os
98
import subprocess
109
import sys
1110
import tempfile
12-
import venv
1311
from pathlib import Path
1412
from typing import NoReturn
1513

1614
import tomli
17-
from utils import colored, print_error, print_success_msg
18-
19-
20-
@functools.lru_cache()
21-
def get_mypy_req() -> str:
22-
with open("requirements-tests.txt", encoding="UTF-8") as f:
23-
return next(line.strip() for line in f if "mypy" in line)
15+
from utils import colored, get_mypy_req, make_venv, print_error, print_success_msg
2416

2517

2618
def run_stubtest(dist: Path, *, verbose: bool = False, specified_stubs_only: bool = False) -> bool:
@@ -44,25 +36,10 @@ def run_stubtest(dist: Path, *, verbose: bool = False, specified_stubs_only: boo
4436
with tempfile.TemporaryDirectory() as tmp:
4537
venv_dir = Path(tmp)
4638
try:
47-
venv.create(venv_dir, with_pip=True, clear=True)
48-
except subprocess.CalledProcessError as e:
49-
if "ensurepip" in e.cmd:
50-
print_error("fail")
51-
print_error(
52-
"stubtest requires a Python installation with ensurepip. "
53-
"If on Linux, you may need to install the python3-venv package."
54-
)
39+
pip_exe, python_exe = make_venv(venv_dir)
40+
except Exception:
41+
print_error("fail")
5542
raise
56-
57-
if sys.platform == "win32":
58-
pip = venv_dir / "Scripts" / "pip.exe"
59-
python = venv_dir / "Scripts" / "python.exe"
60-
else:
61-
pip = venv_dir / "bin" / "pip"
62-
python = venv_dir / "bin" / "python"
63-
64-
pip_exe, python_exe = str(pip), str(python)
65-
6643
dist_version = metadata["version"]
6744
extras = stubtest_meta.get("extras", [])
6845
assert isinstance(dist_version, str)

0 commit comments

Comments
 (0)