Skip to content

Run mypy on Lib/test/libregrtest in CI #109382

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 13 commits into from
Closed
22 changes: 16 additions & 6 deletions .github/workflows/mypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- main
pull_request:
paths:
- "Lib/test/libregrtest/**"
- "Tools/clinic/**"
- "Tools/cases_generator/**"
- "Tools/peg_generator/**"
Expand All @@ -30,11 +31,11 @@ jobs:
mypy:
strategy:
matrix:
target: [
"Tools/cases_generator",
"Tools/clinic",
"Tools/peg_generator",
]
target:
- "Lib/test/libregrtest"
- "Tools/cases_generator"
- "Tools/clinic"
- "Tools/peg_generator"
name: Run mypy on ${{ matrix.target }}
runs-on: ubuntu-latest
timeout-minutes: 10
Expand All @@ -46,4 +47,13 @@ jobs:
cache: pip
cache-dependency-path: Tools/requirements-dev.txt
- run: pip install -r Tools/requirements-dev.txt
- run: mypy --config-file ${{ matrix.target }}/mypy.ini
- if: ${{ matrix.target != 'Lib/test/libregrtest' }}
run: mypy --config-file ${{ matrix.target }}/mypy.ini
- name: Run mypy on libregrtest
if: ${{ matrix.target == 'Lib/test/libregrtest' }}
# Mypy can't be run on libregrtest from the repo root,
# or it (amusingly) thinks that the entire Lib directory is "shadowing the stdlib",
# and refuses to do any type-checking at all
run: |
cd Lib/test
mypy --config-file libregrtest/mypy.ini
5 changes: 3 additions & 2 deletions Lib/test/libregrtest/cmdline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import shlex
import sys
from test.support import os_helper
from typing import Literal


USAGE = """\
Expand Down Expand Up @@ -157,11 +158,11 @@ def __init__(self, **kwargs) -> None:
self.randomize = False
self.fromfile = None
self.fail_env_changed = False
self.use_resources = None
self.use_resources: list[str] | None = None
self.trace = False
self.coverdir = 'coverage'
self.runleaks = False
self.huntrleaks = False
self.huntrleaks: tuple[int, int, str] | Literal[False] = False
self.rerun = False
self.verbose3 = False
self.print_slow = False
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/libregrtest/findtests.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
}


def findtestdir(path=None):
def findtestdir(path: StrPath | None = None) -> StrPath:
return path or os.path.dirname(os.path.dirname(__file__)) or os.curdir


Expand Down
6 changes: 3 additions & 3 deletions Lib/test/libregrtest/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def __init__(self, results: TestResults, quiet: bool, pgo: bool):
self.start_time = time.perf_counter()
self.test_count_text = ''
self.test_count_width = 3
self.win_load_tracker = None
self.win_load_tracker: WindowsLoadTracker | None = None
self._results: TestResults = results
self._quiet: bool = quiet
self._pgo: bool = pgo
Expand All @@ -32,9 +32,9 @@ def log(self, line: str = '') -> None:

mins, secs = divmod(int(test_time), 60)
hours, mins = divmod(mins, 60)
test_time = "%d:%02d:%02d" % (hours, mins, secs)
formatted_test_time = "%d:%02d:%02d" % (hours, mins, secs)

line = f"{test_time} {line}"
line = f"{formatted_test_time} {line}"
if empty:
line = line[:-1]

Expand Down
16 changes: 10 additions & 6 deletions Lib/test/libregrtest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import re
import sys
import time
from typing import cast

from test import support
from test.support import os_helper
Expand Down Expand Up @@ -71,11 +72,11 @@ def __init__(self, ns: Namespace):

# Select tests
if ns.match_tests:
self.match_tests: FilterTuple = tuple(ns.match_tests)
self.match_tests: FilterTuple | None = tuple(ns.match_tests)
else:
self.match_tests = None
if ns.ignore_tests:
self.ignore_tests: FilterTuple = tuple(ns.ignore_tests)
self.ignore_tests: FilterTuple | None = tuple(ns.ignore_tests)
else:
self.ignore_tests = None
self.exclude: bool = ns.exclude
Expand Down Expand Up @@ -105,16 +106,16 @@ def __init__(self, ns: Namespace):
if ns.huntrleaks:
warmups, runs, filename = ns.huntrleaks
filename = os.path.abspath(filename)
self.hunt_refleak: HuntRefleak = HuntRefleak(warmups, runs, filename)
self.hunt_refleak: HuntRefleak | None = HuntRefleak(warmups, runs, filename)
else:
self.hunt_refleak = None
self.test_dir: StrPath | None = ns.testdir
self.junit_filename: StrPath | None = ns.xmlpath
self.memory_limit: str | None = ns.memlimit
self.gc_threshold: int | None = ns.threshold
self.use_resources: tuple[str] = tuple(ns.use_resources)
self.use_resources: tuple[str, ...] = tuple(ns.use_resources or ())
if ns.python:
self.python_cmd: tuple[str] = tuple(ns.python)
self.python_cmd: tuple[str, ...] | None = tuple(ns.python)
else:
self.python_cmd = None
self.coverage: bool = ns.trace
Expand All @@ -139,6 +140,7 @@ def log(self, line=''):
self.logger.log(line)

def find_tests(self, tests: TestList | None = None) -> tuple[TestTuple, TestList | None]:
assert self.tmp_dir is not None
if self.single_test_run:
self.next_single_filename = os.path.join(self.tmp_dir, 'pynexttest')
try:
Expand Down Expand Up @@ -183,6 +185,7 @@ def find_tests(self, tests: TestList | None = None) -> tuple[TestTuple, TestList
else:
selected = alltests
else:
assert tests is not None
selected = tests

if self.single_test_run:
Expand Down Expand Up @@ -389,7 +392,7 @@ def create_run_tests(self, tests: TestTuple):
match_tests=self.match_tests,
ignore_tests=self.ignore_tests,
match_tests_dict=None,
rerun=None,
rerun=False,
forever=self.forever,
pgo=self.pgo,
pgo_extended=self.pgo_extended,
Expand Down Expand Up @@ -457,6 +460,7 @@ def _run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
self.fail_rerun)

def run_tests(self, selected: TestTuple, tests: TestList | None) -> int:
assert self.tmp_dir is not None
os.makedirs(self.tmp_dir, exist_ok=True)
work_dir = get_work_dir(self.tmp_dir)

Expand Down
43 changes: 43 additions & 0 deletions Lib/test/libregrtest/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Config file for running mypy on libregrtest.
#
# Note: mypy can't be run on libregrtest from the CPython repo root.
# If you try to do so, mypy will complain
# about the entire `Lib/` directory "shadowing the stdlib".
# Instead, `cd` into `Lib/test`, then run `mypy --config-file libregrtest/mypy.ini`.

[mypy]
packages = libregrtest
python_version = 3.11
platform = linux
pretty = True

# Enable most stricter settings
enable_error_code = ignore-without-code
strict = True

# Various stricter settings that we can't yet enable
# Try to enable these in the following order:
disallow_incomplete_defs = False
disallow_untyped_calls = False
disallow_untyped_defs = False
check_untyped_defs = False
warn_return_any = False

# Various internal modules that typeshed deliberately doesn't have stubs for:
[mypy-_abc.*]
ignore_missing_imports = True

[mypy-_opcode.*]
ignore_missing_imports = True

[mypy-_overlapped.*]
ignore_missing_imports = True

[mypy-_testcapi.*]
ignore_missing_imports = True

[mypy-_testinternalcapi.*]
ignore_missing_imports = True

[mypy-test.*]
ignore_missing_imports = True
7 changes: 5 additions & 2 deletions Lib/test/libregrtest/refleak.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
import warnings
from inspect import isabstract
from typing import Any, Callable

from test import support
from test.support import os_helper
Expand Down Expand Up @@ -45,12 +46,13 @@ def runtest_refleak(test_name, test_func,
fs = warnings.filters[:]
ps = copyreg.dispatch_table.copy()
pic = sys.path_importer_cache.copy()
zdc: dict[str, Any] | None
try:
import zipimport
except ImportError:
zdc = None # Run unmodified on platforms without zipimport support
else:
zdc = zipimport._zip_directory_cache.copy()
zdc = zipimport._zip_directory_cache.copy() # type: ignore[attr-defined]
abcs = {}
for abc in [getattr(collections.abc, a) for a in collections.abc.__all__]:
if not isabstract(abc):
Expand Down Expand Up @@ -78,7 +80,7 @@ def get_pooled_int(value):
fd_deltas = [0] * repcount
getallocatedblocks = sys.getallocatedblocks
gettotalrefcount = sys.gettotalrefcount
getunicodeinternedsize = sys.getunicodeinternedsize
getunicodeinternedsize: Callable[[], int] = sys.getunicodeinternedsize # type: ignore[attr-defined]
fd_count = os_helper.fd_count
# initialize variables to make pyflakes quiet
rc_before = alloc_before = fd_before = interned_before = 0
Expand Down Expand Up @@ -175,6 +177,7 @@ def dash_R_cleanup(fs, ps, pic, zdc, abcs):
except ImportError:
pass # Run unmodified on platforms without zipimport support
else:
assert zdc is not None
zipimport._zip_directory_cache.clear()
zipimport._zip_directory_cache.update(zdc)

Expand Down
13 changes: 6 additions & 7 deletions Lib/test/libregrtest/results.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import xml.etree.ElementTree as ET
Copy link
Member

Choose a reason for hiding this comment

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

Please don't do that. I want to minimize imports at startup, to minimize how many modules are imported when a test is run. Maybe even add a comment to explain why the import is not done at top level, that it's a deliberate choice.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay. The reason why I did this was to accurately add type hints for the TestResults.testsuite_xml attribute on line 35. The current type hints for that attribute are incorrect, and caused several mypy errors.

A possible workaround is to do the top-level import inside an if TYPE_CHECKING block.

Copy link
Member Author

Choose a reason for hiding this comment

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

A possible workaround is to do the top-level import inside an if TYPE_CHECKING block.

I made that change in 581ef0d; let me know what you think!

from test.support import TestStats

from .runtests import RunTests
Expand Down Expand Up @@ -31,7 +32,7 @@ def __init__(self):
self.test_times: list[tuple[float, TestName]] = []
self.stats = TestStats()
# used by --junit-xml
self.testsuite_xml: list[str] = []
self.testsuite_xml: list[ET.Element] = []

def get_executed(self):
return (set(self.good) | set(self.bad) | set(self.skipped)
Expand Down Expand Up @@ -98,6 +99,7 @@ def accumulate_result(self, result: TestResult, runtests: RunTests):
raise ValueError(f"invalid test state: {result.state!r}")

if result.has_meaningful_duration() and not rerun:
assert result.duration is not None
self.test_times.append((result.duration, test_name))
if result.stats is not None:
self.stats.accumulate(result.stats)
Expand All @@ -111,7 +113,7 @@ def accumulate_result(self, result: TestResult, runtests: RunTests):
def need_rerun(self):
return bool(self.bad_results)

def prepare_rerun(self) -> (TestTuple, FilterDict):
def prepare_rerun(self) -> tuple[TestTuple, FilterDict]:
tests: TestList = []
match_tests_dict = {}
for result in self.bad_results:
Expand All @@ -130,7 +132,6 @@ def prepare_rerun(self) -> (TestTuple, FilterDict):
return (tuple(tests), match_tests_dict)

def add_junit(self, xml_data: list[str]):
import xml.etree.ElementTree as ET
for e in xml_data:
try:
self.testsuite_xml.append(ET.fromstring(e))
Expand Down Expand Up @@ -231,8 +232,7 @@ def display_summary(self, first_runtests: RunTests, filtered: bool):
report.append(f'failures={stats.failures:,}')
if stats.skipped:
report.append(f'skipped={stats.skipped:,}')
report = ' '.join(report)
print(f"Total tests: {report}")
print(f"Total tests: {' '.join(report)}")

# Total test files
all_tests = [self.good, self.bad, self.rerun,
Expand All @@ -256,5 +256,4 @@ def display_summary(self, first_runtests: RunTests, filtered: bool):
):
if tests:
report.append(f'{name}={len(tests)}')
report = ' '.join(report)
print(f"Total test files: {report}")
print(f"Total test files: {' '.join(report)}")
Loading