scitex-linter 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
scitex_linter/rules.py ADDED
@@ -0,0 +1,384 @@
1
+ """Rule definitions for SciTeX linter."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class Rule:
8
+ id: str
9
+ severity: str # "error", "warning", "info"
10
+ category: str # "structure", "import", "io", "plot", "stats"
11
+ message: str
12
+ suggestion: str
13
+
14
+
15
+ # =============================================================================
16
+ # Category S: Structure
17
+ # =============================================================================
18
+
19
+ S001 = Rule(
20
+ id="STX-S001",
21
+ severity="error",
22
+ category="structure",
23
+ message="Missing @stx.session decorator on main function",
24
+ suggestion=(
25
+ "Add @stx.session to enable reproducible session tracking, "
26
+ "auto-CLI, and provenance.\n"
27
+ " @stx.session\n"
28
+ " def main(...):\n"
29
+ " return 0"
30
+ ),
31
+ )
32
+
33
+ S002 = Rule(
34
+ id="STX-S002",
35
+ severity="error",
36
+ category="structure",
37
+ message="Missing `if __name__ == '__main__'` guard",
38
+ suggestion="Add `if __name__ == '__main__': main()` at the end of the script.",
39
+ )
40
+
41
+ S003 = Rule(
42
+ id="STX-S003",
43
+ severity="error",
44
+ category="structure",
45
+ message="argparse detected — @stx.session auto-generates CLI from function signature",
46
+ suggestion=(
47
+ "Remove `import argparse` and define parameters as function arguments:\n"
48
+ " @stx.session\n"
49
+ " def main(data_path: str, threshold: float = 0.5):\n"
50
+ " # Auto-generates: --data-path, --threshold"
51
+ ),
52
+ )
53
+
54
+ S004 = Rule(
55
+ id="STX-S004",
56
+ severity="warning",
57
+ category="structure",
58
+ message="@stx.session function should return an integer exit code",
59
+ suggestion="Add `return 0` for success at the end of your session function.",
60
+ )
61
+
62
+ S005 = Rule(
63
+ id="STX-S005",
64
+ severity="warning",
65
+ category="structure",
66
+ message="Missing `import scitex as stx`",
67
+ suggestion="Add `import scitex as stx` to use SciTeX modules.",
68
+ )
69
+
70
+
71
+ # =============================================================================
72
+ # Category I: Imports
73
+ # =============================================================================
74
+
75
+ I001 = Rule(
76
+ id="STX-I001",
77
+ severity="warning",
78
+ category="import",
79
+ message="Use `stx.plt` instead of importing matplotlib.pyplot directly",
80
+ suggestion="Replace with `stx.plt` (or `plt` injected by @stx.session).",
81
+ )
82
+
83
+ I002 = Rule(
84
+ id="STX-I002",
85
+ severity="warning",
86
+ category="import",
87
+ message="Use `stx.stats` instead of importing scipy.stats directly",
88
+ suggestion="Replace with `stx.stats` which adds effect sizes, CI, and power analysis.",
89
+ )
90
+
91
+ I003 = Rule(
92
+ id="STX-I003",
93
+ severity="warning",
94
+ category="import",
95
+ message="Use `stx.io` instead of pickle for file I/O",
96
+ suggestion="Replace with `stx.io.save(obj, 'file.pkl')` / `stx.io.load('file.pkl')`.",
97
+ )
98
+
99
+ I004 = Rule(
100
+ id="STX-I004",
101
+ severity="warning",
102
+ category="import",
103
+ message="Use `stx.io` for CSV/DataFrame I/O instead of pandas I/O functions",
104
+ suggestion="Replace `pd.read_csv()` with `stx.io.load()`, `df.to_csv()` with `stx.io.save()`.",
105
+ )
106
+
107
+ I005 = Rule(
108
+ id="STX-I005",
109
+ severity="warning",
110
+ category="import",
111
+ message="Use `stx.io` for array I/O instead of numpy save/load",
112
+ suggestion="Replace `np.save()`/`np.load()` with `stx.io.save()`/`stx.io.load()`.",
113
+ )
114
+
115
+ I006 = Rule(
116
+ id="STX-I006",
117
+ severity="info",
118
+ category="import",
119
+ message="Use `rngg` (injected by @stx.session) for reproducible randomness",
120
+ suggestion="Remove `import random` and use `rngg` from @stx.session injection.",
121
+ )
122
+
123
+ I007 = Rule(
124
+ id="STX-I007",
125
+ severity="warning",
126
+ category="import",
127
+ message="Use `logger` (injected by @stx.session) instead of logging module",
128
+ suggestion="Remove `import logging` and use `logger` from @stx.session injection.",
129
+ )
130
+
131
+
132
+ # =============================================================================
133
+ # Category IO: Call-level I/O (Phase 2)
134
+ # =============================================================================
135
+
136
+ IO001 = Rule(
137
+ id="STX-IO001",
138
+ severity="warning",
139
+ category="io",
140
+ message="`np.save()` detected — use `stx.io.save()` for provenance tracking",
141
+ suggestion="Replace `np.save(path, arr)` with `stx.io.save(arr, path)`.",
142
+ )
143
+
144
+ IO002 = Rule(
145
+ id="STX-IO002",
146
+ severity="warning",
147
+ category="io",
148
+ message="`np.load()` detected — use `stx.io.load()` for provenance tracking",
149
+ suggestion="Replace `np.load(path)` with `stx.io.load(path)`.",
150
+ )
151
+
152
+ IO003 = Rule(
153
+ id="STX-IO003",
154
+ severity="warning",
155
+ category="io",
156
+ message="`pd.read_csv()` detected — use `stx.io.load()` for provenance tracking",
157
+ suggestion="Replace `pd.read_csv(path)` with `stx.io.load(path)`.",
158
+ )
159
+
160
+ IO004 = Rule(
161
+ id="STX-IO004",
162
+ severity="warning",
163
+ category="io",
164
+ message="`.to_csv()` detected — use `stx.io.save()` for provenance tracking",
165
+ suggestion="Replace `df.to_csv(path)` with `stx.io.save(df, path)`.",
166
+ )
167
+
168
+ IO005 = Rule(
169
+ id="STX-IO005",
170
+ severity="warning",
171
+ category="io",
172
+ message="`pickle.dump()` detected — use `stx.io.save()` for provenance tracking",
173
+ suggestion="Replace `pickle.dump(obj, f)` with `stx.io.save(obj, 'file.pkl')`.",
174
+ )
175
+
176
+ IO006 = Rule(
177
+ id="STX-IO006",
178
+ severity="warning",
179
+ category="io",
180
+ message="`json.dump()` detected — use `stx.io.save()` for provenance tracking",
181
+ suggestion="Replace `json.dump(obj, f)` with `stx.io.save(obj, 'file.json')`.",
182
+ )
183
+
184
+ IO007 = Rule(
185
+ id="STX-IO007",
186
+ severity="warning",
187
+ category="io",
188
+ message="`plt.savefig()` detected — use `stx.io.save(fig, path)` for metadata embedding",
189
+ suggestion="Replace `plt.savefig(path)` with `stx.io.save(fig, path)`.",
190
+ )
191
+
192
+
193
+ # =============================================================================
194
+ # Category P: Plotting (Phase 2)
195
+ # =============================================================================
196
+
197
+ P001 = Rule(
198
+ id="STX-P001",
199
+ severity="info",
200
+ category="plot",
201
+ message="`ax.plot()` — consider `ax.stx_line()` for automatic CSV data export",
202
+ suggestion="Replace `ax.plot(x, y)` with `ax.stx_line(x, y)` for tracked plotting.",
203
+ )
204
+
205
+ P002 = Rule(
206
+ id="STX-P002",
207
+ severity="info",
208
+ category="plot",
209
+ message="`ax.scatter()` — consider `ax.stx_scatter()` for automatic CSV data export",
210
+ suggestion="Replace `ax.scatter(x, y)` with `ax.stx_scatter(x, y)` for tracked plotting.",
211
+ )
212
+
213
+ P003 = Rule(
214
+ id="STX-P003",
215
+ severity="info",
216
+ category="plot",
217
+ message="`ax.bar()` — consider `ax.stx_bar()` for automatic sample size annotation",
218
+ suggestion="Replace `ax.bar(x, y)` with `ax.stx_bar(x, y)` for tracked plotting.",
219
+ )
220
+
221
+ P004 = Rule(
222
+ id="STX-P004",
223
+ severity="info",
224
+ category="plot",
225
+ message="`plt.show()` is non-reproducible in batch/CI environments",
226
+ suggestion="Remove `plt.show()` — figures are auto-saved in session output directory.",
227
+ )
228
+
229
+ P005 = Rule(
230
+ id="STX-P005",
231
+ severity="info",
232
+ category="plot",
233
+ message="`print()` inside @stx.session — use `logger` for tracked logging",
234
+ suggestion="Replace `print(msg)` with `logger.info(msg)` (injected by @stx.session).",
235
+ )
236
+
237
+
238
+ # =============================================================================
239
+ # Category ST: Statistics (Phase 2)
240
+ # =============================================================================
241
+
242
+ ST001 = Rule(
243
+ id="STX-ST001",
244
+ severity="warning",
245
+ category="stats",
246
+ message="`scipy.stats.ttest_ind()` — use `stx.stats.ttest_ind()` for auto effect size + CI",
247
+ suggestion="Replace with `stx.stats.ttest_ind(a, b)` which includes Cohen's d and CI.",
248
+ )
249
+
250
+ ST002 = Rule(
251
+ id="STX-ST002",
252
+ severity="warning",
253
+ category="stats",
254
+ message="`scipy.stats.mannwhitneyu()` — use `stx.stats.mannwhitneyu()` for auto effect size",
255
+ suggestion="Replace with `stx.stats.mannwhitneyu(a, b)` which includes Cliff's delta.",
256
+ )
257
+
258
+ ST003 = Rule(
259
+ id="STX-ST003",
260
+ severity="warning",
261
+ category="stats",
262
+ message="`scipy.stats.pearsonr()` — use `stx.stats.pearsonr()` for auto CI + power",
263
+ suggestion="Replace with `stx.stats.pearsonr(a, b)` which includes CI and power analysis.",
264
+ )
265
+
266
+ ST004 = Rule(
267
+ id="STX-ST004",
268
+ severity="warning",
269
+ category="stats",
270
+ message="`scipy.stats.f_oneway()` — use `stx.stats.anova_oneway()` for post-hoc + effect sizes",
271
+ suggestion="Replace with `stx.stats.anova_oneway(*groups)` which includes eta-squared.",
272
+ )
273
+
274
+ ST005 = Rule(
275
+ id="STX-ST005",
276
+ severity="warning",
277
+ category="stats",
278
+ message="`scipy.stats.wilcoxon()` — use `stx.stats.wilcoxon()` for auto effect size",
279
+ suggestion="Replace with `stx.stats.wilcoxon(a, b)` which includes effect size and CI.",
280
+ )
281
+
282
+ ST006 = Rule(
283
+ id="STX-ST006",
284
+ severity="warning",
285
+ category="stats",
286
+ message="`scipy.stats.kruskal()` — use `stx.stats.kruskal()` for post-hoc + effect sizes",
287
+ suggestion="Replace with `stx.stats.kruskal(*groups)` which includes epsilon-squared.",
288
+ )
289
+
290
+
291
+ # =============================================================================
292
+ # Category PA: Path Handling
293
+ # =============================================================================
294
+
295
+ PA001 = Rule(
296
+ id="STX-PA001",
297
+ severity="warning",
298
+ category="path",
299
+ message="Absolute path in `stx.io` call — use relative paths for reproducibility",
300
+ suggestion="Use `stx.io.save(obj, './relative/path.ext')` — paths resolve to script_out/.",
301
+ )
302
+
303
+ PA002 = Rule(
304
+ id="STX-PA002",
305
+ severity="warning",
306
+ category="path",
307
+ message="`open()` detected — use `stx.io.save()`/`stx.io.load()` which includes auto-logging",
308
+ suggestion=(
309
+ "Replace `open(path)` with `stx.io.load(path)` or `stx.io.save(obj, path)`.\n"
310
+ " stx.io automatically logs all I/O operations for provenance tracking."
311
+ ),
312
+ )
313
+
314
+ PA003 = Rule(
315
+ id="STX-PA003",
316
+ severity="info",
317
+ category="path",
318
+ message="`os.makedirs()`/`mkdir()` detected — `stx.io.save()` creates directories automatically",
319
+ suggestion=(
320
+ "Remove manual directory creation.\n"
321
+ " `stx.io.save(obj, './subdir/file.ext')` auto-creates `subdir/` inside script_out/."
322
+ ),
323
+ )
324
+
325
+ PA004 = Rule(
326
+ id="STX-PA004",
327
+ severity="warning",
328
+ category="path",
329
+ message="`os.chdir()` detected — scripts should be run from project root",
330
+ suggestion="Remove `os.chdir()` and run scripts from the project root directory.",
331
+ )
332
+
333
+ PA005 = Rule(
334
+ id="STX-PA005",
335
+ severity="info",
336
+ category="path",
337
+ message="Path without `./` prefix in `stx.io` call — use `./` for explicit relative intent",
338
+ suggestion="Use `./filename.ext` instead of `filename.ext` to clarify relative path intent.",
339
+ )
340
+
341
+
342
+ # All rules indexed by ID
343
+ ALL_RULES = {
344
+ r.id: r
345
+ for r in [
346
+ S001,
347
+ S002,
348
+ S003,
349
+ S004,
350
+ S005,
351
+ I001,
352
+ I002,
353
+ I003,
354
+ I004,
355
+ I005,
356
+ I006,
357
+ I007,
358
+ IO001,
359
+ IO002,
360
+ IO003,
361
+ IO004,
362
+ IO005,
363
+ IO006,
364
+ IO007,
365
+ P001,
366
+ P002,
367
+ P003,
368
+ P004,
369
+ P005,
370
+ ST001,
371
+ ST002,
372
+ ST003,
373
+ ST004,
374
+ ST005,
375
+ ST006,
376
+ PA001,
377
+ PA002,
378
+ PA003,
379
+ PA004,
380
+ PA005,
381
+ ]
382
+ }
383
+
384
+ SEVERITY_ORDER = {"error": 2, "warning": 1, "info": 0}
@@ -0,0 +1,72 @@
1
+ """Run a Python script after linting it.
2
+
3
+ Core function used by the `scitex-linter python` subcommand.
4
+ """
5
+
6
+ import os
7
+ import subprocess
8
+ import sys
9
+
10
+ from .checker import lint_file
11
+ from .formatter import format_issue, format_summary
12
+ from .rules import SEVERITY_ORDER
13
+
14
+
15
+ def _is_git_root() -> bool:
16
+ """Check if the current working directory is a git repository root."""
17
+ return os.path.isdir(os.path.join(os.getcwd(), ".git"))
18
+
19
+
20
+ def run_script(filepath: str, strict: bool = False, script_args: list = None) -> int:
21
+ """Lint a script then execute it.
22
+
23
+ Returns the subprocess return code, or 2 if strict mode blocks execution.
24
+ """
25
+ if script_args is None:
26
+ script_args = []
27
+
28
+ # Check if running from git root
29
+ use_color = sys.stderr.isatty()
30
+ if not _is_git_root():
31
+ hint = "\033[94mInfo\033[0m" if use_color else "Info"
32
+ print(
33
+ f"{hint}: not running from a git root directory (cwd: {os.getcwd()})",
34
+ file=sys.stderr,
35
+ )
36
+
37
+ # Lint
38
+ issues = lint_file(filepath)
39
+
40
+ has_errors = any(i.rule.severity == "error" for i in issues)
41
+ has_warnings = any(
42
+ SEVERITY_ORDER[i.rule.severity] >= SEVERITY_ORDER["warning"] for i in issues
43
+ )
44
+
45
+ if issues:
46
+ header = "\033[1mSciTeX Lint\033[0m" if use_color else "SciTeX Lint"
47
+ print(f"\n{header}\n", file=sys.stderr)
48
+
49
+ for issue in issues:
50
+ print(format_issue(issue, filepath, color=use_color), file=sys.stderr)
51
+ print(format_summary(issues, filepath, color=use_color), file=sys.stderr)
52
+ print(file=sys.stderr)
53
+
54
+ if strict and has_errors:
55
+ msg = "\033[91mAborted\033[0m" if use_color else "Aborted"
56
+ print(f"{msg}: errors found (--strict mode)\n", file=sys.stderr)
57
+ return 2
58
+
59
+ if not has_errors and not has_warnings:
60
+ ok = "\033[92mOK\033[0m" if use_color else "OK"
61
+ print(f"{ok} {filepath}", file=sys.stderr)
62
+
63
+ # Execute
64
+ sep = "\u2500" * 60
65
+ if use_color:
66
+ print(f"\n\033[90m{sep}\033[0m", file=sys.stderr)
67
+ else:
68
+ print(f"\n{sep}", file=sys.stderr)
69
+
70
+ cmd = [sys.executable, filepath] + script_args
71
+ result = subprocess.run(cmd)
72
+ return result.returncode