Skip to content

Commit 1605f07

Browse files
committed
mypy: misc.py, test_misc.py
1 parent 4f3ccf2 commit 1605f07

File tree

5 files changed

+68
-55
lines changed

5 files changed

+68
-55
lines changed

coverage/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ def post_process(self) -> None:
520520

521521
def debug_info(self) -> Iterable[Tuple[str, Any]]:
522522
"""Make a list of (name, value) pairs for writing debug info."""
523-
return human_sorted_items( # type: ignore
523+
return human_sorted_items(
524524
(k, v) for k, v in self.__dict__.items() if not k.startswith("_")
525525
)
526526

coverage/files.py

+5-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
"""File wrangling."""
55

6+
from __future__ import annotations
7+
68
import hashlib
79
import ntpath
810
import os
@@ -11,7 +13,7 @@
1113
import re
1214
import sys
1315

14-
from typing import Callable, Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING
16+
from typing import Callable, Dict, Iterable, List, Optional, Tuple
1517

1618
from coverage import env
1719
from coverage.exceptions import ConfigError
@@ -20,11 +22,6 @@
2022

2123
os = isolate_module(os)
2224

23-
if TYPE_CHECKING:
24-
Regex = re.Pattern[str]
25-
else:
26-
Regex = re.Pattern # Python <3.9 can't subscript Pattern
27-
2825

2926
RELATIVE_DIR: str = ""
3027
CANONICAL_FILENAME_CACHE: Dict[str, str] = {}
@@ -355,7 +352,7 @@ def globs_to_regex(
355352
patterns: Iterable[str],
356353
case_insensitive: bool=False,
357354
partial: bool=False
358-
) -> Regex:
355+
) -> re.Pattern[str]:
359356
"""Convert glob patterns to a compiled regex that matches any of them.
360357
361358
Slashes are always converted to match either slash or backslash, for
@@ -399,7 +396,7 @@ def __init__(
399396
relative: bool=False,
400397
) -> None:
401398
# A list of (original_pattern, regex, result)
402-
self.aliases: List[Tuple[str, Regex, str]] = []
399+
self.aliases: List[Tuple[str, re.Pattern[str], str]] = []
403400
self.debugfn = debugfn or (lambda msg: 0)
404401
self.relative = relative
405402
self.pprinted = False

coverage/misc.py

+46-30
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
"""Miscellaneous stuff for coverage.py."""
55

6+
from __future__ import annotations
7+
68
import contextlib
9+
import datetime
710
import errno
811
import hashlib
912
import importlib
@@ -16,20 +19,25 @@
1619
import sys
1720
import types
1821

19-
from typing import Iterable
22+
from types import ModuleType
23+
from typing import (
24+
Any, Callable, Dict, Generator, IO, Iterable, List, Mapping, Optional,
25+
Tuple, TypeVar, Union,
26+
)
2027

2128
from coverage import env
2229
from coverage.exceptions import CoverageException
30+
from coverage.types import TArc
2331

2432
# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of
2533
# other packages were importing the exceptions from misc, so import them here.
2634
# pylint: disable=unused-wildcard-import
2735
from coverage.exceptions import * # pylint: disable=wildcard-import
2836

29-
ISOLATED_MODULES = {}
37+
ISOLATED_MODULES: Dict[ModuleType, ModuleType] = {}
3038

3139

32-
def isolate_module(mod):
40+
def isolate_module(mod: ModuleType) -> ModuleType:
3341
"""Copy a module so that we are isolated from aggressive mocking.
3442
3543
If a test suite mocks os.path.exists (for example), and then we need to use
@@ -52,18 +60,18 @@ def isolate_module(mod):
5260

5361
class SysModuleSaver:
5462
"""Saves the contents of sys.modules, and removes new modules later."""
55-
def __init__(self):
63+
def __init__(self) -> None:
5664
self.old_modules = set(sys.modules)
5765

58-
def restore(self):
66+
def restore(self) -> None:
5967
"""Remove any modules imported since this object started."""
6068
new_modules = set(sys.modules) - self.old_modules
6169
for m in new_modules:
6270
del sys.modules[m]
6371

6472

6573
@contextlib.contextmanager
66-
def sys_modules_saved():
74+
def sys_modules_saved() -> Generator[None, None, None]:
6775
"""A context manager to remove any modules imported during a block."""
6876
saver = SysModuleSaver()
6977
try:
@@ -72,7 +80,7 @@ def sys_modules_saved():
7280
saver.restore()
7381

7482

75-
def import_third_party(modname):
83+
def import_third_party(modname: str) -> Tuple[ModuleType, bool]:
7684
"""Import a third-party module we need, but might not be installed.
7785
7886
This also cleans out the module after the import, so that coverage won't
@@ -95,7 +103,7 @@ def import_third_party(modname):
95103
return sys, False
96104

97105

98-
def nice_pair(pair):
106+
def nice_pair(pair: TArc) -> str:
99107
"""Make a nice string representation of a pair of numbers.
100108
101109
If the numbers are equal, just return the number, otherwise return the pair
@@ -109,7 +117,10 @@ def nice_pair(pair):
109117
return "%d-%d" % (start, end)
110118

111119

112-
def expensive(fn):
120+
TSelf = TypeVar("TSelf")
121+
TRetVal = TypeVar("TRetVal")
122+
123+
def expensive(fn: Callable[[TSelf], TRetVal]) -> Callable[[TSelf], TRetVal]:
113124
"""A decorator to indicate that a method shouldn't be called more than once.
114125
115126
Normally, this does nothing. During testing, this raises an exception if
@@ -119,7 +130,7 @@ def expensive(fn):
119130
if env.TESTING:
120131
attr = "_once_" + fn.__name__
121132

122-
def _wrapper(self):
133+
def _wrapper(self: TSelf) -> TRetVal:
123134
if hasattr(self, attr):
124135
raise AssertionError(f"Shouldn't have called {fn.__name__} more than once")
125136
setattr(self, attr, True)
@@ -129,7 +140,7 @@ def _wrapper(self):
129140
return fn # pragma: not testing
130141

131142

132-
def bool_or_none(b):
143+
def bool_or_none(b: Any) -> Optional[bool]:
133144
"""Return bool(b), but preserve None."""
134145
if b is None:
135146
return None
@@ -146,7 +157,7 @@ def join_regex(regexes: Iterable[str]) -> str:
146157
return "|".join(f"(?:{r})" for r in regexes)
147158

148159

149-
def file_be_gone(path):
160+
def file_be_gone(path: str) -> None:
150161
"""Remove a file, and don't get annoyed if it doesn't exist."""
151162
try:
152163
os.remove(path)
@@ -155,7 +166,7 @@ def file_be_gone(path):
155166
raise
156167

157168

158-
def ensure_dir(directory):
169+
def ensure_dir(directory: str) -> None:
159170
"""Make sure the directory exists.
160171
161172
If `directory` is None or empty, do nothing.
@@ -164,12 +175,12 @@ def ensure_dir(directory):
164175
os.makedirs(directory, exist_ok=True)
165176

166177

167-
def ensure_dir_for_file(path):
178+
def ensure_dir_for_file(path: str) -> None:
168179
"""Make sure the directory for the path exists."""
169180
ensure_dir(os.path.dirname(path))
170181

171182

172-
def output_encoding(outfile=None):
183+
def output_encoding(outfile: Optional[IO[str]]=None) -> str:
173184
"""Determine the encoding to use for output written to `outfile` or stdout."""
174185
if outfile is None:
175186
outfile = sys.stdout
@@ -183,10 +194,10 @@ def output_encoding(outfile=None):
183194

184195
class Hasher:
185196
"""Hashes Python data for fingerprinting."""
186-
def __init__(self):
197+
def __init__(self) -> None:
187198
self.hash = hashlib.new("sha3_256")
188199

189-
def update(self, v):
200+
def update(self, v: Any) -> None:
190201
"""Add `v` to the hash, recursively if needed."""
191202
self.hash.update(str(type(v)).encode("utf-8"))
192203
if isinstance(v, str):
@@ -216,12 +227,12 @@ def update(self, v):
216227
self.update(a)
217228
self.hash.update(b'.')
218229

219-
def hexdigest(self):
230+
def hexdigest(self) -> str:
220231
"""Retrieve the hex digest of the hash."""
221232
return self.hash.hexdigest()[:32]
222233

223234

224-
def _needs_to_implement(that, func_name):
235+
def _needs_to_implement(that: Any, func_name: str) -> None:
225236
"""Helper to raise NotImplementedError in interface stubs."""
226237
if hasattr(that, "_coverage_plugin_name"):
227238
thing = "Plugin"
@@ -243,14 +254,14 @@ class DefaultValue:
243254
and Sphinx output.
244255
245256
"""
246-
def __init__(self, display_as):
257+
def __init__(self, display_as: str) -> None:
247258
self.display_as = display_as
248259

249-
def __repr__(self):
260+
def __repr__(self) -> str:
250261
return self.display_as
251262

252263

253-
def substitute_variables(text, variables):
264+
def substitute_variables(text: str, variables: Mapping[str, str]) -> str:
254265
"""Substitute ``${VAR}`` variables in `text` with their values.
255266
256267
Variables in the text can take a number of shell-inspired forms::
@@ -283,7 +294,7 @@ def substitute_variables(text, variables):
283294

284295
dollar_groups = ('dollar', 'word1', 'word2')
285296

286-
def dollar_replace(match):
297+
def dollar_replace(match: re.Match[str]) -> str:
287298
"""Called for each $replacement."""
288299
# Only one of the dollar_groups will have matched, just get its text.
289300
word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks
@@ -301,13 +312,13 @@ def dollar_replace(match):
301312
return text
302313

303314

304-
def format_local_datetime(dt):
315+
def format_local_datetime(dt: datetime.datetime) -> str:
305316
"""Return a string with local timezone representing the date.
306317
"""
307318
return dt.astimezone().strftime('%Y-%m-%d %H:%M %z')
308319

309320

310-
def import_local_file(modname, modfile=None):
321+
def import_local_file(modname: str, modfile: Optional[str]=None) -> ModuleType:
311322
"""Import a local file as a module.
312323
313324
Opens a file in the current directory named `modname`.py, imports it
@@ -318,18 +329,20 @@ def import_local_file(modname, modfile=None):
318329
if modfile is None:
319330
modfile = modname + '.py'
320331
spec = importlib.util.spec_from_file_location(modname, modfile)
332+
assert spec is not None
321333
mod = importlib.util.module_from_spec(spec)
322334
sys.modules[modname] = mod
335+
assert spec.loader is not None
323336
spec.loader.exec_module(mod)
324337

325338
return mod
326339

327340

328-
def _human_key(s):
341+
def _human_key(s: str) -> List[Union[str, int]]:
329342
"""Turn a string into a list of string and number chunks.
330343
"z23a" -> ["z", 23, "a"]
331344
"""
332-
def tryint(s):
345+
def tryint(s: str) -> Union[str, int]:
333346
"""If `s` is a number, return an int, else `s` unchanged."""
334347
try:
335348
return int(s)
@@ -338,7 +351,7 @@ def tryint(s):
338351

339352
return [tryint(c) for c in re.split(r"(\d+)", s)]
340353

341-
def human_sorted(strings):
354+
def human_sorted(strings: Iterable[str]) -> List[str]:
342355
"""Sort the given iterable of strings the way that humans expect.
343356
344357
Numeric components in the strings are sorted as numbers.
@@ -348,7 +361,10 @@ def human_sorted(strings):
348361
"""
349362
return sorted(strings, key=_human_key)
350363

351-
def human_sorted_items(items, reverse=False):
364+
def human_sorted_items(
365+
items: Iterable[Tuple[str, Any]],
366+
reverse: bool=False,
367+
) -> List[Tuple[str, Any]]:
352368
"""Sort (string, ...) items the way humans expect.
353369
354370
The elements of `items` can be any tuple/list. They'll be sorted by the
@@ -359,7 +375,7 @@ def human_sorted_items(items, reverse=False):
359375
return sorted(items, key=lambda item: (_human_key(item[0]), *item[1:]), reverse=reverse)
360376

361377

362-
def plural(n, thing="", things=""):
378+
def plural(n: int, thing: str="", things: str="") -> str:
363379
"""Pluralize a word.
364380
365381
If n is 1, return thing. Otherwise return things, or thing+s.

0 commit comments

Comments
 (0)