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.
Files changed (33) hide show
  1. {scitex_linter-0.1.0/src/scitex_linter.egg-info → scitex_linter-0.1.2}/PKG-INFO +53 -15
  2. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/README.md +52 -14
  3. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/pyproject.toml +1 -1
  4. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/__init__.py +1 -1
  5. scitex_linter-0.1.2/src/scitex_linter/_cmd_format.py +70 -0
  6. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_mcp/tools/lint.py +8 -4
  7. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_server.py +1 -1
  8. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/checker.py +63 -25
  9. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/cli.py +31 -23
  10. scitex_linter-0.1.2/src/scitex_linter/config.py +206 -0
  11. scitex_linter-0.1.2/src/scitex_linter/fixer.py +333 -0
  12. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/formatter.py +0 -1
  13. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/rules.py +20 -0
  14. {scitex_linter-0.1.0 → scitex_linter-0.1.2/src/scitex_linter.egg-info}/PKG-INFO +53 -15
  15. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/SOURCES.txt +5 -0
  16. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_checker.py +82 -4
  17. scitex_linter-0.1.2/tests/test_cli_subcommands.py +258 -0
  18. scitex_linter-0.1.2/tests/test_config.py +171 -0
  19. scitex_linter-0.1.2/tests/test_fixer.py +415 -0
  20. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_flake8_and_runner.py +7 -1
  21. scitex_linter-0.1.0/tests/test_cli_subcommands.py +0 -157
  22. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/LICENSE +0 -0
  23. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/setup.cfg +0 -0
  24. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_mcp/__init__.py +0 -0
  25. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/_mcp/tools/__init__.py +0 -0
  26. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/flake8_plugin.py +0 -0
  27. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter/runner.py +0 -0
  28. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/dependency_links.txt +0 -0
  29. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/entry_points.txt +0 -0
  30. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/requires.txt +0 -0
  31. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/src/scitex_linter.egg-info/top_level.txt +0 -0
  32. {scitex_linter-0.1.0 → scitex_linter-0.1.2}/tests/test_path_rules.py +0 -0
  33. {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.0
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 lint script.py
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 list-rules
79
+ scitex-linter rule
80
80
  ```
81
81
 
82
- ## Four Interfaces
82
+ ## Five Interfaces
83
83
 
84
84
  | Interface | For | Description |
85
85
  |-----------|-----|-------------|
86
- | 🖥️ **CLI** | Terminal users | `scitex-linter lint`, `scitex-linter python` |
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
- # Lint - Check for SciTeX pattern violations
101
- scitex-linter lint script.py # Lint a file
102
- scitex-linter lint ./src/ # Lint a directory
103
- scitex-linter lint script.py --severity error # Only errors
104
- scitex-linter lint script.py --category path # Only path rules
105
- scitex-linter lint script.py --json # JSON output for CI
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 list-rules # List all 35 rules
114
- scitex-linter list-rules --category stats # Filter by category
115
- scitex-linter list-rules --json # JSON output
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
- | `linter_lint` | Lint a Python file for SciTeX compliance |
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 lint script.py
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 list-rules
50
+ scitex-linter rule
51
51
  ```
52
52
 
53
- ## Four Interfaces
53
+ ## Five Interfaces
54
54
 
55
55
  | Interface | For | Description |
56
56
  |-----------|-----|-------------|
57
- | 🖥️ **CLI** | Terminal users | `scitex-linter lint`, `scitex-linter python` |
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
- # Lint - Check for SciTeX pattern violations
72
- scitex-linter lint script.py # Lint a file
73
- scitex-linter lint ./src/ # Lint a directory
74
- scitex-linter lint script.py --severity error # Only errors
75
- scitex-linter lint script.py --category path # Only path rules
76
- scitex-linter lint script.py --json # JSON output for CI
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 list-rules # List all 35 rules
85
- scitex-linter list-rules --category stats # Filter by category
86
- scitex-linter list-rules --json # JSON output
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
- | `linter_lint` | Lint a Python file for SciTeX compliance |
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scitex-linter"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "AST-based linter enforcing SciTeX reproducible research patterns"
9
9
  readme = "README.md"
10
10
  license = {text = "AGPL-3.0"}
@@ -1,3 +1,3 @@
1
1
  """SciTeX Linter — enforce reproducible research patterns via AST analysis."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
@@ -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 linter_lint(
10
+ def linter_check(
11
11
  path: str, severity: str = "info", category: Optional[str] = None
12
12
  ) -> dict:
13
- """[linter] Lint a Python file for SciTeX pattern compliance."""
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
- issues = lint_file(path)
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
- issues = lint_source(source, filepath=filepath)
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
- - linter_lint: Lint a Python file
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, S004, S005 = (
12
- rules.S001,
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
- name = Path(filepath).name
73
- if name == "__init__.py":
74
- return False
75
- if name.startswith("test_") or name == "conftest.py":
76
- return False
77
- if name in ("setup.py", "manage.py"):
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 lint <path> [--json] [--severity] [--category] [--no-color]
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 list-rules [--json] [--category] [--severity]
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 = {"__pycache__", ".git", "node_modules", ".tox", "venv", ".venv"}
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: lint
48
+ # Subcommand: check
42
49
  # =========================================================================
43
50
 
44
51
 
45
- def _register_lint(subparsers) -> None:
52
+ def _register_check(subparsers) -> None:
46
53
  p = subparsers.add_parser(
47
- "lint",
48
- help="Lint Python files for SciTeX pattern compliance",
49
- description="Lint Python files for SciTeX pattern compliance.",
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 lint")
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=_cmd_lint)
71
+ p.set_defaults(func=_cmd_check)
65
72
 
66
73
 
67
- def _cmd_lint(args) -> int:
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: list-rules
162
+ # Subcommand: rule
155
163
  # =========================================================================
156
164
 
157
165
 
158
- def _register_list_rules(subparsers) -> None:
166
+ def _register_rule(subparsers) -> None:
159
167
  p = subparsers.add_parser(
160
- "list-rules",
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=_cmd_list_rules)
182
+ p.set_defaults(func=_cmd_rule)
175
183
 
176
184
 
177
- def _cmd_list_rules(args) -> int:
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
- ("linter_lint", "Lint a Python file for SciTeX pattern compliance"),
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
- _register_lint(subparsers)
419
+ _register_check(subparsers)
420
+ _register_format(subparsers)
413
421
  _register_python(subparsers)
414
- _register_list_rules(subparsers)
422
+ _register_rule(subparsers)
415
423
  _register_mcp(subparsers)
416
424
 
417
425
  # Split on -- to capture script args for the 'python' subcommand