Skip to content

Commit e948601

Browse files
committed
Significantly optimize; more misc cleanups
1 parent 42f722f commit e948601

File tree

2 files changed

+98
-73
lines changed

2 files changed

+98
-73
lines changed

tests/regr_test.py

Lines changed: 86 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from utils import (
1818
PackageInfo,
19+
VenvInfo,
1920
colored,
2021
get_all_testcase_directories,
2122
get_mypy_req,
@@ -28,6 +29,10 @@
2829

2930
ReturnCode: TypeAlias = int
3031

32+
TEST_CASES = "test_cases"
33+
VENV_DIR = ".venv"
34+
TYPESHED = "typeshed"
35+
3136
SUPPORTED_PLATFORMS = ["linux", "darwin", "win32"]
3237
SUPPORTED_VERSIONS = ["3.11", "3.10", "3.9", "3.8", "3.7"]
3338

@@ -36,7 +41,7 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
3641
"""Helper function for argument-parsing"""
3742

3843
if package_name == "stdlib":
39-
return PackageInfo("stdlib", Path("test_cases"))
44+
return PackageInfo("stdlib", Path(TEST_CASES))
4045
test_case_dir = testcase_dir_from_package_name(package_name)
4146
if test_case_dir.is_dir():
4247
if not os.listdir(test_case_dir):
@@ -87,90 +92,102 @@ def package_with_test_cases(package_name: str) -> PackageInfo:
8792
)
8893

8994

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+
def setup_testcase_dir(package: PackageInfo, tempdir: Path, new_test_case_dir: Path) -> None:
96+
# --warn-unused-ignores doesn't work for files inside typeshed.
97+
# SO, to work around this, we copy the test_cases directory into a TemporaryDirectory,
98+
# and run the test cases inside of that.
9599
shutil.copytree(package.test_case_directory, new_test_case_dir)
100+
if package.is_stdlib:
101+
return
102+
103+
# HACK: we want to run these test cases in an isolated environment --
104+
# we want mypy to see all stub packages listed in the "requires" field of METADATA.toml
105+
# (and all stub packages required by those stub packages, etc. etc.),
106+
# but none of the other stubs in typeshed.
107+
#
108+
# The best way of doing that without stopping --warn-unused-ignore from working
109+
# seems to be to create a "new typeshed" directory in a tempdir
110+
# that has only the required stubs copied over.
111+
new_typeshed = tempdir / TYPESHED
112+
new_typeshed.mkdir()
113+
shutil.copytree(Path("stdlib"), new_typeshed / "stdlib")
114+
requirements = get_recursive_requirements(package.name)
115+
# mypy refuses to consider a directory a "valid typeshed directory"
116+
# unless there's a stubs/mypy-extensions path inside it,
117+
# so add that to the list of stubs to copy over to the new directory
118+
for requirement in {package.name, *requirements.typeshed_pkgs, "mypy-extensions"}:
119+
shutil.copytree(Path("stubs", requirement), new_typeshed / "stubs" / requirement)
120+
121+
if requirements.external_pkgs:
122+
pip_exe = make_venv(tempdir / VENV_DIR).pip_exe
123+
pip_command = [pip_exe, "install", get_mypy_req(), *requirements.external_pkgs]
124+
try:
125+
subprocess.run(pip_command, check=True, capture_output=True, text=True)
126+
except subprocess.CalledProcessError as e:
127+
print(e.stderr)
128+
raise
129+
130+
131+
def run_testcases(package: PackageInfo, version: str, platform: str, *, tempdir: Path) -> subprocess.CompletedProcess[str]:
96132
env_vars = dict(os.environ)
133+
new_test_case_dir = tempdir / TEST_CASES
134+
testcasedir_already_setup = new_test_case_dir.exists() and new_test_case_dir.is_dir()
135+
136+
if not testcasedir_already_setup:
137+
setup_testcase_dir(package, tempdir=tempdir, new_test_case_dir=new_test_case_dir)
138+
139+
# "--enable-error-code ignore-without-code" is purposefully ommited. See https://github.com/python/typeshed/pull/8083
140+
flags = [
141+
"--python-version",
142+
version,
143+
"--show-traceback",
144+
"--show-error-codes",
145+
"--no-error-summary",
146+
"--platform",
147+
platform,
148+
"--strict",
149+
"--pretty",
150+
"--no-incremental",
151+
]
152+
97153
if package.is_stdlib:
98-
flags.extend(["--no-site-packages", "--custom-typeshed-dir", str(Path(__file__).parent.parent)])
154+
python_exe = sys.executable
155+
custom_typeshed = Path(__file__).parent.parent
156+
flags.append("--no-site-packages")
99157
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 {package.name, *requirements.typeshed_pkgs, "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-
pip_command = [pip_exe, "install", get_mypy_req(), *requirements.external_pkgs]
121-
try:
122-
subprocess.run(pip_command, check=True, capture_output=True, text=True)
123-
except subprocess.CalledProcessError as e:
124-
print(e.stderr)
125-
raise
158+
custom_typeshed = tempdir / TYPESHED
159+
env_vars["MYPYPATH"] = os.pathsep.join(map(str, custom_typeshed.glob("stubs/*")))
160+
has_non_types_dependencies = (tempdir / VENV_DIR).exists()
161+
if has_non_types_dependencies:
162+
python_exe = VenvInfo.of_existing_venv(tempdir / VENV_DIR).python_exe
126163
else:
164+
python_exe = sys.executable
127165
flags.append("--no-site-packages")
128166

129-
env_vars["MYPYPATH"] = os.pathsep.join(map(str, new_typeshed.glob("stubs/*")))
130-
flags.extend(["--custom-typeshed-dir", str(new_typeshed)])
167+
flags.extend(["--custom-typeshed-dir", str(custom_typeshed)])
131168

132169
# If the test-case filename ends with -py39,
133170
# only run the test if --python-version was set to 3.9 or higher (for example)
134171
for path in new_test_case_dir.rglob("*.py"):
135172
if match := re.fullmatch(r".*-py3(\d{1,2})", path.stem):
136173
minor_version_required = int(match[1])
137174
assert f"3.{minor_version_required}" in SUPPORTED_VERSIONS
138-
if minor_version_required <= python_minor_version:
139-
flags.append(str(path))
140-
else:
141-
flags.append(str(path))
175+
python_minor_version = int(version.split(".")[1])
176+
if minor_version_required > python_minor_version:
177+
continue
178+
flags.append(str(path))
142179

143180
mypy_command = [python_exe, "-m", "mypy"] + flags
144-
result = subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars)
145-
return new_test_case_dir, result
181+
return subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars)
146182

147183

148-
def test_testcase_directory(package: PackageInfo, version: str, platform: str, quiet: bool) -> ReturnCode:
184+
def test_testcase_directory(package: PackageInfo, version: str, platform: str, *, quiet: bool, tempdir: Path) -> ReturnCode:
149185
msg = f"Running mypy --platform {platform} --python-version {version} on the "
150186
msg += "standard library test cases..." if package.is_stdlib else f"test cases for {package.name!r}..."
151187
if not quiet:
152188
print(msg, end=" ", flush=True)
153189

154-
# "--enable-error-code ignore-without-code" is purposefully ommited. See https://github.com/python/typeshed/pull/8083
155-
flags = [
156-
"--python-version",
157-
version,
158-
"--show-traceback",
159-
"--show-error-codes",
160-
"--no-error-summary",
161-
"--platform",
162-
platform,
163-
"--strict",
164-
"--pretty",
165-
]
166-
167-
# --warn-unused-ignores doesn't work for files inside typeshed.
168-
# SO, to work around this, we copy the test_cases directory into a TemporaryDirectory,
169-
# and run the test cases inside of that.
170-
with tempfile.TemporaryDirectory() as td:
171-
new_test_case_dir, result = run_testcases(
172-
package=package, flags=flags, tmpdir_path=Path(td), python_minor_version=int(version.split(".")[1])
173-
)
190+
result = run_testcases(package=package, version=version, platform=platform, tempdir=tempdir)
174191

175192
if result.returncode:
176193
if quiet:
@@ -179,7 +196,7 @@ def test_testcase_directory(package: PackageInfo, version: str, platform: str, q
179196
# If there are errors, the output is inscrutable if this isn't printed.
180197
print(msg, end=" ")
181198
print_error("failure\n")
182-
replacements = (str(new_test_case_dir), str(package.test_case_directory))
199+
replacements = (str(tempdir / TEST_CASES), str(package.test_case_directory))
183200
if result.stderr:
184201
print_error(result.stderr, fix_path=replacements)
185202
if result.stdout:
@@ -204,8 +221,12 @@ def main() -> ReturnCode:
204221
versions_to_test = args.versions_to_test or [f"3.{sys.version_info[1]}"]
205222

206223
code = 0
207-
for platform, version, directory in product(platforms_to_test, versions_to_test, testcase_directories):
208-
code = max(code, test_testcase_directory(directory, version, platform, args.quiet))
224+
for testcase_dir in testcase_directories:
225+
with tempfile.TemporaryDirectory() as td:
226+
tempdir = Path(td)
227+
for platform, version in product(platforms_to_test, versions_to_test):
228+
this_code = test_testcase_directory(testcase_dir, version, platform, quiet=args.quiet, tempdir=tempdir)
229+
code = max(code, this_code)
209230
if code:
210231
print_error("\nTest completed with errors")
211232
else:

tests/utils.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ class VenvInfo(NamedTuple):
127127
pip_exe: Annotated[str, "A path to the venv's pip executable"]
128128
python_exe: Annotated[str, "A path to the venv's python executable"]
129129

130+
@staticmethod
131+
def of_existing_venv(venv_dir: Path) -> VenvInfo:
132+
if sys.platform == "win32":
133+
pip = venv_dir / "Scripts" / "pip.exe"
134+
python = venv_dir / "Scripts" / "python.exe"
135+
else:
136+
pip = venv_dir / "bin" / "pip"
137+
python = venv_dir / "bin" / "python"
138+
139+
return VenvInfo(str(pip), str(python))
140+
130141

131142
def make_venv(venv_dir: Path) -> VenvInfo:
132143
try:
@@ -139,14 +150,7 @@ def make_venv(venv_dir: Path) -> VenvInfo:
139150
)
140151
raise
141152

142-
if sys.platform == "win32":
143-
pip = venv_dir / "Scripts" / "pip.exe"
144-
python = venv_dir / "Scripts" / "python.exe"
145-
else:
146-
pip = venv_dir / "bin" / "pip"
147-
python = venv_dir / "bin" / "python"
148-
149-
return VenvInfo(str(pip), str(python))
153+
return VenvInfo.of_existing_venv(venv_dir)
150154

151155

152156
@cache

0 commit comments

Comments
 (0)