1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
"""
A monkeypatch on a module attribute only works where that module *reads* the
attribute. A function looks its globals up in the module it is defined in, so
patching a name that a module merely imports and passes on — a re-export —
replaces a binding nothing reads: the code under test keeps the original, and
the test goes on passing for a reason that has nothing to do with it.
`monkeypatch.setattr` on a name that does not exist at all raises, which is
loud and fine. This test covers the quiet case: every module attribute a test
in this suite patches must be read by that module's own code, or defined there.
"""
import ast
import importlib
import types
from pathlib import Path
TESTS = Path(__file__).resolve().parent
def _module_aliases(tree: ast.Module) -> dict[str, str]:
"""alias -> dotted name, for every name a test module binds by import."""
out = {}
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
if a.asname:
out[a.asname] = a.name
else:
out[a.name.split(".")[0]] = a.name.split(".")[0]
elif isinstance(node, ast.ImportFrom) and node.module and not node.level:
for a in node.names:
out[a.asname or a.name] = f"{node.module}.{a.name}"
return out
def _resolve(dotted: str):
"""The object a dotted name designates, or None."""
parts = dotted.split(".")
for i in range(len(parts), 0, -1):
try:
obj = importlib.import_module(".".join(parts[:i]))
except ImportError:
continue
try:
for p in parts[i:]:
obj = getattr(obj, p)
except AttributeError:
return None
return obj
return None
def _patches(path: Path):
"""(module_object, attribute, line) for every replacement of a module
attribute: `monkeypatch.setattr`, `patch`, `patch.object`, or a plain
assignment."""
tree = ast.parse(path.read_text(encoding="utf-8"))
aliases = _module_aliases(tree)
for node in ast.walk(tree):
# `module.NAME = stand_in`, restored by hand in a `finally`.
if isinstance(node, ast.Assign):
for t in node.targets:
if isinstance(t, ast.Attribute) and isinstance(t.value, ast.Name) \
and t.value.id in aliases:
yield _resolve(aliases[t.value.id]), t.attr, node.lineno
continue
if not isinstance(node, ast.Call) or not node.args:
continue
f = node.func
name = (f.attr if isinstance(f, ast.Attribute)
else f.id if isinstance(f, ast.Name) else "")
first = node.args[0]
if name in ("setattr", "object") and len(node.args) >= 2 \
and isinstance(first, ast.Name) \
and isinstance(node.args[1], ast.Constant) \
and isinstance(node.args[1].value, str):
target = _resolve(aliases.get(first.id, ""))
yield target, node.args[1].value, node.lineno
elif name in ("setattr", "patch") and isinstance(first, ast.Constant) \
and isinstance(first.value, str) and "." in first.value:
mod, _, attr = first.value.rpartition(".")
yield _resolve(mod), attr, node.lineno
def _reads_or_defines(module: types.ModuleType, attr: str) -> bool:
tree = ast.parse(Path(module.__file__).read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.Name) and node.id == attr and \
isinstance(node.ctx, ast.Load):
return True
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) \
and node.name == attr:
return True
if isinstance(node, (ast.Assign, ast.AnnAssign, ast.AugAssign)):
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
if any(isinstance(t, ast.Name) and t.id == attr for t in targets):
return True
return False
def test_every_patched_module_attribute_is_read_where_it_is_patched():
dead, checked = [], 0
for path in sorted(TESTS.glob("*.py")):
for target, attr, line in _patches(path):
if not isinstance(target, types.ModuleType) or not getattr(
target, "__file__", None):
continue # a class or an instance: an attribute lookup, always live
if not target.__name__.startswith("meshbay_"):
continue # the standard library and dependencies are not ours to police
checked += 1
if not _reads_or_defines(target, attr):
dead.append(f"{path.name}:{line} {target.__name__}.{attr}")
assert checked > 50, "the scan found almost nothing to check — did the patterns change?"
assert not dead, (
"these patches replace a name the module only imports; the code under "
"test reads it somewhere else, so the patch changes nothing:\n "
+ "\n ".join(dead))
|