diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2022-12-31 13:28:11 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2022-12-31 13:28:11 -0500 |
commit | ee1e4150529e55cd860fc3628b820d3a2ed471de (patch) | |
tree | 2cb9db89f9c7bf3d980801cb90a9a2c0bcb49ca8 | |
parent | 3120803d5e2580188c6298281a1c6f2c671ced07 (diff) | |
download | python-coveragepy-git-ee1e4150529e55cd860fc3628b820d3a2ed471de.tar.gz |
mypy: check tests/helpers.py
-rw-r--r-- | tests/helpers.py | 96 | ||||
-rw-r--r-- | tox.ini | 2 |
2 files changed, 63 insertions, 35 deletions
diff --git a/tests/helpers.py b/tests/helpers.py index 414b66d7..1645138c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -3,6 +3,8 @@ """Helpers for coverage.py tests.""" +from __future__ import annotations + import collections import contextlib import os @@ -13,6 +15,11 @@ import subprocess import textwrap import warnings +from types import ModuleType +from typing import ( + cast, + Any, Callable, Generator, Iterable, List, Optional, Set, Tuple, Type, Union, +) from unittest import mock import pytest @@ -20,9 +27,10 @@ import pytest from coverage import env from coverage.exceptions import CoverageWarning from coverage.misc import output_encoding +from coverage.types import TLineNo -def run_command(cmd): +def run_command(cmd: str) -> Tuple[int, str]: """Run a command in a sub-process. Returns the exit status code and the combined stdout and stderr. @@ -30,8 +38,8 @@ def run_command(cmd): """ # Subprocesses are expensive, but convenient, and so may be over-used in # the test suite. Use these lines to get a list of the tests using them: - if 0: # pragma: debugging - with open("/tmp/processes.txt", "a") as proctxt: + if 0: # pragma: debugging + with open("/tmp/processes.txt", "a") as proctxt: # type: ignore[unreachable] print(os.environ.get("PYTEST_CURRENT_TEST", "unknown"), file=proctxt, flush=True) # In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of @@ -46,17 +54,22 @@ def run_command(cmd): env=sub_env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT + stderr=subprocess.STDOUT, ) output, _ = proc.communicate() status = proc.returncode # Get the output, and canonicalize it to strings with newlines. - output = output.decode(output_encoding()).replace("\r", "") - return status, output + output_str = output.decode(output_encoding()).replace("\r", "") + return status, output_str -def make_file(filename, text="", bytes=b"", newline=None): +def make_file( + filename: str, + text: str="", + bytes: bytes=b"", + newline: Optional[str]=None, +) -> str: """Create a file for testing. `filename` is the relative path to the file, including directories if @@ -91,8 +104,8 @@ def make_file(filename, text="", bytes=b"", newline=None): f.write(data) # For debugging, enable this to show the contents of files created. - if 0: # pragma: debugging - print(f" ───┬──┤ {filename} ├───────────────────────") + if 0: # pragma: debugging + print(f" ───┬──┤ {filename} ├───────────────────────") # type: ignore[unreachable] for lineno, line in enumerate(data.splitlines(), start=1): print(f"{lineno:6}│ {line.rstrip().decode()}") print() @@ -100,7 +113,7 @@ def make_file(filename, text="", bytes=b"", newline=None): return filename -def nice_file(*fparts): +def nice_file(*fparts: str) -> str: """Canonicalize the file name composed of the parts in `fparts`.""" fname = os.path.join(*fparts) return os.path.normcase(os.path.abspath(os.path.realpath(fname))) @@ -113,12 +126,13 @@ def os_sep(s: str) -> str: class CheckUniqueFilenames: """Asserts the uniqueness of file names passed to a function.""" - def __init__(self, wrapped): - self.filenames = set() + + def __init__(self, wrapped: Callable[..., Any]) -> None: + self.filenames: Set[str] = set() self.wrapped = wrapped @classmethod - def hook(cls, obj, method_name): + def hook(cls, obj: Any, method_name: str) -> CheckUniqueFilenames: """Replace a method with our checking wrapper. The method must take a string as a first argument. That argument @@ -133,17 +147,16 @@ class CheckUniqueFilenames: setattr(obj, method_name, hook.wrapper) return hook - def wrapper(self, filename, *args, **kwargs): + def wrapper(self, filename: str, *args: Any, **kwargs: Any) -> Any: """The replacement method. Check that we don't have dupes.""" assert filename not in self.filenames, ( f"File name {filename!r} passed to {self.wrapped!r} twice" ) self.filenames.add(filename) - ret = self.wrapped(filename, *args, **kwargs) - return ret + return self.wrapped(filename, *args, **kwargs) -def re_lines(pat, text, match=True): +def re_lines(pat: str, text: str, match: bool=True) -> List[str]: """Return a list of lines selected by `pat` in the string `text`. If `match` is false, the selection is inverted: only the non-matching @@ -156,12 +169,12 @@ def re_lines(pat, text, match=True): return [l for l in text.splitlines() if bool(re.search(pat, l)) == match] -def re_lines_text(pat, text, match=True): +def re_lines_text(pat: str, text: str, match: bool=True) -> str: """Return the multi-line text of lines selected by `pat`.""" return "".join(l + "\n" for l in re_lines(pat, text, match=match)) -def re_line(pat, text): +def re_line(pat: str, text: str) -> str: """Return the one line in `text` that matches regex `pat`. Raises an AssertionError if more than one, or less than one, line matches. @@ -172,7 +185,7 @@ def re_line(pat, text): return lines[0] -def remove_tree(dirname): +def remove_tree(dirname: str) -> None: """Remove a directory tree. It's fine for the directory to not exist in the first place. @@ -186,7 +199,8 @@ _arcz_map = {'.': -1} _arcz_map.update({c: ord(c) - ord('0') for c in '123456789'}) _arcz_map.update({c: 10 + ord(c) - ord('A') for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'}) -def arcz_to_arcs(arcz): + +def arcz_to_arcs(arcz: str) -> List[Tuple[TLineNo, TLineNo]]: """Convert a compact textual representation of arcs to a list of pairs. The text has space-separated pairs of letters. Period is -1, 1-9 are @@ -200,19 +214,23 @@ def arcz_to_arcs(arcz): "-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)] """ + # The `type: ignore[misc]` here are to suppress "Unpacking a string is + # disallowed". + a: str + b: str arcs = [] for pair in arcz.split(): asgn = bsgn = 1 if len(pair) == 2: - a, b = pair + a, b = pair # type: ignore[misc] else: assert len(pair) == 3 - if pair[0] == '-': - _, a, b = pair + if pair[0] == "-": + _, a, b = pair # type: ignore[misc] asgn = -1 else: - assert pair[1] == '-' - a, _, b = pair + assert pair[1] == "-" + a, _, b = pair # type: ignore[misc] bsgn = -1 arcs.append((asgn * _arcz_map[a], bsgn * _arcz_map[b])) return sorted(arcs) @@ -220,7 +238,8 @@ def arcz_to_arcs(arcz): _arcz_unmap = {val: ch for ch, val in _arcz_map.items()} -def _arcs_to_arcz_repr_one(num): + +def _arcs_to_arcz_repr_one(num: TLineNo) -> str: """Return an arcz form of the number `num`, or "?" if there is none.""" if num == -1: return "." @@ -232,7 +251,7 @@ def _arcs_to_arcz_repr_one(num): return z -def arcs_to_arcz_repr(arcs): +def arcs_to_arcz_repr(arcs: Iterable[Tuple[TLineNo, TLineNo]]) -> str: """Convert a list of arcs to a readable multi-line form for asserting. Each pair is on its own line, with a comment showing the arcz form, @@ -250,7 +269,7 @@ def arcs_to_arcz_repr(arcs): @contextlib.contextmanager -def change_dir(new_dir): +def change_dir(new_dir: str) -> Generator[None, None, None]: """Change directory, and then change back. Use as a context manager, it will return to the original @@ -265,7 +284,7 @@ def change_dir(new_dir): os.chdir(old_dir) -def without_module(using_module, missing_module_name): +def without_module(using_module: ModuleType, missing_module_name: str) -> mock._patch[Any]: """ Hide a module for testing. @@ -283,7 +302,7 @@ def without_module(using_module, missing_module_name): return mock.patch.object(using_module, missing_module_name, None) -def assert_count_equal(a, b): +def assert_count_equal(a: Iterable[Union[int, str]], b: Iterable[Union[int, str]]) -> None: """ A pytest-friendly implementation of assertCountEqual. @@ -293,14 +312,20 @@ def assert_count_equal(a, b): assert collections.Counter(list(a)) == collections.Counter(list(b)) -def assert_coverage_warnings(warns, *msgs): +def assert_coverage_warnings( + warns: Iterable[warnings.WarningMessage], + *msgs: Union[str, re.Pattern[str]], +) -> None: """ Assert that the CoverageWarning's in `warns` have `msgs` as messages. + + Each msg can be a string compared for equality, or a compiled regex used to + search the text. """ assert msgs # don't call this without some messages. warns = [w for w in warns if issubclass(w.category, CoverageWarning)] assert len(warns) == len(msgs) - for actual, expected in zip((w.message.args[0] for w in warns), msgs): + for actual, expected in zip((cast(Warning, w.message).args[0] for w in warns), msgs): if hasattr(expected, "search"): assert expected.search(actual), f"{actual!r} didn't match {expected!r}" else: @@ -308,7 +333,10 @@ def assert_coverage_warnings(warns, *msgs): @contextlib.contextmanager -def swallow_warnings(message=r".", category=CoverageWarning): +def swallow_warnings( + message: str=r".", + category: Type[Warning]=CoverageWarning, +) -> Generator[None, None, None]: """Swallow particular warnings. It's OK if they happen, or if they don't happen. Just ignore them. @@ -98,7 +98,7 @@ setenv = C_AN=coverage/config.py coverage/data.py coverage/disposition.py coverage/files.py coverage/inorout.py coverage/multiproc.py coverage/numbits.py C_OP=coverage/parser.py coverage/phystokens.py coverage/plugin.py coverage/python.py C_QZ=coverage/results.py coverage/sqldata.py coverage/tomlconfig.py coverage/types.py - T_AN=tests/test_api.py + T_AN=tests/test_api.py tests/helpers.py TYPEABLE={env:C_AN} {env:C_OP} {env:C_QZ} {env:T_AN} commands = |