-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathnapari_plugin_checks.py
177 lines (143 loc) · 4.66 KB
/
napari_plugin_checks.py
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Set of sanity checks for napari plugin
use as a pre-commit hook, or command line interface.
"""
import argparse
import logging
import re
from configparser import ConfigParser
from fnmatch import fnmatch
from pathlib import Path
from typing import Callable, List, Optional, Sequence, Tuple
logging.getLogger("grimp").setLevel(logging.ERROR)
Str2Bool = Callable[[str], bool]
CHECKERS: List[Tuple[Str2Bool, Str2Bool]] = []
BASE_NAME_REGEX = re.compile(r"[^!=><\s@~]+")
REQ_REGEX = re.compile(r"(===|==|!=|~=|>=?|<=?|@)\s*([^,]+)")
qtpy_message = "use Qt compatibility library, like qtpy"
FORBIDDEN_REQUIRES = {
"pyqt": qtpy_message,
"pyqt5": qtpy_message,
"pyside": qtpy_message,
"pyside2": qtpy_message,
"pyside6": qtpy_message,
"pyqt6": qtpy_message,
}
FORBIDDEN_IMPORTS = {
"PyQt5": qtpy_message,
"PyQt6": qtpy_message,
"PySide2": qtpy_message,
"PySide6": qtpy_message,
}
def match(pattern: str) -> Callable[[Str2Bool], Str2Bool]:
"""decorator that declares a checker for a filepattern:
Examples
--------
@match("setup.cfg")
def check_setup_cfg(fname: str) -> bool:
...
"""
def deco(f: Str2Bool) -> Str2Bool:
def _check_name(fname: str) -> bool:
if "/" not in pattern and "*" not in pattern:
return fname.endswith(pattern)
return fnmatch(fname, pat=pattern)
CHECKERS.append((_check_name, f))
return f
return deco
def _req_base(req: str) -> str:
basem = re.match(BASE_NAME_REGEX, req.split(";")[0])
return basem[0].strip() if basem else ""
@match("setup.cfg")
def check_setup_cfg(fname: str) -> bool:
cfg = ConfigParser()
cfg.read(fname)
retv = False
requires = (
cfg.get(
"options",
"install_requires",
fallback="",
)
.strip()
.splitlines()
)
if requires:
# check for Forbidden dependencies, like PyQt5, etc...
for req in requires:
lib = _req_base(req)
err = FORBIDDEN_REQUIRES.get(lib.lower())
if err:
print(
f"Forbidden dependency detected in setup.cfg ({lib!r}): {err}",
)
retv = True
return retv
@match("requirements.txt")
def check_requirements_txt(fname: str) -> bool:
retv = False
for line in Path(fname).read_text().strip().splitlines():
lib = _req_base(line)
err = FORBIDDEN_REQUIRES.get(lib.lower())
if err:
print(
f"Forbidden dependency detected in requirements.txt ({lib!r}): {err}",
)
retv = True
return retv
@match("*.py")
def check_py(fname: str) -> bool:
return _check_imports(fname)
def _check_imports(fname: str) -> bool:
from grimp.adaptors import filesystem, importscanner
from grimp.domain.valueobjects import Module
from grimp.application.ports.modulefinder import FoundPackage
p = Path(fname)
if not p.parent or str(p.parent) == ".":
return False # pragma: no cover
root = p.parent
module = Module(f"{p.parent.name}.{p.name[:-3]}")
scanner = importscanner.ImportScanner(
file_system=filesystem.FileSystem(),
found_packages={
FoundPackage(
name=root.name, directory=str(root), modules=frozenset([module])
)
},
include_external_packages=True,
)
imports = scanner.scan_for_imports(module=module)
retv = False
for imp in imports:
err = FORBIDDEN_IMPORTS.get(imp.imported.name)
if err:
msg = (
f"Forbidden import detected ({imp.imported.name!r}) in {fname!r}: {err}"
)
print(msg)
retv = True
return retv
def check_file(filename: str) -> bool:
# the first checker wins, and must dispatch to others manually if desired.
return next(
(function(filename) for checker, function in CHECKERS if checker(filename)),
False,
)
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("filenames", nargs="*", default=["."])
args = parser.parse_args(argv)
for name in list(args.filenames):
p = Path(name)
if p.is_dir():
args.filenames.remove(name)
args.filenames.extend(
[
str(f)
for f in p.glob("**/*")
if f.is_file()
if not str(f).startswith(".")
],
)
return sum(check_file(filename) for filename in args.filenames)
if __name__ == "__main__":
raise SystemExit(main())