onetool-mcp 1.0.0b1__py3-none-any.whl → 1.0.0rc2__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.
- onetool/cli.py +63 -4
- onetool_mcp-1.0.0rc2.dist-info/METADATA +266 -0
- onetool_mcp-1.0.0rc2.dist-info/RECORD +129 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/LICENSE.txt +1 -1
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/licenses/NOTICE.txt +54 -64
- ot/__main__.py +6 -6
- ot/config/__init__.py +48 -46
- ot/config/global_templates/__init__.py +2 -2
- ot/config/{defaults → global_templates}/diagram-templates/api-flow.mmd +33 -33
- ot/config/{defaults → global_templates}/diagram-templates/c4-context.puml +30 -30
- ot/config/{defaults → global_templates}/diagram-templates/class-diagram.mmd +87 -87
- ot/config/{defaults → global_templates}/diagram-templates/feature-mindmap.mmd +70 -70
- ot/config/{defaults → global_templates}/diagram-templates/microservices.d2 +81 -81
- ot/config/{defaults → global_templates}/diagram-templates/project-gantt.mmd +37 -37
- ot/config/{defaults → global_templates}/diagram-templates/state-machine.mmd +42 -42
- ot/config/global_templates/diagram.yaml +167 -0
- ot/config/global_templates/onetool.yaml +3 -1
- ot/config/{defaults → global_templates}/prompts.yaml +102 -97
- ot/config/global_templates/security.yaml +31 -0
- ot/config/global_templates/servers.yaml +93 -12
- ot/config/global_templates/snippets.yaml +5 -26
- ot/config/{defaults → global_templates}/tool_templates/__init__.py +7 -7
- ot/config/loader.py +221 -105
- ot/config/mcp.py +5 -1
- ot/config/secrets.py +192 -190
- ot/decorators.py +116 -116
- ot/executor/__init__.py +35 -35
- ot/executor/base.py +16 -16
- ot/executor/fence_processor.py +83 -83
- ot/executor/linter.py +142 -142
- ot/executor/pep723.py +288 -288
- ot/executor/runner.py +20 -6
- ot/executor/simple.py +163 -163
- ot/executor/validator.py +603 -164
- ot/http_client.py +145 -145
- ot/logging/__init__.py +37 -37
- ot/logging/entry.py +213 -213
- ot/logging/format.py +191 -188
- ot/logging/span.py +349 -349
- ot/meta.py +236 -14
- ot/paths.py +32 -49
- ot/prompts.py +218 -218
- ot/proxy/manager.py +14 -2
- ot/registry/__init__.py +189 -189
- ot/registry/parser.py +269 -269
- ot/server.py +330 -315
- ot/shortcuts/__init__.py +15 -15
- ot/shortcuts/aliases.py +87 -87
- ot/shortcuts/snippets.py +258 -258
- ot/stats/__init__.py +35 -35
- ot/stats/html.py +2 -2
- ot/stats/reader.py +354 -354
- ot/stats/timing.py +57 -57
- ot/support.py +63 -63
- ot/tools.py +1 -1
- ot/utils/batch.py +161 -161
- ot/utils/cache.py +120 -120
- ot/utils/exceptions.py +23 -23
- ot/utils/factory.py +178 -179
- ot/utils/format.py +65 -65
- ot/utils/http.py +202 -202
- ot/utils/platform.py +45 -45
- ot/utils/truncate.py +69 -69
- ot_tools/__init__.py +4 -4
- ot_tools/_convert/__init__.py +12 -12
- ot_tools/_convert/pdf.py +254 -254
- ot_tools/diagram.yaml +167 -167
- ot_tools/scaffold.py +2 -2
- ot_tools/transform.py +124 -19
- ot_tools/web_fetch.py +94 -43
- onetool_mcp-1.0.0b1.dist-info/METADATA +0 -163
- onetool_mcp-1.0.0b1.dist-info/RECORD +0 -132
- ot/config/defaults/bench.yaml +0 -4
- ot/config/defaults/onetool.yaml +0 -25
- ot/config/defaults/servers.yaml +0 -7
- ot/config/defaults/snippets.yaml +0 -4
- ot_tools/firecrawl.py +0 -732
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/WHEEL +0 -0
- {onetool_mcp-1.0.0b1.dist-info → onetool_mcp-1.0.0rc2.dist-info}/entry_points.txt +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/extension.py +0 -0
- /ot/config/{defaults → global_templates}/tool_templates/isolated.py +0 -0
ot/executor/__init__.py
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
"""Execution engines for OneTool.
|
|
2
|
-
|
|
3
|
-
Provides direct host execution via SimpleExecutor.
|
|
4
|
-
The unified runner provides a single entry point for all command execution.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from ot.executor.base import ExecutionResult
|
|
8
|
-
from ot.executor.fence_processor import strip_fences
|
|
9
|
-
from ot.executor.runner import (
|
|
10
|
-
CommandResult,
|
|
11
|
-
PreparedCommand,
|
|
12
|
-
execute_command,
|
|
13
|
-
execute_python_code,
|
|
14
|
-
prepare_command,
|
|
15
|
-
)
|
|
16
|
-
from ot.executor.simple import SimpleExecutor
|
|
17
|
-
from ot.executor.validator import (
|
|
18
|
-
ValidationResult,
|
|
19
|
-
validate_for_exec,
|
|
20
|
-
validate_python_code,
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
__all__ = [
|
|
24
|
-
"CommandResult",
|
|
25
|
-
"ExecutionResult",
|
|
26
|
-
"PreparedCommand",
|
|
27
|
-
"SimpleExecutor",
|
|
28
|
-
"ValidationResult",
|
|
29
|
-
"execute_command",
|
|
30
|
-
"execute_python_code",
|
|
31
|
-
"prepare_command",
|
|
32
|
-
"strip_fences",
|
|
33
|
-
"validate_for_exec",
|
|
34
|
-
"validate_python_code",
|
|
35
|
-
]
|
|
1
|
+
"""Execution engines for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides direct host execution via SimpleExecutor.
|
|
4
|
+
The unified runner provides a single entry point for all command execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ot.executor.base import ExecutionResult
|
|
8
|
+
from ot.executor.fence_processor import strip_fences
|
|
9
|
+
from ot.executor.runner import (
|
|
10
|
+
CommandResult,
|
|
11
|
+
PreparedCommand,
|
|
12
|
+
execute_command,
|
|
13
|
+
execute_python_code,
|
|
14
|
+
prepare_command,
|
|
15
|
+
)
|
|
16
|
+
from ot.executor.simple import SimpleExecutor
|
|
17
|
+
from ot.executor.validator import (
|
|
18
|
+
ValidationResult,
|
|
19
|
+
validate_for_exec,
|
|
20
|
+
validate_python_code,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"CommandResult",
|
|
25
|
+
"ExecutionResult",
|
|
26
|
+
"PreparedCommand",
|
|
27
|
+
"SimpleExecutor",
|
|
28
|
+
"ValidationResult",
|
|
29
|
+
"execute_command",
|
|
30
|
+
"execute_python_code",
|
|
31
|
+
"prepare_command",
|
|
32
|
+
"strip_fences",
|
|
33
|
+
"validate_for_exec",
|
|
34
|
+
"validate_python_code",
|
|
35
|
+
]
|
ot/executor/base.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
"""Base types for executor module."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass
|
|
9
|
-
class ExecutionResult:
|
|
10
|
-
"""Result from executing a tool."""
|
|
11
|
-
|
|
12
|
-
success: bool
|
|
13
|
-
result: str
|
|
14
|
-
duration_seconds: float = 0.0
|
|
15
|
-
executor: str = "simple"
|
|
16
|
-
error_type: str | None = None
|
|
1
|
+
"""Base types for executor module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ExecutionResult:
|
|
10
|
+
"""Result from executing a tool."""
|
|
11
|
+
|
|
12
|
+
success: bool
|
|
13
|
+
result: str
|
|
14
|
+
duration_seconds: float = 0.0
|
|
15
|
+
executor: str = "simple"
|
|
16
|
+
error_type: str | None = None
|
ot/executor/fence_processor.py
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
"""Fence processing for command execution.
|
|
2
|
-
|
|
3
|
-
Handles stripping of:
|
|
4
|
-
- Execution trigger prefixes (__ot, __onetool, mcp__onetool__run)
|
|
5
|
-
- Markdown code fences (triple backticks with/without language)
|
|
6
|
-
- Inline backticks (single and double)
|
|
7
|
-
|
|
8
|
-
Used by the runner to clean commands before execution.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from __future__ import annotations
|
|
12
|
-
|
|
13
|
-
import re
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def strip_fences(command: str) -> tuple[str, bool]:
|
|
17
|
-
"""Strip execution prefixes, markdown code fences, and inline backticks.
|
|
18
|
-
|
|
19
|
-
Execution trigger prefixes (stripped first):
|
|
20
|
-
__ot - short name, default tool
|
|
21
|
-
__ot__run - short name, explicit tool call
|
|
22
|
-
__onetool - full name, default tool
|
|
23
|
-
__onetool__run - full name, explicit tool call
|
|
24
|
-
mcp__onetool__run - explicit MCP call
|
|
25
|
-
|
|
26
|
-
Each prefix supports three invocation styles:
|
|
27
|
-
<prefix> func(arg="value") - simple call
|
|
28
|
-
<prefix> `code` - inline backticks
|
|
29
|
-
<prefix> + code fence - multi-line code fence
|
|
30
|
-
|
|
31
|
-
Note: mcp__ot__run is NOT a valid prefix.
|
|
32
|
-
|
|
33
|
-
Markdown fences (stripped after prefix):
|
|
34
|
-
```python
|
|
35
|
-
code here
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
`code here`
|
|
39
|
-
|
|
40
|
-
`` `code here` ``
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
command: Raw command string that may contain prefixes and fences
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
Tuple of (stripped command, whether anything was stripped)
|
|
47
|
-
"""
|
|
48
|
-
stripped = command.strip()
|
|
49
|
-
anything_stripped = False
|
|
50
|
-
|
|
51
|
-
# Strip execution trigger prefixes:
|
|
52
|
-
# - __ot, __ot__run (short name)
|
|
53
|
-
# - __onetool, __onetool__run (full name)
|
|
54
|
-
# - mcp__onetool__run (explicit MCP call)
|
|
55
|
-
# Note: mcp__ot__run is NOT valid
|
|
56
|
-
prefix_pattern = r"^(?:mcp__onetool__run|__onetool(?:__run)?|__ot(?:__run)?)\s*"
|
|
57
|
-
match = re.match(prefix_pattern, stripped)
|
|
58
|
-
if match:
|
|
59
|
-
stripped = stripped[match.end() :].strip()
|
|
60
|
-
anything_stripped = True
|
|
61
|
-
|
|
62
|
-
# Handle triple backtick fenced blocks
|
|
63
|
-
if stripped.startswith("```"):
|
|
64
|
-
first_newline = stripped.find("\n")
|
|
65
|
-
if first_newline != -1 and stripped.endswith("```"):
|
|
66
|
-
content = stripped[first_newline + 1 : -3].strip()
|
|
67
|
-
return content, True
|
|
68
|
-
|
|
69
|
-
# Handle double backtick fenced blocks: `` `code` ``
|
|
70
|
-
if stripped.startswith("`` `") and stripped.endswith("` ``"):
|
|
71
|
-
content = stripped[4:-4].strip()
|
|
72
|
-
return content, True
|
|
73
|
-
|
|
74
|
-
if stripped.startswith("``") and stripped.endswith("``"):
|
|
75
|
-
content = stripped[2:-2].strip()
|
|
76
|
-
return content, True
|
|
77
|
-
|
|
78
|
-
# Handle inline single backticks: `code`
|
|
79
|
-
if stripped.startswith("`") and stripped.endswith("`") and stripped.count("`") == 2:
|
|
80
|
-
content = stripped[1:-1].strip()
|
|
81
|
-
return content, True
|
|
82
|
-
|
|
83
|
-
return stripped, anything_stripped
|
|
1
|
+
"""Fence processing for command execution.
|
|
2
|
+
|
|
3
|
+
Handles stripping of:
|
|
4
|
+
- Execution trigger prefixes (__ot, __onetool, mcp__onetool__run)
|
|
5
|
+
- Markdown code fences (triple backticks with/without language)
|
|
6
|
+
- Inline backticks (single and double)
|
|
7
|
+
|
|
8
|
+
Used by the runner to clean commands before execution.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def strip_fences(command: str) -> tuple[str, bool]:
|
|
17
|
+
"""Strip execution prefixes, markdown code fences, and inline backticks.
|
|
18
|
+
|
|
19
|
+
Execution trigger prefixes (stripped first):
|
|
20
|
+
__ot - short name, default tool
|
|
21
|
+
__ot__run - short name, explicit tool call
|
|
22
|
+
__onetool - full name, default tool
|
|
23
|
+
__onetool__run - full name, explicit tool call
|
|
24
|
+
mcp__onetool__run - explicit MCP call
|
|
25
|
+
|
|
26
|
+
Each prefix supports three invocation styles:
|
|
27
|
+
<prefix> func(arg="value") - simple call
|
|
28
|
+
<prefix> `code` - inline backticks
|
|
29
|
+
<prefix> + code fence - multi-line code fence
|
|
30
|
+
|
|
31
|
+
Note: mcp__ot__run is NOT a valid prefix.
|
|
32
|
+
|
|
33
|
+
Markdown fences (stripped after prefix):
|
|
34
|
+
```python
|
|
35
|
+
code here
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`code here`
|
|
39
|
+
|
|
40
|
+
`` `code here` ``
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
command: Raw command string that may contain prefixes and fences
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Tuple of (stripped command, whether anything was stripped)
|
|
47
|
+
"""
|
|
48
|
+
stripped = command.strip()
|
|
49
|
+
anything_stripped = False
|
|
50
|
+
|
|
51
|
+
# Strip execution trigger prefixes:
|
|
52
|
+
# - __ot, __ot__run (short name)
|
|
53
|
+
# - __onetool, __onetool__run (full name)
|
|
54
|
+
# - mcp__onetool__run (explicit MCP call)
|
|
55
|
+
# Note: mcp__ot__run is NOT valid
|
|
56
|
+
prefix_pattern = r"^(?:mcp__onetool__run|__onetool(?:__run)?|__ot(?:__run)?)\s*"
|
|
57
|
+
match = re.match(prefix_pattern, stripped)
|
|
58
|
+
if match:
|
|
59
|
+
stripped = stripped[match.end() :].strip()
|
|
60
|
+
anything_stripped = True
|
|
61
|
+
|
|
62
|
+
# Handle triple backtick fenced blocks
|
|
63
|
+
if stripped.startswith("```"):
|
|
64
|
+
first_newline = stripped.find("\n")
|
|
65
|
+
if first_newline != -1 and stripped.endswith("```"):
|
|
66
|
+
content = stripped[first_newline + 1 : -3].strip()
|
|
67
|
+
return content, True
|
|
68
|
+
|
|
69
|
+
# Handle double backtick fenced blocks: `` `code` ``
|
|
70
|
+
if stripped.startswith("`` `") and stripped.endswith("` ``"):
|
|
71
|
+
content = stripped[4:-4].strip()
|
|
72
|
+
return content, True
|
|
73
|
+
|
|
74
|
+
if stripped.startswith("``") and stripped.endswith("``"):
|
|
75
|
+
content = stripped[2:-2].strip()
|
|
76
|
+
return content, True
|
|
77
|
+
|
|
78
|
+
# Handle inline single backticks: `code`
|
|
79
|
+
if stripped.startswith("`") and stripped.endswith("`") and stripped.count("`") == 2:
|
|
80
|
+
content = stripped[1:-1].strip()
|
|
81
|
+
return content, True
|
|
82
|
+
|
|
83
|
+
return stripped, anything_stripped
|
ot/executor/linter.py
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
"""Optional Ruff linting integration for OneTool.
|
|
2
|
-
|
|
3
|
-
Provides style warnings (non-blocking) using Ruff linter if installed.
|
|
4
|
-
Falls back gracefully if Ruff is not available.
|
|
5
|
-
|
|
6
|
-
Example:
|
|
7
|
-
result = lint_code(code)
|
|
8
|
-
if result.available:
|
|
9
|
-
for warning in result.warnings:
|
|
10
|
-
print(f"Style warning: {warning}")
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
from __future__ import annotations
|
|
14
|
-
|
|
15
|
-
import contextlib
|
|
16
|
-
import subprocess
|
|
17
|
-
import tempfile
|
|
18
|
-
from dataclasses import dataclass, field
|
|
19
|
-
from pathlib import Path
|
|
20
|
-
|
|
21
|
-
from loguru import logger
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass
|
|
25
|
-
class LintResult:
|
|
26
|
-
"""Result of linting operation."""
|
|
27
|
-
|
|
28
|
-
available: bool = False # Whether Ruff is available
|
|
29
|
-
warnings: list[str] = field(default_factory=list)
|
|
30
|
-
error: str | None = None # Error message if linting failed
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _check_ruff_available() -> bool:
|
|
34
|
-
"""Check if Ruff is available on the system."""
|
|
35
|
-
try:
|
|
36
|
-
result = subprocess.run(
|
|
37
|
-
["ruff", "--version"],
|
|
38
|
-
capture_output=True,
|
|
39
|
-
text=True,
|
|
40
|
-
timeout=5,
|
|
41
|
-
)
|
|
42
|
-
return result.returncode == 0
|
|
43
|
-
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
44
|
-
return False
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
# Cache Ruff availability check
|
|
48
|
-
_ruff_available: bool | None = None
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def is_ruff_available() -> bool:
|
|
52
|
-
"""Check if Ruff linter is available (cached)."""
|
|
53
|
-
global _ruff_available
|
|
54
|
-
if _ruff_available is None:
|
|
55
|
-
_ruff_available = _check_ruff_available()
|
|
56
|
-
return _ruff_available
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def lint_code(
|
|
60
|
-
code: str,
|
|
61
|
-
select: list[str] | None = None,
|
|
62
|
-
ignore: list[str] | None = None,
|
|
63
|
-
) -> LintResult:
|
|
64
|
-
"""Lint Python code using Ruff.
|
|
65
|
-
|
|
66
|
-
Args:
|
|
67
|
-
code: Python code to lint
|
|
68
|
-
select: Rule codes to enable (e.g., ["E", "F", "W"])
|
|
69
|
-
ignore: Rule codes to ignore (e.g., ["E501"])
|
|
70
|
-
|
|
71
|
-
Returns:
|
|
72
|
-
LintResult with warnings if Ruff is available
|
|
73
|
-
"""
|
|
74
|
-
result = LintResult()
|
|
75
|
-
|
|
76
|
-
if not is_ruff_available():
|
|
77
|
-
result.available = False
|
|
78
|
-
return result
|
|
79
|
-
|
|
80
|
-
result.available = True
|
|
81
|
-
|
|
82
|
-
# Write code to temp file for Ruff
|
|
83
|
-
try:
|
|
84
|
-
with tempfile.NamedTemporaryFile(
|
|
85
|
-
mode="w",
|
|
86
|
-
suffix=".py",
|
|
87
|
-
delete=False,
|
|
88
|
-
) as f:
|
|
89
|
-
f.write(code)
|
|
90
|
-
temp_path = Path(f.name)
|
|
91
|
-
|
|
92
|
-
# Build Ruff command
|
|
93
|
-
cmd = ["ruff", "check", str(temp_path), "--output-format=text"]
|
|
94
|
-
|
|
95
|
-
if select:
|
|
96
|
-
cmd.extend(["--select", ",".join(select)])
|
|
97
|
-
if ignore:
|
|
98
|
-
cmd.extend(["--ignore", ",".join(ignore)])
|
|
99
|
-
|
|
100
|
-
# Run Ruff
|
|
101
|
-
proc = subprocess.run(
|
|
102
|
-
cmd,
|
|
103
|
-
capture_output=True,
|
|
104
|
-
text=True,
|
|
105
|
-
timeout=30,
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
# Parse output - each line is a warning
|
|
109
|
-
if proc.stdout:
|
|
110
|
-
for line in proc.stdout.strip().split("\n"):
|
|
111
|
-
if line.strip():
|
|
112
|
-
# Remove temp file path from output
|
|
113
|
-
warning = line.replace(str(temp_path), "<code>")
|
|
114
|
-
result.warnings.append(warning)
|
|
115
|
-
|
|
116
|
-
except subprocess.TimeoutExpired:
|
|
117
|
-
result.error = "Ruff linting timed out"
|
|
118
|
-
logger.warning("Ruff linting timed out")
|
|
119
|
-
except OSError as e:
|
|
120
|
-
result.error = f"Failed to run Ruff: {e}"
|
|
121
|
-
logger.warning(f"Failed to run Ruff: {e}")
|
|
122
|
-
finally:
|
|
123
|
-
# Clean up temp file
|
|
124
|
-
with contextlib.suppress(NameError, OSError):
|
|
125
|
-
temp_path.unlink()
|
|
126
|
-
|
|
127
|
-
return result
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def lint_code_quick(code: str) -> list[str]:
|
|
131
|
-
"""Quick lint that returns just the warnings list.
|
|
132
|
-
|
|
133
|
-
Convenience function for simple use cases.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
code: Python code to lint
|
|
137
|
-
|
|
138
|
-
Returns:
|
|
139
|
-
List of warning strings (empty if Ruff unavailable)
|
|
140
|
-
"""
|
|
141
|
-
result = lint_code(code)
|
|
142
|
-
return result.warnings
|
|
1
|
+
"""Optional Ruff linting integration for OneTool.
|
|
2
|
+
|
|
3
|
+
Provides style warnings (non-blocking) using Ruff linter if installed.
|
|
4
|
+
Falls back gracefully if Ruff is not available.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
result = lint_code(code)
|
|
8
|
+
if result.available:
|
|
9
|
+
for warning in result.warnings:
|
|
10
|
+
print(f"Style warning: {warning}")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import subprocess
|
|
17
|
+
import tempfile
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class LintResult:
|
|
26
|
+
"""Result of linting operation."""
|
|
27
|
+
|
|
28
|
+
available: bool = False # Whether Ruff is available
|
|
29
|
+
warnings: list[str] = field(default_factory=list)
|
|
30
|
+
error: str | None = None # Error message if linting failed
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _check_ruff_available() -> bool:
|
|
34
|
+
"""Check if Ruff is available on the system."""
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["ruff", "--version"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
timeout=5,
|
|
41
|
+
)
|
|
42
|
+
return result.returncode == 0
|
|
43
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Cache Ruff availability check
|
|
48
|
+
_ruff_available: bool | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_ruff_available() -> bool:
|
|
52
|
+
"""Check if Ruff linter is available (cached)."""
|
|
53
|
+
global _ruff_available
|
|
54
|
+
if _ruff_available is None:
|
|
55
|
+
_ruff_available = _check_ruff_available()
|
|
56
|
+
return _ruff_available
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def lint_code(
|
|
60
|
+
code: str,
|
|
61
|
+
select: list[str] | None = None,
|
|
62
|
+
ignore: list[str] | None = None,
|
|
63
|
+
) -> LintResult:
|
|
64
|
+
"""Lint Python code using Ruff.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
code: Python code to lint
|
|
68
|
+
select: Rule codes to enable (e.g., ["E", "F", "W"])
|
|
69
|
+
ignore: Rule codes to ignore (e.g., ["E501"])
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
LintResult with warnings if Ruff is available
|
|
73
|
+
"""
|
|
74
|
+
result = LintResult()
|
|
75
|
+
|
|
76
|
+
if not is_ruff_available():
|
|
77
|
+
result.available = False
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
result.available = True
|
|
81
|
+
|
|
82
|
+
# Write code to temp file for Ruff
|
|
83
|
+
try:
|
|
84
|
+
with tempfile.NamedTemporaryFile(
|
|
85
|
+
mode="w",
|
|
86
|
+
suffix=".py",
|
|
87
|
+
delete=False,
|
|
88
|
+
) as f:
|
|
89
|
+
f.write(code)
|
|
90
|
+
temp_path = Path(f.name)
|
|
91
|
+
|
|
92
|
+
# Build Ruff command
|
|
93
|
+
cmd = ["ruff", "check", str(temp_path), "--output-format=text"]
|
|
94
|
+
|
|
95
|
+
if select:
|
|
96
|
+
cmd.extend(["--select", ",".join(select)])
|
|
97
|
+
if ignore:
|
|
98
|
+
cmd.extend(["--ignore", ",".join(ignore)])
|
|
99
|
+
|
|
100
|
+
# Run Ruff
|
|
101
|
+
proc = subprocess.run(
|
|
102
|
+
cmd,
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=30,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Parse output - each line is a warning
|
|
109
|
+
if proc.stdout:
|
|
110
|
+
for line in proc.stdout.strip().split("\n"):
|
|
111
|
+
if line.strip():
|
|
112
|
+
# Remove temp file path from output
|
|
113
|
+
warning = line.replace(str(temp_path), "<code>")
|
|
114
|
+
result.warnings.append(warning)
|
|
115
|
+
|
|
116
|
+
except subprocess.TimeoutExpired:
|
|
117
|
+
result.error = "Ruff linting timed out"
|
|
118
|
+
logger.warning("Ruff linting timed out")
|
|
119
|
+
except OSError as e:
|
|
120
|
+
result.error = f"Failed to run Ruff: {e}"
|
|
121
|
+
logger.warning(f"Failed to run Ruff: {e}")
|
|
122
|
+
finally:
|
|
123
|
+
# Clean up temp file
|
|
124
|
+
with contextlib.suppress(NameError, OSError):
|
|
125
|
+
temp_path.unlink()
|
|
126
|
+
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def lint_code_quick(code: str) -> list[str]:
|
|
131
|
+
"""Quick lint that returns just the warnings list.
|
|
132
|
+
|
|
133
|
+
Convenience function for simple use cases.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
code: Python code to lint
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
List of warning strings (empty if Ruff unavailable)
|
|
140
|
+
"""
|
|
141
|
+
result = lint_code(code)
|
|
142
|
+
return result.warnings
|