Coverage for coverage / misc.py: 97.758%
169 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Miscellaneous stuff for coverage.py."""
6from __future__ import annotations 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
8import contextlib 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
9import datetime 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
10import errno 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
11import functools 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
12import hashlib 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
13import importlib 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
14import importlib.util 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
15import inspect 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
16import os 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
17import os.path 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
18import re 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
19import sys 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
20import types 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
21from collections.abc import Iterable, Iterator, Mapping, Sequence 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
22from types import ModuleType 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
23from typing import Any, NoReturn, TypeVar 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
25# In 6.0, the exceptions moved from misc.py to exceptions.py. But a number of
26# other packages were importing the exceptions from misc, so import them here.
27# pylint: disable=unused-wildcard-import
28from coverage.exceptions import * # pylint: disable=wildcard-import 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
29from coverage.exceptions import CoverageException 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
30from coverage.types import TArc 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
32ISOLATED_MODULES: dict[ModuleType, ModuleType] = {} 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
35def isolate_module(mod: ModuleType) -> ModuleType: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
36 """Copy a module so that we are isolated from aggressive mocking.
38 If a test suite mocks os.path.exists (for example), and then we need to use
39 it during the test, everything will get tangled up if we use their mock.
40 Making a copy of the module when we import it will isolate coverage.py from
41 those complications.
42 """
43 if mod not in ISOLATED_MODULES: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
44 new_mod = types.ModuleType(mod.__name__) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
45 ISOLATED_MODULES[mod] = new_mod 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
46 for name in dir(mod): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
47 value = getattr(mod, name) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
48 if isinstance(value, types.ModuleType): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
49 value = isolate_module(value) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
50 setattr(new_mod, name, value) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
51 return ISOLATED_MODULES[mod] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
54os = isolate_module(os) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
57class SysModuleSaver: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
58 """Saves the contents of sys.modules, and removes new modules later."""
60 def __init__(self) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
61 self.old_modules = set(sys.modules) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
63 def restore(self) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
64 """Remove any modules imported since this object started."""
65 new_modules = set(sys.modules) - self.old_modules 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
66 for m in new_modules: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
67 del sys.modules[m] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
70@contextlib.contextmanager 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
71def sys_modules_saved() -> Iterator[None]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
72 """A context manager to remove any modules imported during a block."""
73 saver = SysModuleSaver() 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
74 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
75 yield 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
76 finally:
77 saver.restore() 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
80def import_third_party(modname: str) -> tuple[ModuleType, bool]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
81 """Import a third-party module we need, but might not be installed.
83 This also cleans out the module after the import, so that coverage won't
84 appear to have imported it. This lets the third party use coverage for
85 their own tests.
87 Arguments:
88 modname (str): the name of the module to import.
90 Returns:
91 The imported module, and a boolean indicating if the module could be imported.
93 If the boolean is False, the module returned is not the one you want: don't use it.
95 """
96 with sys_modules_saved(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
97 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
98 return importlib.import_module(modname), True 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
99 except ImportError: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
100 return sys, False 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
103def nice_pair(pair: TArc) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
104 """Make a nice string representation of a pair of numbers.
106 If the numbers are equal, just return the number, otherwise return the pair
107 with a dash between them, indicating the range.
109 """
110 start, end = pair 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
111 if start == end: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
112 return f"{start}" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
113 else:
114 return f"{start}-{end}" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uVvW8wX9xY'yZ(z0!A1)234
117def bool_or_none(b: Any) -> bool | None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
118 """Return bool(b), but preserve None."""
119 if b is None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
120 return None 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
121 else:
122 return bool(b) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
125def join_regex(regexes: Iterable[str]) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
126 """Combine a series of regex strings into one that matches any of them."""
127 regexes = list(regexes) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
128 if len(regexes) == 1: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
129 return regexes[0] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
130 else:
131 return "|".join(f"(?:{r})" for r in regexes) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
134def file_be_gone(path: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
135 """Remove a file, and don't get annoyed if it doesn't exist."""
136 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
137 os.remove(path) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
138 except OSError as e: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
139 if e.errno != errno.ENOENT: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
140 raise 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
143def ensure_dir(directory: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
144 """Make sure the directory exists.
146 If `directory` is None or empty, do nothing.
147 """
148 if directory: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
149 os.makedirs(directory, exist_ok=True) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
152def ensure_dir_for_file(path: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
153 """Make sure the directory for the path exists."""
154 ensure_dir(os.path.dirname(path)) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
157class Hasher: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
158 """Hashes Python data for fingerprinting."""
160 def __init__(self) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
161 self.hash = hashlib.new("sha3_256", usedforsecurity=False) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
163 def update(self, v: Any) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
164 """Add `v` to the hash, recursively if needed."""
165 self.hash.update(str(type(v)).encode("utf-8")) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
166 match v: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
167 case None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
168 pass 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
169 case str(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
170 self.hash.update(v.encode("utf-8")) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
171 case bytes(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
172 self.hash.update(v) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
173 case int() | float(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
174 self.hash.update(str(v).encode("utf-8")) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
175 case tuple() | list(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
176 for e in v: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
177 self.update(e) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
178 case dict(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
179 keys = v.keys() 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
180 for k in sorted(keys): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
181 self.update(k) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtU5uVvWwXxYyZz0A1234
182 self.update(v[k]) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtU5uVvWwXxYyZz0A1234
183 case _: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
184 for k in dir(v): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
185 if k.startswith("__"): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
186 continue 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
187 a = getattr(v, k) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
188 if inspect.isroutine(a): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
189 continue 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
190 self.update(k) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
191 self.update(a) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
192 self.hash.update(b".") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
194 def hexdigest(self) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
195 """Retrieve the hex digest of the hash."""
196 return self.hash.hexdigest()[:32] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
199def _needs_to_implement(that: Any, func_name: str) -> NoReturn: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
200 """Helper to raise NotImplementedError in interface stubs."""
201 if hasattr(that, "_coverage_plugin_name"): 201 ↛ 205line 201 didn't jump to line 205 because the condition on line 201 was always true1abcdefghijklmnopqrstuvwxyzA
202 thing = "Plugin" 1abcdefghijklmnopqrstuvwxyzA
203 name = that._coverage_plugin_name 1abcdefghijklmnopqrstuvwxyzA
204 else:
205 thing = "Class"
206 klass = that.__class__
207 name = f"{klass.__module__}.{klass.__name__}"
209 raise NotImplementedError( 1abcdefghijklmnopqrstuvwxyzA
210 f"{thing} {name!r} needs to implement {func_name}()",
211 )
214class DefaultValue: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
215 """A sentinel object to use for unusual default-value needs.
217 Construct with a string that will be used as the repr, for display in help
218 and Sphinx output.
220 """
222 def __init__(self, display_as: str) -> None: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
223 self.display_as = display_as 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
225 def __repr__(self) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
226 return self.display_as 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
229def substitute_variables(text: str, variables: Mapping[str, str]) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
230 """Substitute ``${VAR}`` variables in `text` with their values.
232 Variables in the text can take a number of shell-inspired forms::
234 $VAR
235 ${VAR}
236 ${VAR?} strict: an error if VAR isn't defined.
237 ${VAR-missing} defaulted: "missing" if VAR isn't defined.
238 $$ just a dollar sign.
240 `variables` is a dictionary of variable values.
242 Returns the resulting text with values substituted.
244 """
245 dollar_pattern = r"""(?x) # Use extended regex syntax 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
246 \$ # A dollar sign,
247 (?: # then
248 (?P<dollar> \$ ) | # a dollar sign, or
249 (?P<word1> \w+ ) | # a plain word, or
250 \{ # a {-wrapped
251 (?P<word2> \w+ ) # word,
252 (?: # either
253 (?P<strict> \? ) | # with a strict marker
254 -(?P<defval> [^}]* ) # or a default value
255 )? # maybe.
256 }
257 )
258 """
260 dollar_groups = ("dollar", "word1", "word2") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
262 def dollar_replace(match: re.Match[str]) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
263 """Called for each $replacement."""
264 # Only one of the dollar_groups will have matched, just get its text.
265 word = next(g for g in match.group(*dollar_groups) if g) # pragma: always breaks 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
266 if word == "$": 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
267 return "$" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
268 elif word in variables: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
269 return variables[word] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
270 elif match["strict"]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
271 msg = f"Variable {word} is undefined: {text!r}" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqR6rSsT7tU5uV#vW8wX9xYyZz0!A1234
272 raise CoverageException(msg) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqR6rSsT7tU5uV#vW8wX9xYyZz0!A1234
273 else:
274 return match["defval"] 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
276 text = re.sub(dollar_pattern, dollar_replace, text) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
277 return text 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
280def format_local_datetime(dt: datetime.datetime) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
281 """Return a string with local timezone representing the date."""
282 return dt.astimezone().strftime("%Y-%m-%d %H:%M %z") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
285def import_local_file(modname: str, modfile: str | None = None) -> ModuleType: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
286 """Import a local file as a module.
288 Opens a file in the current directory named `modname`.py, imports it
289 as `modname`, and returns the module object. `modfile` is the file to
290 import if it isn't in the current directory.
292 """
293 if modfile is None: 293 ↛ 295line 293 didn't jump to line 295 because the condition on line 293 was always true1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
294 modfile = modname + ".py" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
295 spec = importlib.util.spec_from_file_location(modname, modfile) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
296 assert spec is not None 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
297 mod = importlib.util.module_from_spec(spec) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
298 sys.modules[modname] = mod 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
299 assert spec.loader is not None 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
300 spec.loader.exec_module(mod) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
302 return mod 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
305@functools.cache 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
306def _human_key(s: str) -> tuple[list[str | int], str]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
307 """Turn a string into a list of string and number chunks.
309 "z23a" -> (["z", 23, "a"], "z23a")
311 The original string is appended as a last value to ensure the
312 key is unique enough so that "x1y" and "x001y" can be distinguished.
313 """
315 def tryint(s: str) -> str | int: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
316 """If `s` is a number, return an int, else `s` unchanged."""
317 try: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
318 return int(s) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
319 except ValueError: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
320 return s 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
322 return ([tryint(c) for c in re.split(r"(\d+)", s)], s) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
325def human_sorted(strings: Iterable[str]) -> list[str]: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
326 """Sort the given iterable of strings the way that humans expect.
328 Numeric components in the strings are sorted as numbers.
330 Returns the sorted list.
332 """
333 return sorted(strings, key=_human_key) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
336SortableItem = TypeVar("SortableItem", bound=Sequence[Any]) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
339def human_sorted_items( 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1
340 items: Iterable[SortableItem],
341 reverse: bool = False,
342) -> list[SortableItem]:
343 """Sort (string, ...) items the way humans expect.
345 The elements of `items` can be any tuple/list. They'll be sorted by the
346 first element (a string), with ties broken by the remaining elements.
348 Returns the sorted list of items.
349 """
350 return sorted(items, key=lambda item: (_human_key(item[0]), *item[1:]), reverse=reverse) 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
353def plural(n: int, thing: str = "", things: str = "") -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
354 """Pluralize a word.
356 If n is 1, return thing. Otherwise return things, or thing+s.
357 """
358 if n == 1: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
359 return thing 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
360 else:
361 return things or (thing + "s") 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
364def stdout_link(text: str, url: str) -> str: 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
365 """Format text+url as a clickable link for stdout.
367 If attached to a terminal, use escape sequences. Otherwise, just return
368 the text.
369 """
370 if hasattr(sys.stdout, "isatty") and sys.stdout.isatty(): 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234
371 return f"\033]8;;{url}\a{text}\033]8;;\a" 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0A1234
372 else:
373 return text 1aBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQ$qR6rS%sT7tU5uV#vW8wX9xY'yZ(z0!A1)234