diff --git a/lib/ts_utils/metadata.py b/lib/ts_utils/metadata.py index f851ce536519..ec30f9301425 100644 --- a/lib/ts_utils/metadata.py +++ b/lib/ts_utils/metadata.py @@ -166,6 +166,7 @@ def is_obsolete(self) -> bool: "tool", "partial_stub", "requires_python", + "mypy-tests", } ) _KNOWN_METADATA_TOOL_FIELDS: Final = { diff --git a/lib/ts_utils/mypy.py b/lib/ts_utils/mypy.py new file mode 100644 index 000000000000..7fc050b155d1 --- /dev/null +++ b/lib/ts_utils/mypy.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +from collections.abc import Generator, Iterable +from contextlib import contextmanager +from typing import Any, NamedTuple + +import tomli + +from ts_utils.metadata import metadata_path +from ts_utils.utils import NamedTemporaryFile, TemporaryFileWrapper + + +class MypyDistConf(NamedTuple): + module_name: str + values: dict[str, dict[str, Any]] + + +# The configuration section in the metadata file looks like the following, with multiple module sections possible +# [mypy-tests] +# [mypy-tests.yaml] +# module_name = "yaml" +# [mypy-tests.yaml.values] +# disallow_incomplete_defs = true +# disallow_untyped_defs = true + + +def mypy_configuration_from_distribution(distribution: str) -> list[MypyDistConf]: + with metadata_path(distribution).open("rb") as f: + data = tomli.load(f) + + # TODO: This could be added to ts_utils.metadata + mypy_tests_conf: dict[str, dict[str, Any]] = data.get("mypy-tests", {}) + if not mypy_tests_conf: + return [] + + def validate_configuration(section_name: str, mypy_section: dict[str, Any]) -> MypyDistConf: + assert isinstance(mypy_section, dict), f"{section_name} should be a section" + module_name = mypy_section.get("module_name") + + assert module_name is not None, f"{section_name} should have a module_name key" + assert isinstance(module_name, str), f"{section_name} should be a key-value pair" + + assert "values" in mypy_section, f"{section_name} should have a values section" + values: dict[str, dict[str, Any]] = mypy_section["values"] + assert isinstance(values, dict), "values should be a section" + return MypyDistConf(module_name, values.copy()) + + assert isinstance(mypy_tests_conf, dict), "mypy-tests should be a section" + return [validate_configuration(section_name, mypy_section) for section_name, mypy_section in mypy_tests_conf.items()] + + +@contextmanager +def temporary_mypy_config_file(configurations: Iterable[MypyDistConf]) -> Generator[TemporaryFileWrapper[str]]: + temp = NamedTemporaryFile("w+") + try: + for dist_conf in configurations: + temp.write(f"[mypy-{dist_conf.module_name}]\n") + for k, v in dist_conf.values.items(): + temp.write(f"{k} = {v}\n") + temp.write("[mypy]\n") + temp.flush() + yield temp + finally: + temp.close() diff --git a/lib/ts_utils/utils.py b/lib/ts_utils/utils.py index e4a687600099..fba574d7557f 100644 --- a/lib/ts_utils/utils.py +++ b/lib/ts_utils/utils.py @@ -3,16 +3,24 @@ from __future__ import annotations import functools +import os import re import sys +import tempfile from collections.abc import Iterable, Mapping from pathlib import Path -from typing import Any, Final, NamedTuple +from types import MethodType +from typing import TYPE_CHECKING, Any, Final, NamedTuple from typing_extensions import TypeAlias import pathspec from packaging.requirements import Requirement +from .paths import REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path + +if TYPE_CHECKING: + from _typeshed import OpenTextMode + try: from termcolor import colored as colored # pyright: ignore[reportAssignmentType] except ImportError: @@ -21,8 +29,6 @@ def colored(text: str, color: str | None = None, **kwargs: Any) -> str: # type: return text -from .paths import REQUIREMENTS_PATH, STDLIB_PATH, STUBS_PATH, TEST_CASES_DIR, allowlists_path, test_cases_path - PYTHON_VERSION: Final = f"{sys.version_info.major}.{sys.version_info.minor}" @@ -196,6 +202,26 @@ def allowlists(distribution_name: str) -> list[str]: return ["stubtest_allowlist.txt", platform_allowlist] +# Re-exposing as a public name to avoid many pyright reportPrivateUsage +TemporaryFileWrapper = tempfile._TemporaryFileWrapper # pyright: ignore[reportPrivateUsage] + +# We need to work around a limitation of tempfile.NamedTemporaryFile on Windows +# For details, see https://github.com/python/typeshed/pull/13620#discussion_r1990185997 +# Python 3.12 added a cross-platform solution with `tempfile.NamedTemporaryFile("w+", delete_on_close=False)` +if sys.platform != "win32": + NamedTemporaryFile = tempfile.NamedTemporaryFile # noqa: TID251 +else: + + def NamedTemporaryFile(mode: OpenTextMode) -> TemporaryFileWrapper[str]: # noqa: N802 + def close(self: TemporaryFileWrapper[str]) -> None: + TemporaryFileWrapper.close(self) # pyright: ignore[reportUnknownMemberType] + os.remove(self.name) + + temp = tempfile.NamedTemporaryFile(mode, delete=False) # noqa: SIM115, TID251 + temp.close = MethodType(close, temp) # type: ignore[method-assign] + return temp + + # ==================================================================== # Parsing .gitignore # ==================================================================== @@ -215,7 +241,7 @@ def spec_matches_path(spec: pathspec.PathSpec, path: Path) -> bool: # ==================================================================== -# mypy/stubtest call +# stubtest call # ==================================================================== diff --git a/pyproject.toml b/pyproject.toml index 5d6bd434156b..b3e1a5821102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,6 +139,8 @@ select = [ "TC005", # Found empty type-checking block # "TC008", # TODO: Enable when out of preview "TC010", # Invalid string member in `X | Y`-style union type + # Used for lint.flake8-import-conventions.aliases + "TID251", # `{name}` is banned: {message} ] extend-safe-fixes = [ "UP036", # Remove unnecessary `sys.version_info` blocks @@ -235,6 +237,9 @@ convention = "pep257" # https://docs.astral.sh/ruff/settings/#lint_pydocstyle_co typing_extensions = "typing_extensions" typing = "typing" +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"tempfile.NamedTemporaryFile".msg = "Use `ts_util.util.NamedTemporaryFile` instead." + [tool.ruff.lint.isort] split-on-trailing-comma = false combine-as-imports = true diff --git a/tests/mypy_test.py b/tests/mypy_test.py index 2eeb532d1ca6..84c8fa1467a8 100755 --- a/tests/mypy_test.py +++ b/tests/mypy_test.py @@ -5,14 +5,12 @@ import argparse import concurrent.futures -import functools import os import subprocess import sys import tempfile import time from collections import defaultdict -from collections.abc import Generator from dataclasses import dataclass from enum import Enum from itertools import product @@ -21,10 +19,10 @@ from typing import Annotated, Any, NamedTuple from typing_extensions import TypeAlias -import tomli from packaging.requirements import Requirement -from ts_utils.metadata import PackageDependencies, get_recursive_requirements, metadata_path, read_metadata +from ts_utils.metadata import PackageDependencies, get_recursive_requirements, read_metadata +from ts_utils.mypy import MypyDistConf, mypy_configuration_from_distribution, temporary_mypy_config_file from ts_utils.paths import STDLIB_PATH, STUBS_PATH, TESTS_DIR, TS_BASE_PATH, distribution_path from ts_utils.utils import ( PYTHON_VERSION, @@ -46,24 +44,6 @@ print_error("Cannot import mypy. Did you install it?") sys.exit(1) -# We need to work around a limitation of tempfile.NamedTemporaryFile on Windows -# For details, see https://github.com/python/typeshed/pull/13620#discussion_r1990185997 -# Python 3.12 added a workaround with `tempfile.NamedTemporaryFile("w+", delete_on_close=False)` -if sys.platform != "win32": - _named_temporary_file = functools.partial(tempfile.NamedTemporaryFile, "w+") -else: - from contextlib import contextmanager - - @contextmanager - def _named_temporary_file() -> Generator[tempfile._TemporaryFileWrapper[str]]: # pyright: ignore[reportPrivateUsage] - temp = tempfile.NamedTemporaryFile("w+", delete=False) # noqa: SIM115 - try: - yield temp - finally: - temp.close() - os.remove(temp.name) - - SUPPORTED_VERSIONS = ["3.13", "3.12", "3.11", "3.10", "3.9"] SUPPORTED_PLATFORMS = ("linux", "win32", "darwin") DIRECTORIES_TO_TEST = [STDLIB_PATH, STUBS_PATH] @@ -177,49 +157,20 @@ def add_files(files: list[Path], module: Path, args: TestConfig) -> None: files.extend(sorted(file for file in module.rglob("*.pyi") if match(file, args))) -class MypyDistConf(NamedTuple): - module_name: str - values: dict[str, dict[str, Any]] - - -# The configuration section in the metadata file looks like the following, with multiple module sections possible -# [mypy-tests] -# [mypy-tests.yaml] -# module_name = "yaml" -# [mypy-tests.yaml.values] -# disallow_incomplete_defs = true -# disallow_untyped_defs = true - - -def add_configuration(configurations: list[MypyDistConf], distribution: str) -> None: - with metadata_path(distribution).open("rb") as f: - data = tomli.load(f) - - # TODO: This could be added to ts_utils.metadata, but is currently unused - mypy_tests_conf: dict[str, dict[str, Any]] = data.get("mypy-tests", {}) - if not mypy_tests_conf: - return - - assert isinstance(mypy_tests_conf, dict), "mypy-tests should be a section" - for section_name, mypy_section in mypy_tests_conf.items(): - assert isinstance(mypy_section, dict), f"{section_name} should be a section" - module_name = mypy_section.get("module_name") - - assert module_name is not None, f"{section_name} should have a module_name key" - assert isinstance(module_name, str), f"{section_name} should be a key-value pair" - - assert "values" in mypy_section, f"{section_name} should have a values section" - values: dict[str, dict[str, Any]] = mypy_section["values"] - assert isinstance(values, dict), "values should be a section" - - configurations.append(MypyDistConf(module_name, values.copy())) - - class MypyResult(Enum): SUCCESS = 0 FAILURE = 1 CRASH = 2 + @staticmethod + def from_process_result(result: subprocess.CompletedProcess[Any]) -> MypyResult: + if result.returncode == 0: + return MypyResult.SUCCESS + elif result.returncode == 1: + return MypyResult.FAILURE + else: + return MypyResult.CRASH + def run_mypy( args: TestConfig, @@ -234,15 +185,7 @@ def run_mypy( env_vars = dict(os.environ) if mypypath is not None: env_vars["MYPYPATH"] = mypypath - - with _named_temporary_file() as temp: - temp.write("[mypy]\n") - for dist_conf in configurations: - temp.write(f"[mypy-{dist_conf.module_name}]\n") - for k, v in dist_conf.values.items(): - temp.write(f"{k} = {v}\n") - temp.flush() - + with temporary_mypy_config_file(configurations) as temp: flags = [ "--python-version", args.version, @@ -278,29 +221,23 @@ def run_mypy( if args.verbose: print(colored(f"running {' '.join(mypy_command)}", "blue")) result = subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars, check=False) - if result.returncode: - print_error(f"failure (exit code {result.returncode})\n") - if result.stdout: - print_error(result.stdout) - if result.stderr: - print_error(result.stderr) - if non_types_dependencies and args.verbose: - print("Ran with the following environment:") - subprocess.run(["uv", "pip", "freeze"], env={**os.environ, "VIRTUAL_ENV": str(venv_dir)}, check=False) - print() - else: - print_success_msg() - if result.returncode == 0: - return MypyResult.SUCCESS - elif result.returncode == 1: - return MypyResult.FAILURE - else: - return MypyResult.CRASH + if result.returncode: + print_error(f"failure (exit code {result.returncode})\n") + if result.stdout: + print_error(result.stdout) + if result.stderr: + print_error(result.stderr) + if non_types_dependencies and args.verbose: + print("Ran with the following environment:") + subprocess.run(["uv", "pip", "freeze"], env={**os.environ, "VIRTUAL_ENV": str(venv_dir)}, check=False) + print() + else: + print_success_msg() + + return MypyResult.from_process_result(result) -def add_third_party_files( - distribution: str, files: list[Path], args: TestConfig, configurations: list[MypyDistConf], seen_dists: set[str] -) -> None: +def add_third_party_files(distribution: str, files: list[Path], args: TestConfig, seen_dists: set[str]) -> None: typeshed_reqs = get_recursive_requirements(distribution).typeshed_pkgs if distribution in seen_dists: return @@ -311,7 +248,6 @@ def add_third_party_files( if name.startswith("."): continue add_files(files, (root / name), args) - add_configuration(configurations, distribution) class TestResult(NamedTuple): @@ -328,9 +264,9 @@ def test_third_party_distribution( and the second element is the number of checked files. """ files: list[Path] = [] - configurations: list[MypyDistConf] = [] seen_dists: set[str] = set() - add_third_party_files(distribution, files, args, configurations, seen_dists) + add_third_party_files(distribution, files, args, seen_dists) + configurations = mypy_configuration_from_distribution(distribution) if not files and args.filter: return TestResult(MypyResult.SUCCESS, 0) diff --git a/tests/regr_test.py b/tests/regr_test.py index fc4e48c55ff6..32bfb259390c 100755 --- a/tests/regr_test.py +++ b/tests/regr_test.py @@ -22,6 +22,7 @@ from typing_extensions import TypeAlias from ts_utils.metadata import get_recursive_requirements, read_metadata +from ts_utils.mypy import mypy_configuration_from_distribution, temporary_mypy_config_file from ts_utils.paths import STDLIB_PATH, TEST_CASES_DIR, TS_BASE_PATH, distribution_path from ts_utils.utils import ( PYTHON_VERSION, @@ -169,62 +170,71 @@ def run_testcases( env_vars = dict(os.environ) new_test_case_dir = tempdir / TEST_CASES_DIR - # "--enable-error-code ignore-without-code" is purposefully omitted. - # See https://github.com/python/typeshed/pull/8083 - flags = [ - "--python-version", - version, - "--show-traceback", - "--no-error-summary", - "--platform", - platform, - "--strict", - "--pretty", - # Avoid race conditions when reading the cache - # (https://github.com/python/typeshed/issues/11220) - "--no-incremental", - # Not useful for the test cases - "--disable-error-code=empty-body", - ] - if package.is_stdlib: - python_exe = sys.executable - custom_typeshed = TS_BASE_PATH - flags.append("--no-site-packages") + configurations = [] else: - custom_typeshed = tempdir / TYPESHED - env_vars["MYPYPATH"] = os.pathsep.join(map(str, custom_typeshed.glob("stubs/*"))) - has_non_types_dependencies = (tempdir / VENV_DIR).exists() - if has_non_types_dependencies: - python_exe = str(venv_python(tempdir / VENV_DIR)) - else: + configurations = mypy_configuration_from_distribution(package.name) + + with temporary_mypy_config_file(configurations) as temp: + + # "--enable-error-code ignore-without-code" is purposefully omitted. + # See https://github.com/python/typeshed/pull/8083 + flags = [ + "--python-version", + version, + "--show-traceback", + "--no-error-summary", + "--platform", + platform, + "--strict", + "--pretty", + "--config-file", + temp.name, + # Avoid race conditions when reading the cache + # (https://github.com/python/typeshed/issues/11220) + "--no-incremental", + # Not useful for the test cases + "--disable-error-code=empty-body", + ] + + if package.is_stdlib: python_exe = sys.executable + custom_typeshed = TS_BASE_PATH flags.append("--no-site-packages") - - flags.extend(["--custom-typeshed-dir", str(custom_typeshed)]) - - # If the test-case filename ends with -py39, - # only run the test if --python-version was set to 3.9 or higher (for example) - for path in new_test_case_dir.rglob("*.py"): - if match := re.fullmatch(r".*-py3(\d{1,2})", path.stem): - minor_version_required = int(match[1]) - assert f"3.{minor_version_required}" in SUPPORTED_VERSIONS - python_minor_version = int(version.split(".")[1]) - if minor_version_required > python_minor_version: - continue - flags.append(str(path)) - - mypy_command = [python_exe, "-m", "mypy", *flags] - if verbosity is Verbosity.VERBOSE: - description = f"{package.name}/{version}/{platform}" - msg = f"{description}: {mypy_command=}\n" - if "MYPYPATH" in env_vars: - msg += f"{description}: {env_vars['MYPYPATH']=}" else: - msg += f"{description}: MYPYPATH not set" - msg += "\n" - verbose_log(msg) - return subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars, check=False) + custom_typeshed = tempdir / TYPESHED + env_vars["MYPYPATH"] = os.pathsep.join(map(str, custom_typeshed.glob("stubs/*"))) + has_non_types_dependencies = (tempdir / VENV_DIR).exists() + if has_non_types_dependencies: + python_exe = str(venv_python(tempdir / VENV_DIR)) + else: + python_exe = sys.executable + flags.append("--no-site-packages") + + flags.extend(["--custom-typeshed-dir", str(custom_typeshed)]) + + # If the test-case filename ends with -py39, + # only run the test if --python-version was set to 3.9 or higher (for example) + for path in new_test_case_dir.rglob("*.py"): + if match := re.fullmatch(r".*-py3(\d{1,2})", path.stem): + minor_version_required = int(match[1]) + assert f"3.{minor_version_required}" in SUPPORTED_VERSIONS + python_minor_version = int(version.split(".")[1]) + if minor_version_required > python_minor_version: + continue + flags.append(str(path)) + + mypy_command = [python_exe, "-m", "mypy", *flags] + if verbosity is Verbosity.VERBOSE: + description = f"{package.name}/{version}/{platform}" + msg = f"{description}: {mypy_command=}\n" + if "MYPYPATH" in env_vars: + msg += f"{description}: {env_vars['MYPYPATH']=}" + else: + msg += f"{description}: MYPYPATH not set" + msg += "\n" + verbose_log(msg) + return subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars, check=False) @dataclass(frozen=True) diff --git a/tests/stubtest_third_party.py b/tests/stubtest_third_party.py index 8b8cb6265dfd..0530f6279628 100755 --- a/tests/stubtest_third_party.py +++ b/tests/stubtest_third_party.py @@ -16,6 +16,7 @@ from typing import NoReturn from ts_utils.metadata import NoSuchStubError, get_recursive_requirements, read_metadata +from ts_utils.mypy import mypy_configuration_from_distribution, temporary_mypy_config_file from ts_utils.paths import STUBS_PATH, allowlists_path, tests_path from ts_utils.utils import ( PYTHON_VERSION, @@ -95,89 +96,95 @@ def run_stubtest( print_command_failure("Failed to install", e) return False - ignore_missing_stub = ["--ignore-missing-stub"] if stubtest_settings.ignore_missing_stub else [] - packages_to_check = [d.name for d in dist.iterdir() if d.is_dir() and d.name.isidentifier()] - modules_to_check = [d.stem for d in dist.iterdir() if d.is_file() and d.suffix == ".pyi"] - stubtest_cmd = [ - python_exe, - "-m", - "mypy.stubtest", - # Use --custom-typeshed-dir in case we make linked changes to stdlib or _typeshed - "--custom-typeshed-dir", - str(dist.parent.parent), - *ignore_missing_stub, - *packages_to_check, - *modules_to_check, - *allowlist_stubtest_arguments(dist_name), - ] + mypy_configuration = mypy_configuration_from_distribution(dist_name) + with temporary_mypy_config_file(mypy_configuration) as temp: + ignore_missing_stub = ["--ignore-missing-stub"] if stubtest_settings.ignore_missing_stub else [] + packages_to_check = [d.name for d in dist.iterdir() if d.is_dir() and d.name.isidentifier()] + modules_to_check = [d.stem for d in dist.iterdir() if d.is_file() and d.suffix == ".pyi"] + stubtest_cmd = [ + python_exe, + "-m", + "mypy.stubtest", + "--mypy-config-file", + temp.name, + # Use --custom-typeshed-dir in case we make linked changes to stdlib or _typeshed + "--custom-typeshed-dir", + str(dist.parent.parent), + *ignore_missing_stub, + *packages_to_check, + *modules_to_check, + *allowlist_stubtest_arguments(dist_name), + ] + + stubs_dir = dist.parent + mypypath_items = [str(dist)] + [str(stubs_dir / pkg.name) for pkg in requirements.typeshed_pkgs] + mypypath = os.pathsep.join(mypypath_items) + # For packages that need a display, we need to pass at least $DISPLAY + # to stubtest. $DISPLAY is set by xvfb-run in CI. + # + # It seems that some other environment variables are needed too, + # because the CI fails if we pass only os.environ["DISPLAY"]. I didn't + # "bisect" to see which variables are actually needed. + stubtest_env = os.environ | {"MYPYPATH": mypypath, "MYPY_FORCE_COLOR": "1"} + + # Perform some black magic in order to run stubtest inside uWSGI + if dist_name == "uWSGI": + if not setup_uwsgi_stubtest_command(dist, venv_dir, stubtest_cmd): + return False + + if dist_name == "gdb": + if not setup_gdb_stubtest_command(venv_dir, stubtest_cmd): + return False + + try: + subprocess.run(stubtest_cmd, env=stubtest_env, check=True, capture_output=True) + except subprocess.CalledProcessError as e: + print_time(time() - t) + print_error("fail") + + print_divider() + print("Commands run:") + print_commands(pip_cmd, stubtest_cmd, mypypath) + + print_divider() + print("Command output:\n") + print_command_output(e) + + print_divider() + print("Python version: ", end="", flush=True) + ret = subprocess.run([sys.executable, "-VV"], capture_output=True, check=False) + print_command_output(ret) - stubs_dir = dist.parent - mypypath_items = [str(dist)] + [str(stubs_dir / pkg.name) for pkg in requirements.typeshed_pkgs] - mypypath = os.pathsep.join(mypypath_items) - # For packages that need a display, we need to pass at least $DISPLAY - # to stubtest. $DISPLAY is set by xvfb-run in CI. - # - # It seems that some other environment variables are needed too, - # because the CI fails if we pass only os.environ["DISPLAY"]. I didn't - # "bisect" to see which variables are actually needed. - stubtest_env = os.environ | {"MYPYPATH": mypypath, "MYPY_FORCE_COLOR": "1"} - - # Perform some black magic in order to run stubtest inside uWSGI - if dist_name == "uWSGI": - if not setup_uwsgi_stubtest_command(dist, venv_dir, stubtest_cmd): - return False + print("\nRan with the following environment:") + ret = subprocess.run([pip_exe, "freeze", "--all"], capture_output=True, check=False) + print_command_output(ret) + if keep_tmp_dir: + print("Path to virtual environment:", venv_dir, flush=True) + + print_divider() + main_allowlist_path = allowlists_path(dist_name) / "stubtest_allowlist.txt" + if main_allowlist_path.exists(): + print(f'To fix "unused allowlist" errors, remove the corresponding entries from {main_allowlist_path}') + print() + else: + print(f"Re-running stubtest with --generate-allowlist.\nAdd the following to {main_allowlist_path}:") + ret = subprocess.run( + [*stubtest_cmd, "--generate-allowlist"], env=stubtest_env, capture_output=True, check=False + ) + print_command_output(ret) + + print_divider() + print(f"Upstream repository: {metadata.upstream_repository}") + print(f"Typeshed source code: https://github.com/python/typeshed/tree/main/stubs/{dist.name}") + + print_divider() - if dist_name == "gdb": - if not setup_gdb_stubtest_command(venv_dir, stubtest_cmd): return False - - try: - subprocess.run(stubtest_cmd, env=stubtest_env, check=True, capture_output=True) - except subprocess.CalledProcessError as e: - print_time(time() - t) - print_error("fail") - - print_divider() - print("Commands run:") - print_commands(pip_cmd, stubtest_cmd, mypypath) - - print_divider() - print("Command output:\n") - print_command_output(e) - - print_divider() - print("Python version: ", end="", flush=True) - ret = subprocess.run([sys.executable, "-VV"], capture_output=True, check=False) - print_command_output(ret) - - print("\nRan with the following environment:") - ret = subprocess.run([pip_exe, "freeze", "--all"], capture_output=True, check=False) - print_command_output(ret) - if keep_tmp_dir: - print("Path to virtual environment:", venv_dir, flush=True) - - print_divider() - main_allowlist_path = allowlists_path(dist_name) / "stubtest_allowlist.txt" - if main_allowlist_path.exists(): - print(f'To fix "unused allowlist" errors, remove the corresponding entries from {main_allowlist_path}') - print() else: - print(f"Re-running stubtest with --generate-allowlist.\nAdd the following to {main_allowlist_path}:") - ret = subprocess.run([*stubtest_cmd, "--generate-allowlist"], env=stubtest_env, capture_output=True, check=False) - print_command_output(ret) - - print_divider() - print(f"Upstream repository: {metadata.upstream_repository}") - print(f"Typeshed source code: https://github.com/python/typeshed/tree/main/stubs/{dist.name}") - - print_divider() - - return False - else: - print_time(time() - t) - print_success_msg() - if keep_tmp_dir: - print_info(f"Virtual environment kept at: {venv_dir}") + print_time(time() - t) + print_success_msg() + if keep_tmp_dir: + print_info(f"Virtual environment kept at: {venv_dir}") finally: if not keep_tmp_dir: rmtree(venv_dir)