Skip to content

gh-109413: Add a custom script for running mypy on libregrtest #109464

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Lib/test/libregrtest/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ strict_optional = False
# Various internal modules that typeshed deliberately doesn't have stubs for:
[mypy-_abc.*,_opcode.*,_overlapped.*,_testcapi.*,_testinternalcapi.*,test.*]
ignore_missing_imports = True

[mypy-test.support.*]
disable_error_code = attr-defined,var-annotated,index,assignment
27 changes: 18 additions & 9 deletions Lib/test/support/__init__.py
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revealing the existence of this module to mypy necessitated fixing a few typing-related things to do with this module

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import types
import unittest
import warnings
from collections.abc import Callable

from .testresult import get_test_runner

Expand Down Expand Up @@ -183,12 +184,12 @@ def get_attribute(obj, name):
else:
return attribute

verbose = 1 # Flag set to 0 by regrtest.py
use_resources = None # Flag set to [] by regrtest.py
max_memuse = 0 # Disable bigmem tests (they will still be run with
# small sizes, to make sure they work.)
verbose = 1 # Flag set to 0 by regrtest.py
use_resources: tuple[str, ...] | None = None # Flag set to a tuple by regrtest.py
max_memuse = 0 # Disable bigmem tests (they will still be run with
# small sizes, to make sure they work.)
real_max_memuse = 0
junit_xml_list = None # list of testsuite XML elements
junit_xml_list: list | None = None # list of testsuite XML elements
failfast = False

# _original_stdout is meant to hold stdout at the time regrtest began.
Expand Down Expand Up @@ -1325,6 +1326,18 @@ def flush_std_streams():
sys.stderr.flush()


class WarningsPrinter:
def __init__(self, func: Callable[[str], None]) -> None:
self.func = func
# bpo-39983: Store the original sys.stderr at Python startup to be able to
# log warnings even if sys.stderr is captured temporarily by a test.
self.orig_stderr = sys.stderr

def __call__(self, msg: str) -> None:
return self.func(msg)


@WarningsPrinter
Comment on lines +1329 to +1340
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mypy doesn't like assigning attributes to functions, unfortunately; it much prefers the idea of a custom callable class

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is the solution you said you preferred in #109382 (comment) :-)

def print_warning(msg):
# bpo-45410: Explicitly flush stdout to keep logs in order
flush_std_streams()
Expand All @@ -1333,10 +1346,6 @@ def print_warning(msg):
print(f"Warning -- {line}", file=stream)
stream.flush()

# bpo-39983: Store the original sys.stderr at Python startup to be able to
# log warnings even if sys.stderr is captured temporarily by a test.
print_warning.orig_stderr = sys.stderr


# Flag used by saved_test_environment of test.libregrtest.save_env,
# to check if a test modified the environment. The flag should be set to False
Expand Down
58 changes: 58 additions & 0 deletions Tools/scripts/run_mypy_on_regrtest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Script to run mypy on Lib/test/libregrtest.

This script is necessary due to the fact that,
if you invoke mypy directly on anything inside the Lib/ directory,
it (amusingly) thinks that everything in the stdlib is being "shadowed"
by the modules inside `Lib/`.
"""

import argparse
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import TypeAlias

ReturnCode: TypeAlias = int


def run_mypy_on_libregrtest(root_dir: Path) -> ReturnCode:
test_dot_support_dir = root_dir / "Lib/" / "test" / "support"
# Copy `Lib/test/support/` into a tempdir and point MYPYPATH towards the tempdir,
# so that mypy can see the classes and functions defined in `Lib/test/support/`
with tempfile.TemporaryDirectory() as td:
td_path = Path(td)
(td_path / "test").mkdir()
shutil.copytree(test_dot_support_dir, td_path / "test" / "support")
mypy_command = [
"mypy",
"--config-file",
"Lib/test/libregrtest/mypy.ini",
]
result = subprocess.run(
mypy_command, cwd=root_dir, env=os.environ | {"MYPYPATH": td}
)
return result.returncode


def main() -> ReturnCode:
parser = argparse.ArgumentParser("Script to run mypy on Lib/test/regrtest/")
parser.add_argument(
"--root-dir",
"-r",
type=Path,
required=True,
help="path to the CPython repo root",
)
args = parser.parse_args()
root_dir = args.root_dir
test_dot_support_dir = root_dir / "Lib" / "test" / "support"
if not (test_dot_support_dir.exists() and test_dot_support_dir.is_dir()):
parser.error("--root-dir must point to the root of a CPython clone!")
return run_mypy_on_libregrtest(root_dir)


if __name__ == "__main__":
raise SystemExit(main())