33
44"""Miscellaneous stuff for coverage.py."""
55
6+ from __future__ import annotations
7+
68import contextlib
9+ import datetime
710import errno
811import hashlib
912import importlib
1619import sys
1720import 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
2128from coverage import env
2229from 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
2735from 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
5361class 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
184195class 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