Skip to content

Commit 641c686

Browse files
committed
regr_test.py: Allow non-types dependencies
1 parent 805452f commit 641c686

File tree

4 files changed

+152
-92
lines changed

4 files changed

+152
-92
lines changed

tests/mypy_test.py

Lines changed: 1 addition & 1 deletion
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

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
PackageInfo,
1919
colored,
2020
get_all_testcase_directories,
21+
get_mypy_req,
2122
get_recursive_requirements,
23+
make_venv,
2224
print_error,
2325
print_success_msg,
2426
testcase_dir_from_package_name,
@@ -85,14 +87,66 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
8587
)
8688

8789

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"
90+
def run_testcases(
91+
package: PackageInfo, flags: list[str], tmpdir_path: Path, python_minor_version: int
92+
) -> tuple[Path, subprocess.CompletedProcess[str]]:
93+
python_exe = sys.executable
94+
new_test_case_dir = tmpdir_path / "test_cases"
95+
shutil.copytree(package.test_case_directory, new_test_case_dir)
96+
env_vars = dict(os.environ)
97+
if package.is_stdlib:
98+
flags.extend(["--no-site-packages", "--custom-typeshed-dir", str(Path(__file__).parent.parent)])
99+
else:
100+
# HACK: we want to run these test cases in an isolated environment --
101+
# we want mypy to see all stub packages listed in the "requires" field of METADATA.toml
102+
# (and all stub packages required by those stub packages, etc. etc.),
103+
# but none of the other stubs in typeshed.
104+
#
105+
# The best way of doing that without stopping --warn-unused-ignore from working
106+
# seems to be to create a "new typeshed" directory in a tempdir
107+
# that has only the required stubs copied over.
108+
new_typeshed = tmpdir_path / "typeshed"
109+
new_typeshed.mkdir()
110+
shutil.copytree(Path("stdlib"), new_typeshed / "stdlib")
111+
requirements = get_recursive_requirements(package.name)
112+
# mypy refuses to consider a directory a "valid typeshed directory"
113+
# unless there's a stubs/mypy-extensions path inside it,
114+
# so add that to the list of stubs to copy over to the new directory
115+
for requirement in set(requirements.typeshed_pkgs) | {package.name, "mypy-extensions"}:
116+
shutil.copytree(Path("stubs", requirement), new_typeshed / "stubs" / requirement)
117+
118+
if requirements.external_pkgs:
119+
pip_exe, python_exe = make_venv(tmpdir_path / ".venv")
120+
try:
121+
subprocess.run([pip_exe, "install", get_mypy_req(), *requirements.external_pkgs], check=True, capture_output=True)
122+
except subprocess.CalledProcessError as e:
123+
print(e.stderr)
124+
raise
125+
else:
126+
flags.append("--no-site-packages")
127+
128+
env_vars["MYPYPATH"] = os.pathsep.join(map(str, new_typeshed.glob("stubs/*")))
129+
flags.extend(["--custom-typeshed-dir", str(new_typeshed)])
130+
131+
# If the test-case filename ends with -py39,
132+
# only run the test if --python-version was set to 3.9 or higher (for example)
133+
for path in new_test_case_dir.rglob("*.py"):
134+
if match := re.fullmatch(r".*-py3(\d{1,2})", path.stem):
135+
minor_version_required = int(match[1])
136+
assert f"3.{minor_version_required}" in SUPPORTED_VERSIONS
137+
if minor_version_required <= python_minor_version:
138+
flags.append(str(path))
139+
else:
140+
flags.append(str(path))
141+
142+
return new_test_case_dir, subprocess.run([python_exe, "-m", "mypy", *flags], capture_output=True, text=True, env=env_vars)
91143

144+
145+
def test_testcase_directory(package: PackageInfo, version: str, platform: str, quiet: bool) -> ReturnCode:
92146
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}..."
147+
msg += "standard library test cases..." if package.is_stdlib else f"test cases for {package.name!r}..."
94148
if not quiet:
95-
print(msg, end=" ")
149+
print(msg, end=" ", flush=True)
96150

97151
# "--enable-error-code ignore-without-code" is purposefully ommited. See https://github.com/python/typeshed/pull/8083
98152
flags = [
@@ -103,53 +157,17 @@ def test_testcase_directory(package: PackageInfo, version: str, platform: str, q
103157
"--no-error-summary",
104158
"--platform",
105159
platform,
106-
"--no-site-packages",
107160
"--strict",
108161
"--pretty",
109162
]
110163

111164
# --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.
165+
# SO, to work around this, we copy the test_cases directory into a TemporaryDirectory,
166+
# and run the test cases inside of that.
113167
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)])
120-
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)
168+
new_test_case_dir, result = run_testcases(
169+
package=package, flags=flags, tmpdir_path=Path(td), python_minor_version=int(version.split(".")[1])
170+
)
153171

154172
if result.returncode:
155173
if quiet:
@@ -158,11 +176,11 @@ def test_testcase_directory(package: PackageInfo, version: str, platform: str, q
158176
# If there are errors, the output is inscrutable if this isn't printed.
159177
print(msg, end=" ")
160178
print_error("failure\n")
161-
replacements = (str(new_test_case_dir), str(test_case_directory))
179+
replacements = (str(new_test_case_dir), str(package.test_case_directory))
162180
if result.stderr:
163-
print_error(result.stderr.decode(), fix_path=replacements)
181+
print_error(result.stderr, fix_path=replacements)
164182
if result.stdout:
165-
print_error(result.stdout.decode(), fix_path=replacements)
183+
print_error(result.stdout, fix_path=replacements)
166184
elif not quiet:
167185
print_success_msg()
168186
return result.returncode

tests/stubtest_third_party.py

Lines changed: 4 additions & 27 deletions
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)

tests/utils.py

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
import os
66
import re
7+
import subprocess
8+
import sys
9+
import venv
710
from functools import cache
8-
from itertools import filterfalse
911
from pathlib import Path
1012
from typing import NamedTuple
13+
from typing_extensions import Annotated
1114

1215
import pathspec # type: ignore[import]
1316
import tomli
17+
from packaging.requirements import Requirement
1418

1519
# Used to install system-wide packages for different OS types:
1620
METADATA_MAPPING = {"linux": "apt_dependencies", "darwin": "brew_dependencies", "win32": "choco_dependencies"}
@@ -45,25 +49,82 @@ def print_success_msg() -> None:
4549
# ====================================================================
4650

4751

52+
class PackageDependencies(NamedTuple):
53+
typeshed_pkgs: tuple[str, ...]
54+
external_pkgs: tuple[str, ...]
55+
56+
4857
@cache
49-
def read_dependencies(distribution: str) -> tuple[str, ...]:
58+
def read_dependencies(distribution: str) -> PackageDependencies:
5059
with Path("stubs", distribution, "METADATA.toml").open("rb") as f:
5160
data = tomli.load(f)
52-
requires = data.get("requires", [])
53-
assert isinstance(requires, list)
54-
dependencies = []
55-
for dependency in requires:
56-
assert isinstance(dependency, str)
57-
assert dependency.startswith("types-"), f"unrecognized dependency {dependency!r}"
58-
dependencies.append(dependency[6:].split("<")[0])
59-
return tuple(dependencies)
61+
dependencies = data.get("requires", [])
62+
assert isinstance(dependencies, list)
63+
typeshed, external = [], []
64+
for dependency in dependencies:
65+
if dependency.startswith("types-"):
66+
maybe_typeshed_dependency = Requirement(dependency).name.removeprefix("types-")
67+
if maybe_typeshed_dependency in os.listdir("stubs"):
68+
typeshed.append(maybe_typeshed_dependency)
69+
else:
70+
external.append(dependency)
71+
else:
72+
external.append(dependency)
73+
return PackageDependencies(tuple(typeshed), tuple(external))
74+
75+
76+
def get_recursive_requirements(package_name: str, seen: set[str] | None = None) -> PackageDependencies:
77+
typeshed: set[str] = set()
78+
external: set[str] = set()
79+
seen = seen if seen is not None else {package_name}
80+
non_recursive_requirements = read_dependencies(package_name)
81+
typeshed.update(non_recursive_requirements.typeshed_pkgs)
82+
external.update(non_recursive_requirements.external_pkgs)
83+
for pkg in non_recursive_requirements.typeshed_pkgs:
84+
if pkg in seen:
85+
continue
86+
reqs = get_recursive_requirements(pkg)
87+
typeshed.update(reqs.typeshed_pkgs)
88+
external.update(reqs.external_pkgs)
89+
seen.add(pkg)
90+
return PackageDependencies(tuple(sorted(typeshed)), tuple(sorted(external)))
6091

6192

62-
def get_recursive_requirements(package_name: str, seen: set[str] | None = None) -> list[str]:
63-
seen = seen if seen is not None else {package_name}
64-
for dependency in filterfalse(seen.__contains__, read_dependencies(package_name)):
65-
seen.update(get_recursive_requirements(dependency, seen))
66-
return sorted(seen | {package_name})
93+
# ====================================================================
94+
# Dynamic venv creation
95+
# ====================================================================
96+
97+
98+
class VenvInfo(NamedTuple):
99+
pip_exe: Annotated[str, "A path to the venv's pip executable"]
100+
python_exe: Annotated[str, "A path to the venv's python executable"]
101+
102+
103+
def make_venv(venv_dir: Path) -> VenvInfo:
104+
try:
105+
venv.create(venv_dir, with_pip=True, clear=True)
106+
except subprocess.CalledProcessError as e:
107+
if "ensurepip" in e.cmd:
108+
print_error(
109+
"stubtest requires a Python installation with ensurepip. "
110+
"If on Linux, you may need to install the python3-venv package."
111+
)
112+
raise
113+
114+
if sys.platform == "win32":
115+
pip = venv_dir / "Scripts" / "pip.exe"
116+
python = venv_dir / "Scripts" / "python.exe"
117+
else:
118+
pip = venv_dir / "bin" / "pip"
119+
python = venv_dir / "bin" / "python"
120+
121+
return VenvInfo(str(pip), str(python))
122+
123+
124+
@cache
125+
def get_mypy_req() -> str:
126+
with open("requirements-tests.txt", encoding="UTF-8") as f:
127+
return next(line.strip() for line in f if "mypy" in line)
67128

68129

69130
# ====================================================================
@@ -83,6 +144,10 @@ class PackageInfo(NamedTuple):
83144
name: str
84145
test_case_directory: Path
85146

147+
@property
148+
def is_stdlib(self) -> bool:
149+
return self.name == "stdlib"
150+
86151

87152
def testcase_dir_from_package_name(package_name: str) -> Path:
88153
return Path("stubs", package_name, "@tests/test_cases")

0 commit comments

Comments
 (0)