3
3
4
4
"""Miscellaneous stuff for coverage.py."""
5
5
6
+ from __future__ import annotations
7
+
6
8
import contextlib
9
+ import datetime
7
10
import errno
8
11
import hashlib
9
12
import importlib
16
19
import sys
17
20
import types
18
21
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
+ )
20
27
21
28
from coverage import env
22
29
from coverage .exceptions import CoverageException
30
+ from coverage .types import TArc
23
31
24
32
# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of
25
33
# other packages were importing the exceptions from misc, so import them here.
26
34
# pylint: disable=unused-wildcard-import
27
35
from coverage .exceptions import * # pylint: disable=wildcard-import
28
36
29
- ISOLATED_MODULES = {}
37
+ ISOLATED_MODULES : Dict [ ModuleType , ModuleType ] = {}
30
38
31
39
32
- def isolate_module (mod ) :
40
+ def isolate_module (mod : ModuleType ) -> ModuleType :
33
41
"""Copy a module so that we are isolated from aggressive mocking.
34
42
35
43
If a test suite mocks os.path.exists (for example), and then we need to use
@@ -52,18 +60,18 @@ def isolate_module(mod):
52
60
53
61
class SysModuleSaver :
54
62
"""Saves the contents of sys.modules, and removes new modules later."""
55
- def __init__ (self ):
63
+ def __init__ (self ) -> None :
56
64
self .old_modules = set (sys .modules )
57
65
58
- def restore (self ):
66
+ def restore (self ) -> None :
59
67
"""Remove any modules imported since this object started."""
60
68
new_modules = set (sys .modules ) - self .old_modules
61
69
for m in new_modules :
62
70
del sys .modules [m ]
63
71
64
72
65
73
@contextlib .contextmanager
66
- def sys_modules_saved ():
74
+ def sys_modules_saved () -> Generator [ None , None , None ] :
67
75
"""A context manager to remove any modules imported during a block."""
68
76
saver = SysModuleSaver ()
69
77
try :
@@ -72,7 +80,7 @@ def sys_modules_saved():
72
80
saver .restore ()
73
81
74
82
75
- def import_third_party (modname ) :
83
+ def import_third_party (modname : str ) -> Tuple [ ModuleType , bool ] :
76
84
"""Import a third-party module we need, but might not be installed.
77
85
78
86
This also cleans out the module after the import, so that coverage won't
@@ -95,7 +103,7 @@ def import_third_party(modname):
95
103
return sys , False
96
104
97
105
98
- def nice_pair (pair ) :
106
+ def nice_pair (pair : TArc ) -> str :
99
107
"""Make a nice string representation of a pair of numbers.
100
108
101
109
If the numbers are equal, just return the number, otherwise return the pair
@@ -109,7 +117,10 @@ def nice_pair(pair):
109
117
return "%d-%d" % (start , end )
110
118
111
119
112
- def expensive (fn ):
120
+ TSelf = TypeVar ("TSelf" )
121
+ TRetVal = TypeVar ("TRetVal" )
122
+
123
+ def expensive (fn : Callable [[TSelf ], TRetVal ]) -> Callable [[TSelf ], TRetVal ]:
113
124
"""A decorator to indicate that a method shouldn't be called more than once.
114
125
115
126
Normally, this does nothing. During testing, this raises an exception if
@@ -119,7 +130,7 @@ def expensive(fn):
119
130
if env .TESTING :
120
131
attr = "_once_" + fn .__name__
121
132
122
- def _wrapper (self ) :
133
+ def _wrapper (self : TSelf ) -> TRetVal :
123
134
if hasattr (self , attr ):
124
135
raise AssertionError (f"Shouldn't have called { fn .__name__ } more than once" )
125
136
setattr (self , attr , True )
@@ -129,7 +140,7 @@ def _wrapper(self):
129
140
return fn # pragma: not testing
130
141
131
142
132
- def bool_or_none (b ) :
143
+ def bool_or_none (b : Any ) -> Optional [ bool ] :
133
144
"""Return bool(b), but preserve None."""
134
145
if b is None :
135
146
return None
@@ -146,7 +157,7 @@ def join_regex(regexes: Iterable[str]) -> str:
146
157
return "|" .join (f"(?:{ r } )" for r in regexes )
147
158
148
159
149
- def file_be_gone (path ) :
160
+ def file_be_gone (path : str ) -> None :
150
161
"""Remove a file, and don't get annoyed if it doesn't exist."""
151
162
try :
152
163
os .remove (path )
@@ -155,7 +166,7 @@ def file_be_gone(path):
155
166
raise
156
167
157
168
158
- def ensure_dir (directory ) :
169
+ def ensure_dir (directory : str ) -> None :
159
170
"""Make sure the directory exists.
160
171
161
172
If `directory` is None or empty, do nothing.
@@ -164,12 +175,12 @@ def ensure_dir(directory):
164
175
os .makedirs (directory , exist_ok = True )
165
176
166
177
167
- def ensure_dir_for_file (path ) :
178
+ def ensure_dir_for_file (path : str ) -> None :
168
179
"""Make sure the directory for the path exists."""
169
180
ensure_dir (os .path .dirname (path ))
170
181
171
182
172
- def output_encoding (outfile = None ):
183
+ def output_encoding (outfile : Optional [ IO [ str ]] = None ) -> str :
173
184
"""Determine the encoding to use for output written to `outfile` or stdout."""
174
185
if outfile is None :
175
186
outfile = sys .stdout
@@ -183,10 +194,10 @@ def output_encoding(outfile=None):
183
194
184
195
class Hasher :
185
196
"""Hashes Python data for fingerprinting."""
186
- def __init__ (self ):
197
+ def __init__ (self ) -> None :
187
198
self .hash = hashlib .new ("sha3_256" )
188
199
189
- def update (self , v ) :
200
+ def update (self , v : Any ) -> None :
190
201
"""Add `v` to the hash, recursively if needed."""
191
202
self .hash .update (str (type (v )).encode ("utf-8" ))
192
203
if isinstance (v , str ):
@@ -216,12 +227,12 @@ def update(self, v):
216
227
self .update (a )
217
228
self .hash .update (b'.' )
218
229
219
- def hexdigest (self ):
230
+ def hexdigest (self ) -> str :
220
231
"""Retrieve the hex digest of the hash."""
221
232
return self .hash .hexdigest ()[:32 ]
222
233
223
234
224
- def _needs_to_implement (that , func_name ) :
235
+ def _needs_to_implement (that : Any , func_name : str ) -> None :
225
236
"""Helper to raise NotImplementedError in interface stubs."""
226
237
if hasattr (that , "_coverage_plugin_name" ):
227
238
thing = "Plugin"
@@ -243,14 +254,14 @@ class DefaultValue:
243
254
and Sphinx output.
244
255
245
256
"""
246
- def __init__ (self , display_as ) :
257
+ def __init__ (self , display_as : str ) -> None :
247
258
self .display_as = display_as
248
259
249
- def __repr__ (self ):
260
+ def __repr__ (self ) -> str :
250
261
return self .display_as
251
262
252
263
253
- def substitute_variables (text , variables ) :
264
+ def substitute_variables (text : str , variables : Mapping [ str , str ]) -> str :
254
265
"""Substitute ``${VAR}`` variables in `text` with their values.
255
266
256
267
Variables in the text can take a number of shell-inspired forms::
@@ -283,7 +294,7 @@ def substitute_variables(text, variables):
283
294
284
295
dollar_groups = ('dollar' , 'word1' , 'word2' )
285
296
286
- def dollar_replace (match ) :
297
+ def dollar_replace (match : re . Match [ str ]) -> str :
287
298
"""Called for each $replacement."""
288
299
# Only one of the dollar_groups will have matched, just get its text.
289
300
word = next (g for g in match .group (* dollar_groups ) if g ) # pragma: always breaks
@@ -301,13 +312,13 @@ def dollar_replace(match):
301
312
return text
302
313
303
314
304
- def format_local_datetime (dt ) :
315
+ def format_local_datetime (dt : datetime . datetime ) -> str :
305
316
"""Return a string with local timezone representing the date.
306
317
"""
307
318
return dt .astimezone ().strftime ('%Y-%m-%d %H:%M %z' )
308
319
309
320
310
- def import_local_file (modname , modfile = None ):
321
+ def import_local_file (modname : str , modfile : Optional [ str ] = None ) -> ModuleType :
311
322
"""Import a local file as a module.
312
323
313
324
Opens a file in the current directory named `modname`.py, imports it
@@ -318,18 +329,20 @@ def import_local_file(modname, modfile=None):
318
329
if modfile is None :
319
330
modfile = modname + '.py'
320
331
spec = importlib .util .spec_from_file_location (modname , modfile )
332
+ assert spec is not None
321
333
mod = importlib .util .module_from_spec (spec )
322
334
sys .modules [modname ] = mod
335
+ assert spec .loader is not None
323
336
spec .loader .exec_module (mod )
324
337
325
338
return mod
326
339
327
340
328
- def _human_key (s ) :
341
+ def _human_key (s : str ) -> List [ Union [ str , int ]] :
329
342
"""Turn a string into a list of string and number chunks.
330
343
"z23a" -> ["z", 23, "a"]
331
344
"""
332
- def tryint (s ) :
345
+ def tryint (s : str ) -> Union [ str , int ] :
333
346
"""If `s` is a number, return an int, else `s` unchanged."""
334
347
try :
335
348
return int (s )
@@ -338,7 +351,7 @@ def tryint(s):
338
351
339
352
return [tryint (c ) for c in re .split (r"(\d+)" , s )]
340
353
341
- def human_sorted (strings ) :
354
+ def human_sorted (strings : Iterable [ str ]) -> List [ str ] :
342
355
"""Sort the given iterable of strings the way that humans expect.
343
356
344
357
Numeric components in the strings are sorted as numbers.
@@ -348,7 +361,10 @@ def human_sorted(strings):
348
361
"""
349
362
return sorted (strings , key = _human_key )
350
363
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 ]]:
352
368
"""Sort (string, ...) items the way humans expect.
353
369
354
370
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):
359
375
return sorted (items , key = lambda item : (_human_key (item [0 ]), * item [1 :]), reverse = reverse )
360
376
361
377
362
- def plural (n , thing = "" , things = "" ):
378
+ def plural (n : int , thing : str = "" , things : str = "" ) -> str :
363
379
"""Pluralize a word.
364
380
365
381
If n is 1, return thing. Otherwise return things, or thing+s.
0 commit comments