devloop 0.2.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.
- devloop/__init__.py +3 -0
- devloop/agents/__init__.py +33 -0
- devloop/agents/agent_health_monitor.py +105 -0
- devloop/agents/ci_monitor.py +237 -0
- devloop/agents/code_rabbit.py +248 -0
- devloop/agents/doc_lifecycle.py +374 -0
- devloop/agents/echo.py +24 -0
- devloop/agents/file_logger.py +46 -0
- devloop/agents/formatter.py +511 -0
- devloop/agents/git_commit_assistant.py +421 -0
- devloop/agents/linter.py +399 -0
- devloop/agents/performance_profiler.py +284 -0
- devloop/agents/security_scanner.py +322 -0
- devloop/agents/snyk.py +292 -0
- devloop/agents/test_runner.py +484 -0
- devloop/agents/type_checker.py +242 -0
- devloop/cli/__init__.py +1 -0
- devloop/cli/commands/__init__.py +1 -0
- devloop/cli/commands/custom_agents.py +144 -0
- devloop/cli/commands/feedback.py +161 -0
- devloop/cli/commands/summary.py +50 -0
- devloop/cli/main.py +430 -0
- devloop/cli/main_v1.py +144 -0
- devloop/collectors/__init__.py +17 -0
- devloop/collectors/base.py +55 -0
- devloop/collectors/filesystem.py +126 -0
- devloop/collectors/git.py +171 -0
- devloop/collectors/manager.py +159 -0
- devloop/collectors/process.py +221 -0
- devloop/collectors/system.py +195 -0
- devloop/core/__init__.py +21 -0
- devloop/core/agent.py +206 -0
- devloop/core/agent_template.py +498 -0
- devloop/core/amp_integration.py +166 -0
- devloop/core/auto_fix.py +224 -0
- devloop/core/config.py +272 -0
- devloop/core/context.py +0 -0
- devloop/core/context_store.py +530 -0
- devloop/core/contextual_feedback.py +311 -0
- devloop/core/custom_agent.py +439 -0
- devloop/core/debug_trace.py +289 -0
- devloop/core/event.py +105 -0
- devloop/core/event_store.py +316 -0
- devloop/core/feedback.py +311 -0
- devloop/core/learning.py +351 -0
- devloop/core/manager.py +219 -0
- devloop/core/performance.py +433 -0
- devloop/core/proactive_feedback.py +302 -0
- devloop/core/summary_formatter.py +159 -0
- devloop/core/summary_generator.py +275 -0
- devloop-0.2.0.dist-info/METADATA +705 -0
- devloop-0.2.0.dist-info/RECORD +55 -0
- devloop-0.2.0.dist-info/WHEEL +4 -0
- devloop-0.2.0.dist-info/entry_points.txt +3 -0
- devloop-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Formatter agent - auto-formats code on save."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
from devloop.core.agent import Agent, AgentResult
|
|
9
|
+
from devloop.core.context_store import Finding, Severity
|
|
10
|
+
from devloop.core.event import Event
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FormatterConfig:
|
|
14
|
+
"""Configuration for FormatterAgent."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, config: Dict[str, Any]):
|
|
17
|
+
self.enabled = config.get("enabled", True)
|
|
18
|
+
self.format_on_save = config.get("formatOnSave", True)
|
|
19
|
+
self.report_only = config.get("reportOnly", False)
|
|
20
|
+
self.file_patterns = config.get(
|
|
21
|
+
"filePatterns", ["**/*.py", "**/*.js", "**/*.ts"]
|
|
22
|
+
)
|
|
23
|
+
self.formatters = config.get(
|
|
24
|
+
"formatters",
|
|
25
|
+
{
|
|
26
|
+
"python": "black",
|
|
27
|
+
"javascript": "prettier",
|
|
28
|
+
"typescript": "prettier",
|
|
29
|
+
"json": "prettier",
|
|
30
|
+
"markdown": "prettier",
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FormatterAgent(Agent):
|
|
36
|
+
"""Agent that auto-formats code files."""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
name: str,
|
|
41
|
+
triggers: List[str],
|
|
42
|
+
event_bus,
|
|
43
|
+
config: Dict[str, Any] | None = None,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(name, triggers, event_bus)
|
|
46
|
+
self.config = FormatterConfig(config or {})
|
|
47
|
+
|
|
48
|
+
# Loop prevention mechanisms
|
|
49
|
+
self._recent_formats: Dict[
|
|
50
|
+
str, List[float]
|
|
51
|
+
] = {} # file_path -> list of timestamps
|
|
52
|
+
self._format_timeout = 30 # seconds
|
|
53
|
+
self._loop_detection_window = 10 # seconds
|
|
54
|
+
self._max_consecutive_formats = 3 # per file per window
|
|
55
|
+
|
|
56
|
+
async def handle(self, event: Event) -> AgentResult:
|
|
57
|
+
"""Handle file change event by formatting the file."""
|
|
58
|
+
# Skip if both format_on_save and report_only are disabled
|
|
59
|
+
if not self.config.format_on_save and not self.config.report_only:
|
|
60
|
+
return AgentResult(
|
|
61
|
+
agent_name=self.name,
|
|
62
|
+
success=True,
|
|
63
|
+
duration=0,
|
|
64
|
+
message="Formatter disabled (not in format-on-save or report-only mode)",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Extract file path
|
|
68
|
+
file_path = event.payload.get("path")
|
|
69
|
+
if not file_path:
|
|
70
|
+
return AgentResult(
|
|
71
|
+
agent_name=self.name,
|
|
72
|
+
success=True,
|
|
73
|
+
duration=0,
|
|
74
|
+
message="No file path in event",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
path = Path(file_path)
|
|
78
|
+
|
|
79
|
+
# Loop prevention: Check for formatting loops
|
|
80
|
+
if self._detect_formatting_loop(path):
|
|
81
|
+
await self._write_finding_to_context(
|
|
82
|
+
path=path,
|
|
83
|
+
formatter="loop_detector",
|
|
84
|
+
severity="warning",
|
|
85
|
+
message=f"Prevented formatting loop for {path.name} (too many recent format operations)",
|
|
86
|
+
blocking=True,
|
|
87
|
+
)
|
|
88
|
+
result = AgentResult(
|
|
89
|
+
agent_name=self.name,
|
|
90
|
+
success=False,
|
|
91
|
+
duration=0,
|
|
92
|
+
message=f"Prevented formatting loop for {path.name} (too many recent format operations)",
|
|
93
|
+
error="FORMATTING_LOOP_DETECTED",
|
|
94
|
+
)
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
# Check if file should be formatted
|
|
98
|
+
if not self._should_format(path):
|
|
99
|
+
result = AgentResult(
|
|
100
|
+
agent_name=self.name,
|
|
101
|
+
success=True,
|
|
102
|
+
duration=0,
|
|
103
|
+
message=f"Skipped {path.name} (not in patterns)",
|
|
104
|
+
)
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
# Get appropriate formatter
|
|
108
|
+
formatter = self._get_formatter_for_file(path)
|
|
109
|
+
if not formatter:
|
|
110
|
+
result = AgentResult(
|
|
111
|
+
agent_name=self.name,
|
|
112
|
+
success=True,
|
|
113
|
+
duration=0,
|
|
114
|
+
message=f"No formatter configured for {path.suffix}",
|
|
115
|
+
)
|
|
116
|
+
return result
|
|
117
|
+
|
|
118
|
+
# Idempotency check: Only format if file actually needs formatting
|
|
119
|
+
if self.config.format_on_save and not self.config.report_only:
|
|
120
|
+
needs_formatting, check_error = await self._check_formatter(formatter, path)
|
|
121
|
+
if check_error:
|
|
122
|
+
await self._write_finding_to_context(
|
|
123
|
+
path=path,
|
|
124
|
+
formatter=formatter,
|
|
125
|
+
severity="error",
|
|
126
|
+
message=f"Failed to check if {path.name} needs formatting: {check_error}",
|
|
127
|
+
blocking=True,
|
|
128
|
+
)
|
|
129
|
+
result = AgentResult(
|
|
130
|
+
agent_name=self.name,
|
|
131
|
+
success=False,
|
|
132
|
+
duration=0,
|
|
133
|
+
message=f"Failed to check if {path.name} needs formatting: {check_error}",
|
|
134
|
+
error=check_error,
|
|
135
|
+
)
|
|
136
|
+
return result
|
|
137
|
+
if not needs_formatting:
|
|
138
|
+
result = AgentResult(
|
|
139
|
+
agent_name=self.name,
|
|
140
|
+
success=True,
|
|
141
|
+
duration=0,
|
|
142
|
+
message=f"{path.name} is already formatted",
|
|
143
|
+
)
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
# Run formatter
|
|
147
|
+
if self.config.report_only:
|
|
148
|
+
# Check mode: see if formatting is needed but don't modify
|
|
149
|
+
needs_formatting, error = await self._check_formatter(formatter, path)
|
|
150
|
+
if error:
|
|
151
|
+
message = f"Check failed for {path.name}: {error}"
|
|
152
|
+
success = False
|
|
153
|
+
await self._write_finding_to_context(
|
|
154
|
+
path=path,
|
|
155
|
+
formatter=formatter,
|
|
156
|
+
severity="error",
|
|
157
|
+
message=message,
|
|
158
|
+
blocking=True,
|
|
159
|
+
)
|
|
160
|
+
elif needs_formatting:
|
|
161
|
+
message = (
|
|
162
|
+
f"Would format {path.name} with {formatter} (report-only mode)"
|
|
163
|
+
)
|
|
164
|
+
success = True
|
|
165
|
+
await self._write_finding_to_context(
|
|
166
|
+
path=path,
|
|
167
|
+
formatter=formatter,
|
|
168
|
+
severity="info",
|
|
169
|
+
message=f"{path.name} needs formatting with {formatter}",
|
|
170
|
+
auto_fixable=True,
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
message = f"No formatting needed for {path.name}"
|
|
174
|
+
success = True
|
|
175
|
+
|
|
176
|
+
result = AgentResult(
|
|
177
|
+
agent_name=self.name,
|
|
178
|
+
success=success,
|
|
179
|
+
duration=0,
|
|
180
|
+
message=message,
|
|
181
|
+
data={
|
|
182
|
+
"file": str(path),
|
|
183
|
+
"formatter": formatter,
|
|
184
|
+
"needs_formatting": needs_formatting,
|
|
185
|
+
"report_only": True,
|
|
186
|
+
},
|
|
187
|
+
error=error,
|
|
188
|
+
)
|
|
189
|
+
return result
|
|
190
|
+
else:
|
|
191
|
+
# Format mode: actually modify the file
|
|
192
|
+
success, error = await self._run_formatter(formatter, path)
|
|
193
|
+
|
|
194
|
+
if success:
|
|
195
|
+
# Record successful formatting operation for loop prevention
|
|
196
|
+
self._record_formatting_operation(path)
|
|
197
|
+
message = f"Formatted {path.name} with {formatter}"
|
|
198
|
+
else:
|
|
199
|
+
message = f"Failed to format {path.name}: {error}"
|
|
200
|
+
await self._write_finding_to_context(
|
|
201
|
+
path=path,
|
|
202
|
+
formatter=formatter,
|
|
203
|
+
severity="error",
|
|
204
|
+
message=message,
|
|
205
|
+
blocking=True,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
result = AgentResult(
|
|
209
|
+
agent_name=self.name,
|
|
210
|
+
success=success,
|
|
211
|
+
duration=0,
|
|
212
|
+
message=message,
|
|
213
|
+
data={"file": str(path), "formatter": formatter, "formatted": success},
|
|
214
|
+
error=error if not success else None,
|
|
215
|
+
)
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def _should_format(self, path: Path) -> bool:
|
|
219
|
+
"""Check if file should be formatted based on patterns."""
|
|
220
|
+
if not path.exists():
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
suffix = path.suffix
|
|
224
|
+
for pattern in self.config.file_patterns:
|
|
225
|
+
if pattern.endswith(suffix):
|
|
226
|
+
return True
|
|
227
|
+
if "*" in pattern and suffix in pattern:
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def _get_formatter_for_file(self, path: Path) -> Optional[str]:
|
|
233
|
+
"""Get the appropriate formatter for a file."""
|
|
234
|
+
suffix = path.suffix.lstrip(".")
|
|
235
|
+
|
|
236
|
+
# Map file extensions to language
|
|
237
|
+
extension_map = {
|
|
238
|
+
"py": "python",
|
|
239
|
+
"js": "javascript",
|
|
240
|
+
"jsx": "javascript",
|
|
241
|
+
"ts": "typescript",
|
|
242
|
+
"tsx": "typescript",
|
|
243
|
+
"json": "json",
|
|
244
|
+
"md": "markdown",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
language = extension_map.get(suffix)
|
|
248
|
+
if language:
|
|
249
|
+
return self.config.formatters.get(language)
|
|
250
|
+
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
def _detect_formatting_loop(self, path: Path) -> bool:
|
|
254
|
+
"""Detect if we're in a formatting loop for this file."""
|
|
255
|
+
import time
|
|
256
|
+
|
|
257
|
+
file_key = str(path.resolve())
|
|
258
|
+
now = time.time()
|
|
259
|
+
|
|
260
|
+
# Clean up old entries (older than detection window)
|
|
261
|
+
for k in list(self._recent_formats.keys()):
|
|
262
|
+
self._recent_formats[k] = [
|
|
263
|
+
ts
|
|
264
|
+
for ts in self._recent_formats[k]
|
|
265
|
+
if now - ts < self._loop_detection_window
|
|
266
|
+
]
|
|
267
|
+
if not self._recent_formats[k]:
|
|
268
|
+
del self._recent_formats[k]
|
|
269
|
+
|
|
270
|
+
# Count recent formats for this file
|
|
271
|
+
timestamps = self._recent_formats.get(file_key, [])
|
|
272
|
+
recent_count = len(timestamps)
|
|
273
|
+
|
|
274
|
+
if recent_count >= self._max_consecutive_formats + 1:
|
|
275
|
+
self.logger.warning(
|
|
276
|
+
f"Formatting loop detected for {path.name}: "
|
|
277
|
+
f"{recent_count} formats in {self._loop_detection_window}s"
|
|
278
|
+
)
|
|
279
|
+
return True
|
|
280
|
+
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
def _record_formatting_operation(self, path: Path) -> None:
|
|
284
|
+
"""Record that we just formatted this file."""
|
|
285
|
+
import time
|
|
286
|
+
|
|
287
|
+
file_key = str(path.resolve())
|
|
288
|
+
if file_key not in self._recent_formats:
|
|
289
|
+
self._recent_formats[file_key] = []
|
|
290
|
+
self._recent_formats[file_key].append(time.time())
|
|
291
|
+
|
|
292
|
+
async def _write_finding_to_context(
|
|
293
|
+
self,
|
|
294
|
+
path: Path,
|
|
295
|
+
formatter: str,
|
|
296
|
+
severity: str,
|
|
297
|
+
message: str,
|
|
298
|
+
blocking: bool = False,
|
|
299
|
+
auto_fixable: bool = False,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Write a formatting finding to the context store."""
|
|
302
|
+
from devloop.core.context_store import context_store
|
|
303
|
+
|
|
304
|
+
finding = Finding(
|
|
305
|
+
id=f"{self.name}-{path}-{formatter}",
|
|
306
|
+
agent=self.name,
|
|
307
|
+
timestamp=str(datetime.now()),
|
|
308
|
+
file=str(path),
|
|
309
|
+
severity=Severity(severity),
|
|
310
|
+
message=message,
|
|
311
|
+
suggestion=f"Run {formatter} on {path}" if auto_fixable else "",
|
|
312
|
+
auto_fixable=auto_fixable,
|
|
313
|
+
context={
|
|
314
|
+
"formatter": formatter,
|
|
315
|
+
"blocking": blocking,
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
await context_store.add_finding(finding)
|
|
319
|
+
|
|
320
|
+
async def _run_formatter(
|
|
321
|
+
self, formatter: str, path: Path
|
|
322
|
+
) -> tuple[bool, Optional[str]]:
|
|
323
|
+
"""Run formatter on a file with timeout protection."""
|
|
324
|
+
try:
|
|
325
|
+
# Add timeout protection to prevent hanging formatters
|
|
326
|
+
import asyncio
|
|
327
|
+
|
|
328
|
+
if formatter == "black":
|
|
329
|
+
result = await asyncio.wait_for(
|
|
330
|
+
self._run_black(path), timeout=self._format_timeout
|
|
331
|
+
)
|
|
332
|
+
return result
|
|
333
|
+
elif formatter == "prettier":
|
|
334
|
+
result = await asyncio.wait_for(
|
|
335
|
+
self._run_prettier(path), timeout=self._format_timeout
|
|
336
|
+
)
|
|
337
|
+
return result
|
|
338
|
+
else:
|
|
339
|
+
return False, f"Unknown formatter: {formatter}"
|
|
340
|
+
|
|
341
|
+
except asyncio.TimeoutError:
|
|
342
|
+
error_msg = f"Formatter {formatter} timed out after {self._format_timeout}s on {path.name}"
|
|
343
|
+
self.logger.error(error_msg)
|
|
344
|
+
return False, error_msg
|
|
345
|
+
except Exception as e:
|
|
346
|
+
self.logger.error(f"Error running {formatter}: {e}")
|
|
347
|
+
return False, str(e)
|
|
348
|
+
|
|
349
|
+
async def _check_formatter(
|
|
350
|
+
self, formatter: str, path: Path
|
|
351
|
+
) -> tuple[bool, Optional[str]]:
|
|
352
|
+
"""Check if file needs formatting without modifying it."""
|
|
353
|
+
try:
|
|
354
|
+
if formatter == "black":
|
|
355
|
+
return await self._check_black(path)
|
|
356
|
+
elif formatter == "prettier":
|
|
357
|
+
return await self._check_prettier(path)
|
|
358
|
+
else:
|
|
359
|
+
return False, f"Unknown formatter: {formatter}"
|
|
360
|
+
|
|
361
|
+
except Exception as e:
|
|
362
|
+
self.logger.error(f"Error checking {formatter}: {e}")
|
|
363
|
+
return False, str(e)
|
|
364
|
+
|
|
365
|
+
async def _run_black(self, path: Path) -> tuple[bool, Optional[str]]:
|
|
366
|
+
"""Run black formatter on Python file."""
|
|
367
|
+
try:
|
|
368
|
+
# Get updated environment with venv bin in PATH
|
|
369
|
+
import os
|
|
370
|
+
|
|
371
|
+
env = os.environ.copy()
|
|
372
|
+
venv_bin = Path(__file__).parent.parent.parent.parent / ".venv" / "bin"
|
|
373
|
+
if venv_bin.exists():
|
|
374
|
+
env["PATH"] = f"{venv_bin}:{env.get('PATH', '')}"
|
|
375
|
+
|
|
376
|
+
# Check if black is installed
|
|
377
|
+
check = await asyncio.create_subprocess_exec(
|
|
378
|
+
"black",
|
|
379
|
+
"--version",
|
|
380
|
+
stdout=asyncio.subprocess.PIPE,
|
|
381
|
+
stderr=asyncio.subprocess.PIPE,
|
|
382
|
+
env=env,
|
|
383
|
+
)
|
|
384
|
+
await check.communicate()
|
|
385
|
+
|
|
386
|
+
if check.returncode != 0:
|
|
387
|
+
return False, "black not installed"
|
|
388
|
+
|
|
389
|
+
# Run black
|
|
390
|
+
proc = await asyncio.create_subprocess_exec(
|
|
391
|
+
"black",
|
|
392
|
+
"--quiet",
|
|
393
|
+
str(path),
|
|
394
|
+
stdout=asyncio.subprocess.PIPE,
|
|
395
|
+
stderr=asyncio.subprocess.PIPE,
|
|
396
|
+
env=env,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
stdout, stderr = await proc.communicate()
|
|
400
|
+
|
|
401
|
+
if proc.returncode == 0:
|
|
402
|
+
return True, None
|
|
403
|
+
else:
|
|
404
|
+
error = stderr.decode() if stderr else "Unknown error"
|
|
405
|
+
return False, error
|
|
406
|
+
|
|
407
|
+
except FileNotFoundError:
|
|
408
|
+
return False, "black command not found"
|
|
409
|
+
|
|
410
|
+
async def _run_prettier(self, path: Path) -> tuple[bool, Optional[str]]:
|
|
411
|
+
"""Run prettier formatter on JavaScript/TypeScript/JSON/Markdown file."""
|
|
412
|
+
try:
|
|
413
|
+
# Check if prettier is installed
|
|
414
|
+
check = await asyncio.create_subprocess_exec(
|
|
415
|
+
"prettier",
|
|
416
|
+
"--version",
|
|
417
|
+
stdout=asyncio.subprocess.PIPE,
|
|
418
|
+
stderr=asyncio.subprocess.PIPE,
|
|
419
|
+
)
|
|
420
|
+
await check.communicate()
|
|
421
|
+
|
|
422
|
+
if check.returncode != 0:
|
|
423
|
+
return False, "prettier not installed"
|
|
424
|
+
|
|
425
|
+
# Run prettier
|
|
426
|
+
proc = await asyncio.create_subprocess_exec(
|
|
427
|
+
"prettier",
|
|
428
|
+
"--write",
|
|
429
|
+
str(path),
|
|
430
|
+
stdout=asyncio.subprocess.PIPE,
|
|
431
|
+
stderr=asyncio.subprocess.PIPE,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
stdout, stderr = await proc.communicate()
|
|
435
|
+
|
|
436
|
+
if proc.returncode == 0:
|
|
437
|
+
return True, None
|
|
438
|
+
else:
|
|
439
|
+
error = stderr.decode() if stderr else "Unknown error"
|
|
440
|
+
return False, error
|
|
441
|
+
|
|
442
|
+
except FileNotFoundError:
|
|
443
|
+
return False, "prettier command not found"
|
|
444
|
+
|
|
445
|
+
async def _check_black(self, path: Path) -> tuple[bool, Optional[str]]:
|
|
446
|
+
"""Check if black would format this file (without modifying it)."""
|
|
447
|
+
try:
|
|
448
|
+
# Get updated environment with venv bin in PATH
|
|
449
|
+
import os
|
|
450
|
+
|
|
451
|
+
env = os.environ.copy()
|
|
452
|
+
venv_bin = Path(__file__).parent.parent.parent.parent / ".venv" / "bin"
|
|
453
|
+
if venv_bin.exists():
|
|
454
|
+
env["PATH"] = f"{venv_bin}:{env.get('PATH', '')}"
|
|
455
|
+
|
|
456
|
+
# Run black in check mode
|
|
457
|
+
proc = await asyncio.create_subprocess_exec(
|
|
458
|
+
"black",
|
|
459
|
+
"--check",
|
|
460
|
+
"--quiet",
|
|
461
|
+
str(path),
|
|
462
|
+
stdout=asyncio.subprocess.PIPE,
|
|
463
|
+
stderr=asyncio.subprocess.PIPE,
|
|
464
|
+
env=env,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
stdout, stderr = await proc.communicate()
|
|
468
|
+
|
|
469
|
+
# black --check returns 0 if file is formatted, 1 if would reformat
|
|
470
|
+
if proc.returncode == 0:
|
|
471
|
+
# File is already formatted
|
|
472
|
+
return False, None
|
|
473
|
+
elif proc.returncode == 1:
|
|
474
|
+
# File would be reformatted
|
|
475
|
+
return True, None
|
|
476
|
+
else:
|
|
477
|
+
# Error occurred
|
|
478
|
+
error = stderr.decode() if stderr else "Unknown error"
|
|
479
|
+
return False, error
|
|
480
|
+
|
|
481
|
+
except FileNotFoundError:
|
|
482
|
+
return False, "black command not found"
|
|
483
|
+
|
|
484
|
+
async def _check_prettier(self, path: Path) -> tuple[bool, Optional[str]]:
|
|
485
|
+
"""Check if prettier would format this file (without modifying it)."""
|
|
486
|
+
try:
|
|
487
|
+
# Run prettier in check mode
|
|
488
|
+
proc = await asyncio.create_subprocess_exec(
|
|
489
|
+
"prettier",
|
|
490
|
+
"--check",
|
|
491
|
+
str(path),
|
|
492
|
+
stdout=asyncio.subprocess.PIPE,
|
|
493
|
+
stderr=asyncio.subprocess.PIPE,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
stdout, stderr = await proc.communicate()
|
|
497
|
+
|
|
498
|
+
# prettier --check returns 0 if formatted, 1 if would reformat
|
|
499
|
+
if proc.returncode == 0:
|
|
500
|
+
# File is already formatted
|
|
501
|
+
return False, None
|
|
502
|
+
elif proc.returncode == 1:
|
|
503
|
+
# File would be reformatted
|
|
504
|
+
return True, None
|
|
505
|
+
else:
|
|
506
|
+
# Error occurred
|
|
507
|
+
error = stderr.decode() if stderr else "Unknown error"
|
|
508
|
+
return False, error
|
|
509
|
+
|
|
510
|
+
except FileNotFoundError:
|
|
511
|
+
return False, "prettier command not found"
|