-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
127 lines (100 loc) · 3.89 KB
/
Copy pathmain.py
File metadata and controls
127 lines (100 loc) · 3.89 KB
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
119
120
121
122
123
124
125
126
127
"""Scans the current directory, including subfolders, for .py files and
runs every function decorated with @runnable.
This mirrors how Java frameworks find @Test-annotated methods via
reflection, with one key difference: Python has to execute a module
top-to-bottom before it can see what's inside it, so importing a file
here also runs all of that file's top-level code - not just the
decorated functions. Keep files that get scanned limited to
function/class definitions (guard any script-style code behind
`if __name__ == "__main__":`).
"""
from __future__ import annotations
from pathlib import Path
from utils.logger import log
import importlib.util
import inspect
import sys
import types
_MARKER = "__example__"
_DISABLED_MARKER = "__example_disabled__"
_NAME_MARKER = "__example_name__"
def example(func=None, *, disable=False, name=None):
"""Marks a function so the scanner will pick it up and call it.
Usable as @example or @example(disable=True). A disabled example
stays marked but is skipped by the scanner instead of being run.
"""
def decorator(f):
setattr(f, _MARKER, True)
setattr(f, _DISABLED_MARKER, disable)
log.info("----------------------------------------------------")
log.info("- Example: {}".format(name if name is not None else f.__name__))
log.info("----------------------------------------------------")
return f
if func is not None:
return decorator(func)
return decorator
def _module_name(path: Path, scan_dir: Path) -> str:
rel = path.relative_to(scan_dir).with_suffix("")
return ".".join(rel.parts)
def _load_module(path: Path, scan_dir: Path) -> types.ModuleType:
parent = str(path.parent)
if parent not in sys.path:
sys.path.insert(0, parent)
name = _module_name(path, scan_dir)
spec = importlib.util.spec_from_file_location(name, path)
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
def _find_runnables(module: types.ModuleType):
for _, func in inspect.getmembers(module, inspect.isfunction):
if getattr(func, _MARKER, False):
yield func
def _takes_required_args(func) -> bool:
params = inspect.signature(func).parameters.values()
return any(
p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
and p.default is inspect.Parameter.empty
for p in params
)
def _is_hidden(path: Path, scan_dir: Path) -> bool:
return any(
part.startswith(".") or part == "__pycache__"
for part in path.relative_to(scan_dir).parts
)
def main() -> None:
scan_dir = Path(__file__).resolve().parent
self_path = Path(__file__).resolve()
py_files = sorted(
path
for path in scan_dir.rglob("*.py")
if path.resolve() != self_path
and not path.name.startswith("_")
and not _is_hidden(path, scan_dir)
)
if not py_files:
log.info(f"No .py files found in {scan_dir}")
return
for path in py_files:
rel_path = path.relative_to(scan_dir)
try:
module = _load_module(path, scan_dir)
except Exception as exc:
log.warning(f"[SKIP] {rel_path}: import failed ({exc!r})")
continue
for func in _find_runnables(module):
if getattr(func, _DISABLED_MARKER, False):
log.info(f"[SKIP] {rel_path}:{func.__name__} disabled")
continue
if _takes_required_args(func):
log.info(
f"[SKIP] {rel_path}:{func.__name__} needs arguments, can't auto-run"
)
continue
log.info(f"[RUN] {rel_path}:{func.__name__}")
try:
func()
except Exception as exc:
log.error(f"[ERROR] {rel_path}:{func.__name__} raised {exc!r}")
if __name__ == "__main__":
main()