scitex-linter 0.1.0__tar.gz → 0.1.2__tar.gz
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-0.1.0/src/scitex_linter.egg-info → scitex_linter-0.1.2}/PKG-INFO +53 -15
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/README.md +52 -14
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/pyproject.toml +1 -1
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/__init__.py +1 -1
- scitex_linter-0.1.2/src/scitex_linter/_cmd_format.py +70 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_mcp/tools/lint.py +8 -4
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_server.py +1 -1
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/checker.py +63 -25
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/cli.py +31 -23
- scitex_linter-0.1.2/src/scitex_linter/config.py +206 -0
- scitex_linter-0.1.2/src/scitex_linter/fixer.py +333 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/formatter.py +0 -1
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/rules.py +20 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2/src/scitex_linter.egg-info}/PKG-INFO +53 -15
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/SOURCES.txt +5 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_checker.py +82 -4
- scitex_linter-0.1.2/tests/test_cli_subcommands.py +258 -0
- scitex_linter-0.1.2/tests/test_config.py +171 -0
- scitex_linter-0.1.2/tests/test_fixer.py +415 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_flake8_and_runner.py +7 -1
- scitex_linter-0.1.0/tests/test_cli_subcommands.py +0 -157
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/LICENSE +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/setup.cfg +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_mcp/__init__.py +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_mcp/tools/__init__.py +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/flake8_plugin.py +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/runner.py +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/dependency_links.txt +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/entry_points.txt +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/requires.txt +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/top_level.txt +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_path_rules.py +0 -0
- {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_phase2.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scitex-linter
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: AST-based linter enforcing SciTeX reproducible research patterns
|
|
5
5
|
Author: ywatanabe
|
|
6
6
|
License: AGPL-3.0
|
|
@@ -70,20 +70,21 @@ pip install scitex-linter
|
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
# Lint a file
|
|
73
|
-
scitex-linter
|
|
73
|
+
scitex-linter check script.py
|
|
74
74
|
|
|
75
75
|
# Lint then execute
|
|
76
76
|
scitex-linter python experiment.py --strict
|
|
77
77
|
|
|
78
78
|
# List all 35 rules
|
|
79
|
-
scitex-linter
|
|
79
|
+
scitex-linter rule
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## Five Interfaces
|
|
83
83
|
|
|
84
84
|
| Interface | For | Description |
|
|
85
85
|
|-----------|-----|-------------|
|
|
86
|
-
| 🖥️ **CLI** | Terminal users | `scitex-linter
|
|
86
|
+
| 🖥️ **CLI** | Terminal users | `scitex-linter check`, `scitex-linter python` |
|
|
87
|
+
| ✨ **Format** | Auto-fix | `scitex-linter format` — auto-fix SciTeX issues |
|
|
87
88
|
| 🐍 **Python API** | Programmatic use | `from scitex_linter.checker import lint_file` |
|
|
88
89
|
| 🔌 **flake8 Plugin** | CI pipelines | `flake8 --select STX` |
|
|
89
90
|
| 🔧 **MCP Server** | AI agents | 3 tools for Claude/GPT integration |
|
|
@@ -97,12 +98,18 @@ scitex-linter list-rules
|
|
|
97
98
|
scitex-linter --help # Show all commands
|
|
98
99
|
scitex-linter --help-recursive # Show help for all subcommands
|
|
99
100
|
|
|
100
|
-
#
|
|
101
|
-
scitex-linter
|
|
102
|
-
scitex-linter
|
|
103
|
-
scitex-linter
|
|
104
|
-
scitex-linter
|
|
105
|
-
scitex-linter
|
|
101
|
+
# Check - Check for SciTeX pattern violations
|
|
102
|
+
scitex-linter check script.py # Check a file
|
|
103
|
+
scitex-linter check ./src/ # Check a directory
|
|
104
|
+
scitex-linter check script.py --severity error # Only errors
|
|
105
|
+
scitex-linter check script.py --category path # Only path rules
|
|
106
|
+
scitex-linter check script.py --json # JSON output for CI
|
|
107
|
+
|
|
108
|
+
# Format - Auto-fix SciTeX pattern issues
|
|
109
|
+
scitex-linter format script.py # Fix in place
|
|
110
|
+
scitex-linter format script.py --check # Dry run (exit 1 if changes needed)
|
|
111
|
+
scitex-linter format script.py --diff # Show diff of changes
|
|
112
|
+
scitex-linter format ./src/ # Format a directory
|
|
106
113
|
|
|
107
114
|
# Python - Lint then execute
|
|
108
115
|
scitex-linter python experiment.py # Lint and run
|
|
@@ -110,9 +117,9 @@ scitex-linter python experiment.py --strict # Abort on errors
|
|
|
110
117
|
scitex-linter python experiment.py -- --lr 0.001 # Pass script args
|
|
111
118
|
|
|
112
119
|
# Rules - Browse available rules
|
|
113
|
-
scitex-linter
|
|
114
|
-
scitex-linter
|
|
115
|
-
scitex-linter
|
|
120
|
+
scitex-linter rule # List all 35 rules
|
|
121
|
+
scitex-linter rule --category stats # Filter by category
|
|
122
|
+
scitex-linter rule --json # JSON output
|
|
116
123
|
|
|
117
124
|
# MCP - AI agent server
|
|
118
125
|
scitex-linter mcp start # Start MCP server (stdio)
|
|
@@ -165,7 +172,7 @@ Integrates with existing flake8 workflows, pre-commit hooks, and CI pipelines.
|
|
|
165
172
|
|
|
166
173
|
| Tool | Description |
|
|
167
174
|
|------|-------------|
|
|
168
|
-
| `
|
|
175
|
+
| `linter_check` | Check a Python file for SciTeX compliance |
|
|
169
176
|
| `linter_list_rules` | List all available rules |
|
|
170
177
|
| `linter_check_source` | Lint source code string |
|
|
171
178
|
|
|
@@ -238,6 +245,37 @@ SciTeX Linter works as a **post-tool-use hook** for Claude Code, automatically l
|
|
|
238
245
|
|
|
239
246
|
This ensures AI-generated code follows SciTeX patterns from the start.
|
|
240
247
|
|
|
248
|
+
## Configuration
|
|
249
|
+
|
|
250
|
+
<details>
|
|
251
|
+
<summary><strong>Configure via pyproject.toml or environment variables</strong></summary>
|
|
252
|
+
|
|
253
|
+
<br>
|
|
254
|
+
|
|
255
|
+
```toml
|
|
256
|
+
[tool.scitex-linter]
|
|
257
|
+
severity = "info" # Minimum severity: error, warning, info
|
|
258
|
+
disable = ["STX-P004", "STX-I003"] # Disable specific rules
|
|
259
|
+
exclude-dirs = ["venv", ".venv"] # Directories to skip
|
|
260
|
+
library-dirs = ["src"] # Exempt from script-only rules
|
|
261
|
+
|
|
262
|
+
[tool.scitex-linter.per-rule-severity]
|
|
263
|
+
STX-S003 = "warning" # Downgrade argparse rule
|
|
264
|
+
|
|
265
|
+
[tool.scitex-linter.session]
|
|
266
|
+
required-injected = ["CONFIG", "plt", "COLORS", "rngg", "logger"]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Environment variables (highest priority):
|
|
270
|
+
```bash
|
|
271
|
+
SCITEX_LINTER_SEVERITY=error
|
|
272
|
+
SCITEX_LINTER_DISABLE=STX-P004,STX-I003
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Priority: CLI flags > env vars > pyproject.toml > defaults
|
|
276
|
+
|
|
277
|
+
</details>
|
|
278
|
+
|
|
241
279
|
## What a Clean Script Looks Like
|
|
242
280
|
|
|
243
281
|
```python
|
|
@@ -41,20 +41,21 @@ pip install scitex-linter
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
# Lint a file
|
|
44
|
-
scitex-linter
|
|
44
|
+
scitex-linter check script.py
|
|
45
45
|
|
|
46
46
|
# Lint then execute
|
|
47
47
|
scitex-linter python experiment.py --strict
|
|
48
48
|
|
|
49
49
|
# List all 35 rules
|
|
50
|
-
scitex-linter
|
|
50
|
+
scitex-linter rule
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
##
|
|
53
|
+
## Five Interfaces
|
|
54
54
|
|
|
55
55
|
| Interface | For | Description |
|
|
56
56
|
|-----------|-----|-------------|
|
|
57
|
-
| 🖥️ **CLI** | Terminal users | `scitex-linter
|
|
57
|
+
| 🖥️ **CLI** | Terminal users | `scitex-linter check`, `scitex-linter python` |
|
|
58
|
+
| ✨ **Format** | Auto-fix | `scitex-linter format` — auto-fix SciTeX issues |
|
|
58
59
|
| 🐍 **Python API** | Programmatic use | `from scitex_linter.checker import lint_file` |
|
|
59
60
|
| 🔌 **flake8 Plugin** | CI pipelines | `flake8 --select STX` |
|
|
60
61
|
| 🔧 **MCP Server** | AI agents | 3 tools for Claude/GPT integration |
|
|
@@ -68,12 +69,18 @@ scitex-linter list-rules
|
|
|
68
69
|
scitex-linter --help # Show all commands
|
|
69
70
|
scitex-linter --help-recursive # Show help for all subcommands
|
|
70
71
|
|
|
71
|
-
#
|
|
72
|
-
scitex-linter
|
|
73
|
-
scitex-linter
|
|
74
|
-
scitex-linter
|
|
75
|
-
scitex-linter
|
|
76
|
-
scitex-linter
|
|
72
|
+
# Check - Check for SciTeX pattern violations
|
|
73
|
+
scitex-linter check script.py # Check a file
|
|
74
|
+
scitex-linter check ./src/ # Check a directory
|
|
75
|
+
scitex-linter check script.py --severity error # Only errors
|
|
76
|
+
scitex-linter check script.py --category path # Only path rules
|
|
77
|
+
scitex-linter check script.py --json # JSON output for CI
|
|
78
|
+
|
|
79
|
+
# Format - Auto-fix SciTeX pattern issues
|
|
80
|
+
scitex-linter format script.py # Fix in place
|
|
81
|
+
scitex-linter format script.py --check # Dry run (exit 1 if changes needed)
|
|
82
|
+
scitex-linter format script.py --diff # Show diff of changes
|
|
83
|
+
scitex-linter format ./src/ # Format a directory
|
|
77
84
|
|
|
78
85
|
# Python - Lint then execute
|
|
79
86
|
scitex-linter python experiment.py # Lint and run
|
|
@@ -81,9 +88,9 @@ scitex-linter python experiment.py --strict # Abort on errors
|
|
|
81
88
|
scitex-linter python experiment.py -- --lr 0.001 # Pass script args
|
|
82
89
|
|
|
83
90
|
# Rules - Browse available rules
|
|
84
|
-
scitex-linter
|
|
85
|
-
scitex-linter
|
|
86
|
-
scitex-linter
|
|
91
|
+
scitex-linter rule # List all 35 rules
|
|
92
|
+
scitex-linter rule --category stats # Filter by category
|
|
93
|
+
scitex-linter rule --json # JSON output
|
|
87
94
|
|
|
88
95
|
# MCP - AI agent server
|
|
89
96
|
scitex-linter mcp start # Start MCP server (stdio)
|
|
@@ -136,7 +143,7 @@ Integrates with existing flake8 workflows, pre-commit hooks, and CI pipelines.
|
|
|
136
143
|
|
|
137
144
|
| Tool | Description |
|
|
138
145
|
|------|-------------|
|
|
139
|
-
| `
|
|
146
|
+
| `linter_check` | Check a Python file for SciTeX compliance |
|
|
140
147
|
| `linter_list_rules` | List all available rules |
|
|
141
148
|
| `linter_check_source` | Lint source code string |
|
|
142
149
|
|
|
@@ -209,6 +216,37 @@ SciTeX Linter works as a **post-tool-use hook** for Claude Code, automatically l
|
|
|
209
216
|
|
|
210
217
|
This ensures AI-generated code follows SciTeX patterns from the start.
|
|
211
218
|
|
|
219
|
+
## Configuration
|
|
220
|
+
|
|
221
|
+
<details>
|
|
222
|
+
<summary><strong>Configure via pyproject.toml or environment variables</strong></summary>
|
|
223
|
+
|
|
224
|
+
<br>
|
|
225
|
+
|
|
226
|
+
```toml
|
|
227
|
+
[tool.scitex-linter]
|
|
228
|
+
severity = "info" # Minimum severity: error, warning, info
|
|
229
|
+
disable = ["STX-P004", "STX-I003"] # Disable specific rules
|
|
230
|
+
exclude-dirs = ["venv", ".venv"] # Directories to skip
|
|
231
|
+
library-dirs = ["src"] # Exempt from script-only rules
|
|
232
|
+
|
|
233
|
+
[tool.scitex-linter.per-rule-severity]
|
|
234
|
+
STX-S003 = "warning" # Downgrade argparse rule
|
|
235
|
+
|
|
236
|
+
[tool.scitex-linter.session]
|
|
237
|
+
required-injected = ["CONFIG", "plt", "COLORS", "rngg", "logger"]
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Environment variables (highest priority):
|
|
241
|
+
```bash
|
|
242
|
+
SCITEX_LINTER_SEVERITY=error
|
|
243
|
+
SCITEX_LINTER_DISABLE=STX-P004,STX-I003
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Priority: CLI flags > env vars > pyproject.toml > defaults
|
|
247
|
+
|
|
248
|
+
</details>
|
|
249
|
+
|
|
212
250
|
## What a Clean Script Looks Like
|
|
213
251
|
|
|
214
252
|
```python
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""CLI handler for the 'format' subcommand."""
|
|
2
|
+
|
|
3
|
+
import difflib
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .fixer import fix_source
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(subparsers) -> None:
|
|
11
|
+
p = subparsers.add_parser(
|
|
12
|
+
"format",
|
|
13
|
+
help="Auto-fix SciTeX pattern issues",
|
|
14
|
+
description="Auto-fix SciTeX pattern issues (e.g. insert missing INJECTED parameters).",
|
|
15
|
+
)
|
|
16
|
+
p.add_argument("path", help="Python file or directory to format")
|
|
17
|
+
p.add_argument(
|
|
18
|
+
"--check",
|
|
19
|
+
action="store_true",
|
|
20
|
+
help="Check if changes needed without writing (exit 1 if changes needed)",
|
|
21
|
+
)
|
|
22
|
+
p.add_argument("--diff", action="store_true", help="Show diff of changes")
|
|
23
|
+
p.set_defaults(func=cmd_format)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cmd_format(args) -> int:
|
|
27
|
+
from .cli import _collect_files
|
|
28
|
+
from .config import load_config
|
|
29
|
+
|
|
30
|
+
config = load_config(args.path)
|
|
31
|
+
target = Path(args.path)
|
|
32
|
+
if not target.exists():
|
|
33
|
+
print(f"Error: {args.path} not found", file=sys.stderr)
|
|
34
|
+
return 2
|
|
35
|
+
|
|
36
|
+
files = _collect_files(target, config=config)
|
|
37
|
+
if not files:
|
|
38
|
+
print(f"No Python files found in {args.path}", file=sys.stderr)
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
changed_count = 0
|
|
42
|
+
for f in files:
|
|
43
|
+
original = f.read_text(encoding="utf-8")
|
|
44
|
+
fixed = fix_source(original, filepath=str(f), config=config)
|
|
45
|
+
if fixed != original:
|
|
46
|
+
changed_count += 1
|
|
47
|
+
if args.diff:
|
|
48
|
+
diff = difflib.unified_diff(
|
|
49
|
+
original.splitlines(keepends=True),
|
|
50
|
+
fixed.splitlines(keepends=True),
|
|
51
|
+
fromfile=str(f),
|
|
52
|
+
tofile=str(f),
|
|
53
|
+
)
|
|
54
|
+
sys.stdout.writelines(diff)
|
|
55
|
+
if not args.check:
|
|
56
|
+
f.write_text(fixed, encoding="utf-8")
|
|
57
|
+
print(f"Fixed {f}")
|
|
58
|
+
else:
|
|
59
|
+
print(f"Would fix {f}")
|
|
60
|
+
|
|
61
|
+
if changed_count == 0:
|
|
62
|
+
print("All files clean")
|
|
63
|
+
return 0
|
|
64
|
+
|
|
65
|
+
if args.check:
|
|
66
|
+
print(f"\n{changed_count} file(s) would be changed")
|
|
67
|
+
return 1
|
|
68
|
+
|
|
69
|
+
print(f"\n{changed_count} file(s) fixed")
|
|
70
|
+
return 0
|
|
@@ -7,15 +7,17 @@ def register_lint_tools(mcp) -> None:
|
|
|
7
7
|
"""Register lint-related MCP tools."""
|
|
8
8
|
|
|
9
9
|
@mcp.tool()
|
|
10
|
-
def
|
|
10
|
+
def linter_check(
|
|
11
11
|
path: str, severity: str = "info", category: Optional[str] = None
|
|
12
12
|
) -> dict:
|
|
13
|
-
"""[linter]
|
|
13
|
+
"""[linter] Check a Python file for SciTeX pattern compliance."""
|
|
14
14
|
from ...checker import lint_file
|
|
15
|
+
from ...config import load_config
|
|
15
16
|
from ...formatter import to_json
|
|
16
17
|
from ...rules import SEVERITY_ORDER
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
config = load_config(path)
|
|
20
|
+
issues = lint_file(path, config=config)
|
|
19
21
|
min_sev = SEVERITY_ORDER.get(severity, 0)
|
|
20
22
|
categories = set(category.split(",")) if category else None
|
|
21
23
|
|
|
@@ -61,7 +63,9 @@ def register_lint_tools(mcp) -> None:
|
|
|
61
63
|
def linter_check_source(source: str, filepath: str = "<stdin>") -> dict:
|
|
62
64
|
"""[linter] Lint Python source code string for SciTeX pattern compliance."""
|
|
63
65
|
from ...checker import lint_source
|
|
66
|
+
from ...config import load_config
|
|
64
67
|
from ...formatter import to_json
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
config = load_config(filepath)
|
|
70
|
+
issues = lint_source(source, filepath=filepath, config=config)
|
|
67
71
|
return to_json(issues, filepath)
|
|
@@ -8,7 +8,7 @@ _INSTRUCTIONS = """\
|
|
|
8
8
|
SciTeX Linter: AST-based linter enforcing reproducible research patterns.
|
|
9
9
|
|
|
10
10
|
Tools:
|
|
11
|
-
-
|
|
11
|
+
- linter_check: Check a Python file
|
|
12
12
|
- linter_list_rules: List all lint rules
|
|
13
13
|
- linter_check_source: Lint source code string
|
|
14
14
|
"""
|
|
@@ -8,13 +8,8 @@ from . import rules
|
|
|
8
8
|
from .rules import Rule
|
|
9
9
|
|
|
10
10
|
# Shortcuts for Phase 1 rules
|
|
11
|
-
S001, S002, S003,
|
|
12
|
-
|
|
13
|
-
rules.S002,
|
|
14
|
-
rules.S003,
|
|
15
|
-
rules.S004,
|
|
16
|
-
rules.S005,
|
|
17
|
-
)
|
|
11
|
+
S001, S002, S003 = rules.S001, rules.S002, rules.S003
|
|
12
|
+
S004, S005, S006 = rules.S004, rules.S005, rules.S006
|
|
18
13
|
I001, I002, I003 = rules.I001, rules.I002, rules.I003
|
|
19
14
|
I006, I007 = rules.I006, rules.I007
|
|
20
15
|
|
|
@@ -67,24 +62,42 @@ class Issue:
|
|
|
67
62
|
source_line: str = ""
|
|
68
63
|
|
|
69
64
|
|
|
70
|
-
def is_script(filepath: str) -> bool:
|
|
71
|
-
"""Check if file is a script (not a library module).
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
def is_script(filepath: str, config=None) -> bool:
|
|
66
|
+
"""Check if file is a script (not a library module).
|
|
67
|
+
|
|
68
|
+
Uses config.library_patterns and config.library_dirs to determine
|
|
69
|
+
which files are library modules (exempt from script-only rules).
|
|
70
|
+
"""
|
|
71
|
+
from .config import LinterConfig, matches_library_pattern
|
|
72
|
+
|
|
73
|
+
if config is None:
|
|
74
|
+
config = LinterConfig()
|
|
75
|
+
|
|
76
|
+
path = Path(filepath)
|
|
77
|
+
name = path.name
|
|
78
|
+
|
|
79
|
+
# Check filename against library patterns (e.g., __*__.py, test_*.py)
|
|
80
|
+
if matches_library_pattern(name, config):
|
|
78
81
|
return False
|
|
82
|
+
|
|
83
|
+
# Check if file is inside a library directory (e.g., src/)
|
|
84
|
+
parts = path.parts
|
|
85
|
+
for lib_dir in config.library_dirs:
|
|
86
|
+
if lib_dir in parts:
|
|
87
|
+
return False
|
|
88
|
+
|
|
79
89
|
return True
|
|
80
90
|
|
|
81
91
|
|
|
82
92
|
class SciTeXChecker(ast.NodeVisitor):
|
|
83
93
|
"""AST visitor detecting non-SciTeX patterns."""
|
|
84
94
|
|
|
85
|
-
def __init__(self, source_lines: list, filepath: str = "<stdin>"):
|
|
95
|
+
def __init__(self, source_lines: list, filepath: str = "<stdin>", config=None):
|
|
96
|
+
from .config import LinterConfig
|
|
97
|
+
|
|
86
98
|
self.source_lines = source_lines
|
|
87
99
|
self.filepath = filepath
|
|
100
|
+
self.config = config or LinterConfig()
|
|
88
101
|
self.issues: list = []
|
|
89
102
|
|
|
90
103
|
# Tracking state
|
|
@@ -93,7 +106,7 @@ class SciTeXChecker(ast.NodeVisitor):
|
|
|
93
106
|
self._has_session_decorator = False
|
|
94
107
|
self._session_func_returns_int = False
|
|
95
108
|
self._imports: dict = {} # alias -> full module path
|
|
96
|
-
self._is_script = is_script(filepath)
|
|
109
|
+
self._is_script = is_script(filepath, self.config)
|
|
97
110
|
|
|
98
111
|
# -----------------------------------------------------------------
|
|
99
112
|
# Import visitors
|
|
@@ -343,10 +356,15 @@ class SciTeXChecker(ast.NodeVisitor):
|
|
|
343
356
|
# Function/decorator visitors
|
|
344
357
|
# -----------------------------------------------------------------
|
|
345
358
|
|
|
359
|
+
@property
|
|
360
|
+
def _REQUIRED_INJECTED(self):
|
|
361
|
+
return set(self.config.required_injected)
|
|
362
|
+
|
|
346
363
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
347
364
|
if self._has_session_deco(node):
|
|
348
365
|
self._has_session_decorator = True
|
|
349
366
|
self._check_session_return(node)
|
|
367
|
+
self._check_injected_params(node)
|
|
350
368
|
self.generic_visit(node)
|
|
351
369
|
|
|
352
370
|
visit_AsyncFunctionDef = visit_FunctionDef
|
|
@@ -380,6 +398,25 @@ class SciTeXChecker(ast.NodeVisitor):
|
|
|
380
398
|
line = self._get_source(node.lineno)
|
|
381
399
|
self._add(S004, node.lineno, node.col_offset, line)
|
|
382
400
|
|
|
401
|
+
def _check_injected_params(self, node: ast.FunctionDef) -> None:
|
|
402
|
+
"""Check that @stx.session function declares all INJECTED parameters."""
|
|
403
|
+
declared = {arg.arg for arg in node.args.args}
|
|
404
|
+
missing = sorted(self._REQUIRED_INJECTED - declared)
|
|
405
|
+
if missing:
|
|
406
|
+
line = self._get_source(node.lineno)
|
|
407
|
+
missing_str = ", ".join(missing)
|
|
408
|
+
dynamic_rule = Rule(
|
|
409
|
+
id=S006.id,
|
|
410
|
+
severity=S006.severity,
|
|
411
|
+
category=S006.category,
|
|
412
|
+
message=(
|
|
413
|
+
f"@stx.session function missing INJECTED parameters: {missing_str}. "
|
|
414
|
+
f"All 5 must be declared: CONFIG, COLORS, logger, plt, rngg"
|
|
415
|
+
),
|
|
416
|
+
suggestion=S006.suggestion,
|
|
417
|
+
)
|
|
418
|
+
self._add(dynamic_rule, node.lineno, node.col_offset, line)
|
|
419
|
+
|
|
383
420
|
# -----------------------------------------------------------------
|
|
384
421
|
# Module-level checks (run after visiting entire tree)
|
|
385
422
|
# -----------------------------------------------------------------
|
|
@@ -427,11 +464,12 @@ class SciTeXChecker(ast.NodeVisitor):
|
|
|
427
464
|
self.issues.sort(key=lambda i: (-SEVERITY_ORDER[i.rule.severity], i.line))
|
|
428
465
|
return self.issues
|
|
429
466
|
|
|
430
|
-
# -----------------------------------------------------------------
|
|
431
|
-
# Helpers
|
|
432
|
-
# -----------------------------------------------------------------
|
|
433
|
-
|
|
434
467
|
def _add(self, rule: Rule, line: int, col: int, source_line: str) -> None:
|
|
468
|
+
if rule.id in self.config.disable:
|
|
469
|
+
return
|
|
470
|
+
sev = self.config.per_rule_severity.get(rule.id)
|
|
471
|
+
if sev:
|
|
472
|
+
rule = Rule(rule.id, sev, rule.category, rule.message, rule.suggestion)
|
|
435
473
|
self.issues.append(
|
|
436
474
|
Issue(rule=rule, line=line, col=col, source_line=source_line)
|
|
437
475
|
)
|
|
@@ -447,7 +485,7 @@ class SciTeXChecker(ast.NodeVisitor):
|
|
|
447
485
|
# =============================================================================
|
|
448
486
|
|
|
449
487
|
|
|
450
|
-
def lint_source(source: str, filepath: str = "<stdin>") -> list:
|
|
488
|
+
def lint_source(source: str, filepath: str = "<stdin>", config=None) -> list:
|
|
451
489
|
"""Lint Python source code and return list of Issues."""
|
|
452
490
|
try:
|
|
453
491
|
tree = ast.parse(source, filename=filepath)
|
|
@@ -455,15 +493,15 @@ def lint_source(source: str, filepath: str = "<stdin>") -> list:
|
|
|
455
493
|
return []
|
|
456
494
|
|
|
457
495
|
lines = source.splitlines()
|
|
458
|
-
checker = SciTeXChecker(lines, filepath=filepath)
|
|
496
|
+
checker = SciTeXChecker(lines, filepath=filepath, config=config)
|
|
459
497
|
checker.visit(tree)
|
|
460
498
|
return checker.get_issues()
|
|
461
499
|
|
|
462
500
|
|
|
463
|
-
def lint_file(filepath: str) -> list:
|
|
501
|
+
def lint_file(filepath: str, config=None) -> list:
|
|
464
502
|
"""Lint a Python file and return list of Issues."""
|
|
465
503
|
path = Path(filepath)
|
|
466
504
|
if not path.exists() or not path.is_file():
|
|
467
505
|
return []
|
|
468
506
|
source = path.read_text(encoding="utf-8")
|
|
469
|
-
return lint_source(source, filepath=str(path))
|
|
507
|
+
return lint_source(source, filepath=str(path), config=config)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""CLI entry point for scitex-linter.
|
|
2
2
|
|
|
3
3
|
Usage:
|
|
4
|
-
scitex-linter
|
|
4
|
+
scitex-linter check <path> [--json] [--severity] [--category] [--no-color]
|
|
5
|
+
scitex-linter format <path> [--check] [--diff]
|
|
5
6
|
scitex-linter python <script.py> [--strict] [-- script_args...]
|
|
6
|
-
scitex-linter
|
|
7
|
+
scitex-linter rule [--json] [--category] [--severity]
|
|
7
8
|
scitex-linter mcp start
|
|
8
9
|
scitex-linter mcp list-tools
|
|
9
10
|
scitex-linter --help-recursive
|
|
@@ -15,7 +16,9 @@ import sys
|
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
17
18
|
from . import __version__
|
|
19
|
+
from ._cmd_format import register as _register_format
|
|
18
20
|
from .checker import lint_file
|
|
21
|
+
from .config import load_config
|
|
19
22
|
from .formatter import format_issue, format_summary, to_json
|
|
20
23
|
from .rules import ALL_RULES, SEVERITY_ORDER
|
|
21
24
|
|
|
@@ -24,13 +27,17 @@ from .rules import ALL_RULES, SEVERITY_ORDER
|
|
|
24
27
|
# =========================================================================
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
def _collect_files(path: Path, recursive: bool = True) -> list:
|
|
30
|
+
def _collect_files(path: Path, recursive: bool = True, config=None) -> list:
|
|
28
31
|
"""Collect Python files from a path."""
|
|
29
32
|
if path.is_file():
|
|
30
33
|
return [path]
|
|
31
34
|
if path.is_dir():
|
|
32
35
|
pattern = "**/*.py" if recursive else "*.py"
|
|
33
|
-
skip =
|
|
36
|
+
skip = (
|
|
37
|
+
set(config.exclude_dirs)
|
|
38
|
+
if config
|
|
39
|
+
else {"__pycache__", ".git", "node_modules", ".tox", "venv", ".venv"}
|
|
40
|
+
)
|
|
34
41
|
return sorted(
|
|
35
42
|
p for p in path.glob(pattern) if not any(s in p.parts for s in skip)
|
|
36
43
|
)
|
|
@@ -38,17 +45,17 @@ def _collect_files(path: Path, recursive: bool = True) -> list:
|
|
|
38
45
|
|
|
39
46
|
|
|
40
47
|
# =========================================================================
|
|
41
|
-
# Subcommand:
|
|
48
|
+
# Subcommand: check
|
|
42
49
|
# =========================================================================
|
|
43
50
|
|
|
44
51
|
|
|
45
|
-
def
|
|
52
|
+
def _register_check(subparsers) -> None:
|
|
46
53
|
p = subparsers.add_parser(
|
|
47
|
-
"
|
|
48
|
-
help="
|
|
49
|
-
description="
|
|
54
|
+
"check",
|
|
55
|
+
help="Check Python files for SciTeX pattern compliance",
|
|
56
|
+
description="Check Python files for SciTeX pattern compliance.",
|
|
50
57
|
)
|
|
51
|
-
p.add_argument("path", help="Python file or directory to
|
|
58
|
+
p.add_argument("path", help="Python file or directory to check")
|
|
52
59
|
p.add_argument("--json", action="store_true", dest="as_json", help="Output as JSON")
|
|
53
60
|
p.add_argument("--no-color", action="store_true", help="Disable colored output")
|
|
54
61
|
p.add_argument(
|
|
@@ -61,10 +68,11 @@ def _register_lint(subparsers) -> None:
|
|
|
61
68
|
"--category",
|
|
62
69
|
help="Filter by category (comma-separated: structure,import,io,plot,stats)",
|
|
63
70
|
)
|
|
64
|
-
p.set_defaults(func=
|
|
71
|
+
p.set_defaults(func=_cmd_check)
|
|
65
72
|
|
|
66
73
|
|
|
67
|
-
def
|
|
74
|
+
def _cmd_check(args) -> int:
|
|
75
|
+
config = load_config(args.path)
|
|
68
76
|
use_color = not args.no_color and sys.stdout.isatty()
|
|
69
77
|
min_sev = SEVERITY_ORDER[args.severity]
|
|
70
78
|
categories = set(args.category.split(",")) if args.category else None
|
|
@@ -74,14 +82,14 @@ def _cmd_lint(args) -> int:
|
|
|
74
82
|
print(f"Error: {args.path} not found", file=sys.stderr)
|
|
75
83
|
return 2
|
|
76
84
|
|
|
77
|
-
files = _collect_files(target)
|
|
85
|
+
files = _collect_files(target, config=config)
|
|
78
86
|
if not files:
|
|
79
87
|
print(f"No Python files found in {args.path}", file=sys.stderr)
|
|
80
88
|
return 0
|
|
81
89
|
|
|
82
90
|
all_results = {}
|
|
83
91
|
for f in files:
|
|
84
|
-
issues = lint_file(str(f))
|
|
92
|
+
issues = lint_file(str(f), config=config)
|
|
85
93
|
issues = [
|
|
86
94
|
i
|
|
87
95
|
for i in issues
|
|
@@ -151,13 +159,13 @@ def _cmd_python(args) -> int:
|
|
|
151
159
|
|
|
152
160
|
|
|
153
161
|
# =========================================================================
|
|
154
|
-
# Subcommand:
|
|
162
|
+
# Subcommand: rule
|
|
155
163
|
# =========================================================================
|
|
156
164
|
|
|
157
165
|
|
|
158
|
-
def
|
|
166
|
+
def _register_rule(subparsers) -> None:
|
|
159
167
|
p = subparsers.add_parser(
|
|
160
|
-
"
|
|
168
|
+
"rule",
|
|
161
169
|
help="List all lint rules",
|
|
162
170
|
description="List all available SciTeX lint rules.",
|
|
163
171
|
)
|
|
@@ -171,10 +179,10 @@ def _register_list_rules(subparsers) -> None:
|
|
|
171
179
|
choices=["error", "warning", "info"],
|
|
172
180
|
help="Filter by severity",
|
|
173
181
|
)
|
|
174
|
-
p.set_defaults(func=
|
|
182
|
+
p.set_defaults(func=_cmd_rule)
|
|
175
183
|
|
|
176
184
|
|
|
177
|
-
def
|
|
185
|
+
def _cmd_rule(args) -> int:
|
|
178
186
|
categories = set(args.category.split(",")) if args.category else None
|
|
179
187
|
rules_list = list(ALL_RULES.values())
|
|
180
188
|
|
|
@@ -272,7 +280,7 @@ def _cmd_mcp_start(args) -> int:
|
|
|
272
280
|
|
|
273
281
|
def _cmd_mcp_list_tools(args) -> int:
|
|
274
282
|
tools = [
|
|
275
|
-
("
|
|
283
|
+
("linter_check", "Check a Python file for SciTeX pattern compliance"),
|
|
276
284
|
("linter_list_rules", "List all available lint rules"),
|
|
277
285
|
("linter_check_source", "Lint Python source code string"),
|
|
278
286
|
]
|
|
@@ -364,7 +372,6 @@ def _cmd_mcp_installation(args) -> int:
|
|
|
364
372
|
def _print_help_recursive(parser, subparsers_actions) -> None:
|
|
365
373
|
"""Print help for all commands recursively."""
|
|
366
374
|
cyan = "\033[96m" if sys.stdout.isatty() else ""
|
|
367
|
-
bold = "\033[1m" if sys.stdout.isatty() else ""
|
|
368
375
|
reset = "\033[0m" if sys.stdout.isatty() else ""
|
|
369
376
|
|
|
370
377
|
bar = "\u2501" * 3
|
|
@@ -409,9 +416,10 @@ def main(argv: list = None) -> int:
|
|
|
409
416
|
|
|
410
417
|
subparsers = parser.add_subparsers(dest="command")
|
|
411
418
|
|
|
412
|
-
|
|
419
|
+
_register_check(subparsers)
|
|
420
|
+
_register_format(subparsers)
|
|
413
421
|
_register_python(subparsers)
|
|
414
|
-
|
|
422
|
+
_register_rule(subparsers)
|
|
415
423
|
_register_mcp(subparsers)
|
|
416
424
|
|
|
417
425
|
# Split on -- to capture script args for the 'python' subcommand
|