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
44 changes: 44 additions & 0 deletions Lib/test/libregrtest/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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_any_generics = False
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
8 changes: 6 additions & 2 deletions Lib/test/libregrtest/refleak.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import sys
import warnings
from collections.abc import Callable
from inspect import isabstract
from typing import Any

from test import support
from test.support import os_helper
Expand Down Expand Up @@ -45,12 +47,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 +81,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 +178,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
21 changes: 15 additions & 6 deletions Lib/test/libregrtest/results.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import sys
from test.support import TestStats
from typing import TYPE_CHECKING

from .runtests import RunTests
from .result import State, TestResult
Expand All @@ -8,6 +9,13 @@
printlist, count, format_duration)


if TYPE_CHECKING:
# Needed to annotate `TestResults.testsuite_xml` accurately
# Delay the runtime import until later,
# so that we only import it if we actually have to
import xml.etree.ElementTree as ET


EXITCODE_BAD_TEST = 2
EXITCODE_ENV_CHANGED = 3
EXITCODE_NO_TESTS_RAN = 4
Expand All @@ -31,7 +39,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 +106,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 +120,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,6 +139,8 @@ def prepare_rerun(self) -> (TestTuple, FilterDict):
return (tuple(tests), match_tests_dict)

def add_junit(self, xml_data: list[str]):
# Local import, so that we only import this if we actually need the xml module.
# It's best to import as few things as possible when running a test.
import xml.etree.ElementTree as ET
for e in xml_data:
try:
Expand Down Expand Up @@ -231,8 +242,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 +266,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