auto-coder 0.1.314__py3-none-any.whl → 0.1.315__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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

@@ -0,0 +1,616 @@
1
+ """
2
+ Module for linting Python code.
3
+ This module provides functionality to analyze Python code for quality and best practices.
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import json
9
+ import subprocess
10
+ import tempfile
11
+ from typing import Dict, List, Any, Optional, Tuple
12
+
13
+ from autocoder.linters.base_linter import BaseLinter
14
+
15
+ class PythonLinter(BaseLinter):
16
+ """
17
+ A class that provides linting functionality for Python code.
18
+ """
19
+
20
+ def __init__(self, verbose: bool = False):
21
+ """
22
+ Initialize the PythonLinter.
23
+
24
+ Args:
25
+ verbose (bool): Whether to display verbose output.
26
+ """
27
+ super().__init__(verbose)
28
+
29
+ def get_supported_extensions(self) -> List[str]:
30
+ """
31
+ Get the list of file extensions supported by this linter.
32
+
33
+ Returns:
34
+ List[str]: List of supported file extensions.
35
+ """
36
+ return ['.py']
37
+
38
+ def _check_dependencies(self) -> bool:
39
+ """
40
+ Check if required dependencies (pylint, flake8, black) are installed.
41
+
42
+ Returns:
43
+ bool: True if all dependencies are available, False otherwise.
44
+ """
45
+ try:
46
+ # Check if python is installed
47
+ subprocess.run([sys.executable, "--version"], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
48
+
49
+ # Check for pylint or flake8
50
+ has_pylint = False
51
+ has_flake8 = False
52
+ has_black = False
53
+
54
+ try:
55
+ subprocess.run([sys.executable, "-m", "pylint", "--version"],
56
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
57
+ has_pylint = True
58
+ except (subprocess.SubprocessError, FileNotFoundError):
59
+ if self.verbose:
60
+ print("Pylint not found.")
61
+
62
+ try:
63
+ subprocess.run([sys.executable, "-m", "flake8", "--version"],
64
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
65
+ has_flake8 = True
66
+ except (subprocess.SubprocessError, FileNotFoundError):
67
+ if self.verbose:
68
+ print("Flake8 not found.")
69
+
70
+ try:
71
+ subprocess.run([sys.executable, "-m", "black", "--version"],
72
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
73
+ has_black = True
74
+ except (subprocess.SubprocessError, FileNotFoundError):
75
+ if self.verbose:
76
+ print("Black not found.")
77
+
78
+ # Need at least one linter
79
+ return has_pylint or has_flake8 or has_black
80
+
81
+ except (subprocess.SubprocessError, FileNotFoundError):
82
+ return False
83
+
84
+ def _install_dependencies_if_needed(self) -> bool:
85
+ """
86
+ Install required linting tools if they are not already installed.
87
+
88
+ Returns:
89
+ bool: True if installation was successful or dependencies already exist, False otherwise.
90
+ """
91
+ try:
92
+ # Check for each dependency separately
93
+ dependencies_to_install = []
94
+
95
+ try:
96
+ subprocess.run([sys.executable, "-m", "pylint", "--version"],
97
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
98
+ except (subprocess.SubprocessError, FileNotFoundError):
99
+ dependencies_to_install.append("pylint")
100
+
101
+ try:
102
+ subprocess.run([sys.executable, "-m", "flake8", "--version"],
103
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
104
+ except (subprocess.SubprocessError, FileNotFoundError):
105
+ dependencies_to_install.append("flake8")
106
+
107
+ try:
108
+ subprocess.run([sys.executable, "-m", "black", "--version"],
109
+ check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
110
+ except (subprocess.SubprocessError, FileNotFoundError):
111
+ dependencies_to_install.append("black")
112
+
113
+ # Install missing dependencies
114
+ if dependencies_to_install:
115
+ if self.verbose:
116
+ print(f"Installing missing dependencies: {', '.join(dependencies_to_install)}")
117
+
118
+ install_cmd = [sys.executable, "-m", "pip", "install"] + dependencies_to_install
119
+
120
+ result = subprocess.run(
121
+ install_cmd,
122
+ stdout=subprocess.PIPE if not self.verbose else None,
123
+ stderr=subprocess.PIPE if not self.verbose else None
124
+ )
125
+
126
+ return result.returncode == 0
127
+
128
+ return True # All dependencies already installed
129
+
130
+ except Exception as e:
131
+ if self.verbose:
132
+ print(f"Error installing dependencies: {str(e)}")
133
+ return False
134
+
135
+ def _run_pylint(self, target: str) -> Dict[str, Any]:
136
+ """
137
+ Run pylint on the target file or directory.
138
+
139
+ Args:
140
+ target (str): Path to the file or directory to lint.
141
+
142
+ Returns:
143
+ Dict[str, Any]: The pylint results.
144
+ """
145
+ result = {
146
+ 'error_count': 0,
147
+ 'warning_count': 0,
148
+ 'issues': []
149
+ }
150
+
151
+ try:
152
+ # Create a temp file to store JSON output
153
+ with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
154
+ tmp_path = tmp.name
155
+
156
+ # Run pylint with JSON reporter
157
+ cmd = [
158
+ sys.executable,
159
+ "-m",
160
+ "pylint",
161
+ "--output-format=json",
162
+ target
163
+ ]
164
+
165
+ process = subprocess.run(
166
+ cmd,
167
+ stdout=subprocess.PIPE,
168
+ stderr=subprocess.PIPE,
169
+ text=True
170
+ )
171
+
172
+ if process.stdout:
173
+ try:
174
+ pylint_output = json.loads(process.stdout)
175
+
176
+ # Process pylint issues
177
+ for message in pylint_output:
178
+ severity = "warning"
179
+ if message.get('type') in ['error', 'fatal']:
180
+ severity = "error"
181
+ result['error_count'] += 1
182
+ else:
183
+ result['warning_count'] += 1
184
+
185
+ issue = {
186
+ 'file': message.get('path', ''),
187
+ 'line': message.get('line', 0),
188
+ 'column': message.get('column', 0),
189
+ 'severity': severity,
190
+ 'message': message.get('message', ''),
191
+ 'rule': message.get('symbol', 'unknown'),
192
+ 'tool': 'pylint'
193
+ }
194
+
195
+ result['issues'].append(issue)
196
+
197
+ except json.JSONDecodeError:
198
+ # Handle non-JSON output (like when no files to check)
199
+ pass
200
+
201
+ # Cleanup temp file
202
+ try:
203
+ os.unlink(tmp_path)
204
+ except:
205
+ pass
206
+
207
+ except Exception as e:
208
+ if self.verbose:
209
+ print(f"Error running pylint: {str(e)}")
210
+
211
+ return result
212
+
213
+ def _run_flake8(self, target: str) -> Dict[str, Any]:
214
+ """
215
+ Run flake8 on the target file or directory.
216
+
217
+ Args:
218
+ target (str): Path to the file or directory to lint.
219
+
220
+ Returns:
221
+ Dict[str, Any]: The flake8 results.
222
+ """
223
+ result = {
224
+ 'error_count': 0,
225
+ 'warning_count': 0,
226
+ 'issues': []
227
+ }
228
+
229
+ try:
230
+ # Run flake8
231
+ cmd = [
232
+ sys.executable,
233
+ "-m",
234
+ "flake8",
235
+ target
236
+ ]
237
+
238
+ process = subprocess.run(
239
+ cmd,
240
+ stdout=subprocess.PIPE,
241
+ stderr=subprocess.PIPE,
242
+ text=True
243
+ )
244
+
245
+ if process.stdout:
246
+ # Parse flake8 output
247
+ for line in process.stdout.splitlines():
248
+ if not line.strip():
249
+ continue
250
+
251
+ try:
252
+ # Parse the line (format: "file:line:col: code message")
253
+ parts = line.split(':', 3)
254
+ if len(parts) >= 4:
255
+ file_path = parts[0]
256
+ line_num = int(parts[1])
257
+ col_num = int(parts[2].split(' ')[0])
258
+
259
+ code_message = parts[3].strip()
260
+ code_parts = code_message.split(' ', 1)
261
+
262
+ if len(code_parts) >= 2:
263
+ code = code_parts[0]
264
+ message = code_parts[1]
265
+ else:
266
+ code = "unknown"
267
+ message = code_message
268
+
269
+ # Determine severity based on error code
270
+ severity = "warning"
271
+ # E errors are generally more serious than F warnings
272
+ if code.startswith('E'):
273
+ severity = "error"
274
+ result['error_count'] += 1
275
+ else:
276
+ result['warning_count'] += 1
277
+
278
+ issue = {
279
+ 'file': file_path,
280
+ 'line': line_num,
281
+ 'column': col_num,
282
+ 'severity': severity,
283
+ 'message': message,
284
+ 'rule': code,
285
+ 'tool': 'flake8'
286
+ }
287
+
288
+ result['issues'].append(issue)
289
+ except (ValueError, IndexError):
290
+ # Skip malformed lines
291
+ continue
292
+
293
+ except Exception as e:
294
+ if self.verbose:
295
+ print(f"Error running flake8: {str(e)}")
296
+
297
+ return result
298
+
299
+ def _run_black(self, target: str, fix: bool) -> Dict[str, Any]:
300
+ """
301
+ Run black on the target file or directory to check formatting or fix it.
302
+
303
+ Args:
304
+ target (str): Path to the file or directory to format.
305
+ fix (bool): Whether to automatically fix formatting issues.
306
+
307
+ Returns:
308
+ Dict[str, Any]: The black results.
309
+ """
310
+ result = {
311
+ 'error_count': 0,
312
+ 'warning_count': 0,
313
+ 'issues': []
314
+ }
315
+
316
+ try:
317
+ # Build command
318
+ cmd = [
319
+ sys.executable,
320
+ "-m",
321
+ "black",
322
+ ]
323
+
324
+ # Check-only mode if not fixing
325
+ if not fix:
326
+ cmd.append("--check")
327
+
328
+ # Add target
329
+ cmd.append(target)
330
+
331
+ process = subprocess.run(
332
+ cmd,
333
+ stdout=subprocess.PIPE,
334
+ stderr=subprocess.PIPE,
335
+ text=True
336
+ )
337
+
338
+ # Black exit code is 0 if no changes, 1 if changes were needed
339
+ if process.returncode == 1 and not fix:
340
+ # Parse output to find which files would be reformatted
341
+ for line in process.stdout.splitlines():
342
+ if line.startswith("would reformat"):
343
+ file_path = line.replace("would reformat ", "").strip()
344
+
345
+ result['warning_count'] += 1
346
+
347
+ issue = {
348
+ 'file': file_path,
349
+ 'line': 0, # Black doesn't provide line numbers
350
+ 'column': 0,
351
+ 'severity': "warning",
352
+ 'message': "Code formatting doesn't match Black style",
353
+ 'rule': "formatting",
354
+ 'tool': 'black'
355
+ }
356
+
357
+ result['issues'].append(issue)
358
+
359
+ # If auto-fixing and Black reports changes
360
+ if fix and process.returncode == 0 and "reformatted" in process.stderr:
361
+ # This is good - it means Black fixed some issues
362
+ pass
363
+
364
+ except Exception as e:
365
+ if self.verbose:
366
+ print(f"Error running black: {str(e)}")
367
+
368
+ return result
369
+
370
+ def lint_file(self, file_path: str, fix: bool = False) -> Dict[str, Any]:
371
+ """
372
+ Lint a single Python file.
373
+
374
+ Args:
375
+ file_path (str): Path to the file to lint.
376
+ fix (bool): Whether to automatically fix fixable issues.
377
+
378
+ Returns:
379
+ Dict[str, Any]: Lint results.
380
+ """
381
+ result = {
382
+ 'success': False,
383
+ 'language': 'python',
384
+ 'files_analyzed': 1,
385
+ 'error_count': 0,
386
+ 'warning_count': 0,
387
+ 'issues': []
388
+ }
389
+
390
+ # Check if file exists
391
+ if not os.path.exists(file_path) or not os.path.isfile(file_path):
392
+ result['error'] = f"File '{file_path}' does not exist or is not a file"
393
+ return result
394
+
395
+ # Check if file is supported
396
+ if not self.is_supported_file(file_path):
397
+ result['error'] = f"Unsupported file type for '{file_path}'"
398
+ return result
399
+
400
+ # Check dependencies
401
+ if not self._check_dependencies():
402
+ # Try to install dependencies
403
+ if not self._install_dependencies_if_needed():
404
+ result['error'] = "Required dependencies are not installed and could not be installed automatically"
405
+ return result
406
+
407
+ # Run black first (to format if fix=True)
408
+ try:
409
+ black_result = self._run_black(file_path, fix)
410
+ result['issues'].extend(black_result['issues'])
411
+ result['error_count'] += black_result['error_count']
412
+ result['warning_count'] += black_result['warning_count']
413
+ except Exception as e:
414
+ if self.verbose:
415
+ print(f"Error running black: {str(e)}")
416
+
417
+ # Run pylint
418
+ try:
419
+ pylint_result = self._run_pylint(file_path)
420
+ result['issues'].extend(pylint_result['issues'])
421
+ result['error_count'] += pylint_result['error_count']
422
+ result['warning_count'] += pylint_result['warning_count']
423
+ except Exception as e:
424
+ if self.verbose:
425
+ print(f"Error running pylint: {str(e)}")
426
+
427
+ # Run flake8
428
+ try:
429
+ flake8_result = self._run_flake8(file_path)
430
+ result['issues'].extend(flake8_result['issues'])
431
+ result['error_count'] += flake8_result['error_count']
432
+ result['warning_count'] += flake8_result['warning_count']
433
+ except Exception as e:
434
+ if self.verbose:
435
+ print(f"Error running flake8: {str(e)}")
436
+
437
+ # Mark as successful
438
+ result['success'] = True
439
+
440
+ return result
441
+
442
+ def lint_project(self, project_path: str, fix: bool = False) -> Dict[str, Any]:
443
+ """
444
+ Lint a Python project.
445
+
446
+ Args:
447
+ project_path (str): Path to the project directory.
448
+ fix (bool): Whether to automatically fix fixable issues.
449
+
450
+ Returns:
451
+ Dict[str, Any]: Lint results.
452
+ """
453
+ result = {
454
+ 'success': False,
455
+ 'language': 'python',
456
+ 'files_analyzed': 0,
457
+ 'error_count': 0,
458
+ 'warning_count': 0,
459
+ 'issues': []
460
+ }
461
+
462
+ # Check if the path exists
463
+ if not os.path.exists(project_path) or not os.path.isdir(project_path):
464
+ result['error'] = f"Path '{project_path}' does not exist or is not a directory"
465
+ return result
466
+
467
+ # Check dependencies
468
+ if not self._check_dependencies():
469
+ # Try to install dependencies
470
+ if not self._install_dependencies_if_needed():
471
+ result['error'] = "Required dependencies are not installed and could not be installed automatically"
472
+ return result
473
+
474
+ # Find Python files in the project
475
+ python_files = []
476
+ for root, _, files in os.walk(project_path):
477
+ for file in files:
478
+ if file.endswith('.py'):
479
+ python_files.append(os.path.join(root, file))
480
+
481
+ result['files_analyzed'] = len(python_files)
482
+
483
+ # Run black first (to format if fix=True)
484
+ try:
485
+ black_result = self._run_black(project_path, fix)
486
+ result['issues'].extend(black_result['issues'])
487
+ result['error_count'] += black_result['error_count']
488
+ result['warning_count'] += black_result['warning_count']
489
+ except Exception as e:
490
+ if self.verbose:
491
+ print(f"Error running black: {str(e)}")
492
+
493
+ # Run pylint
494
+ try:
495
+ pylint_result = self._run_pylint(project_path)
496
+ result['issues'].extend(pylint_result['issues'])
497
+ result['error_count'] += pylint_result['error_count']
498
+ result['warning_count'] += pylint_result['warning_count']
499
+ except Exception as e:
500
+ if self.verbose:
501
+ print(f"Error running pylint: {str(e)}")
502
+
503
+ # Run flake8
504
+ try:
505
+ flake8_result = self._run_flake8(project_path)
506
+ result['issues'].extend(flake8_result['issues'])
507
+ result['error_count'] += flake8_result['error_count']
508
+ result['warning_count'] += flake8_result['warning_count']
509
+ except Exception as e:
510
+ if self.verbose:
511
+ print(f"Error running flake8: {str(e)}")
512
+
513
+ # Mark as successful
514
+ result['success'] = True
515
+
516
+ return result
517
+
518
+ def format_lint_result(self, lint_result: Dict[str, Any]) -> str:
519
+ """
520
+ Format lint results into a human-readable string.
521
+
522
+ Args:
523
+ lint_result (Dict): The lint result dictionary.
524
+
525
+ Returns:
526
+ str: A formatted string representation of the lint results.
527
+ """
528
+ if not lint_result.get('success', False):
529
+ return f"Linting failed: {lint_result.get('error', 'Unknown error')}"
530
+
531
+ header = "Python Code Lint Results"
532
+ files_analyzed = lint_result.get('files_analyzed', 0)
533
+ error_count = lint_result.get('error_count', 0)
534
+ warning_count = lint_result.get('warning_count', 0)
535
+
536
+ output = [
537
+ header,
538
+ f"{'=' * 30}",
539
+ f"Files analyzed: {files_analyzed}",
540
+ f"Errors: {error_count}",
541
+ f"Warnings: {warning_count}",
542
+ ""
543
+ ]
544
+
545
+ if error_count == 0 and warning_count == 0:
546
+ output.append("No issues found. Great job!")
547
+ else:
548
+ output.append("Issues:")
549
+ output.append(f"{'-' * 30}")
550
+
551
+ # Group issues by file
552
+ issues_by_file = {}
553
+ for issue in lint_result.get('issues', []):
554
+ file_path = issue.get('file', '')
555
+ if file_path not in issues_by_file:
556
+ issues_by_file[file_path] = []
557
+ issues_by_file[file_path].append(issue)
558
+
559
+ # Display issues grouped by file
560
+ for file_path, issues in issues_by_file.items():
561
+ output.append(f"\nFile: {file_path}")
562
+
563
+ for issue in issues:
564
+ severity = issue.get('severity', '').upper()
565
+ line = issue.get('line', 0)
566
+ column = issue.get('column', 0)
567
+ message = issue.get('message', '')
568
+ rule = issue.get('rule', 'unknown')
569
+ tool = issue.get('tool', 'unknown')
570
+
571
+ output.append(f" [{severity}] Line {line}, Col {column}: {message} ({rule} - {tool})")
572
+
573
+ return "\n".join(output)
574
+
575
+ def lint_python_file(file_path: str, fix: bool = False, verbose: bool = False) -> Dict[str, Any]:
576
+ """
577
+ Utility function to lint a single Python file.
578
+
579
+ Args:
580
+ file_path (str): Path to the file to lint.
581
+ fix (bool): Whether to automatically fix fixable issues.
582
+ verbose (bool): Whether to display verbose output.
583
+
584
+ Returns:
585
+ Dict[str, Any]: A dictionary containing lint results.
586
+ """
587
+ linter = PythonLinter(verbose=verbose)
588
+ return linter.lint_file(file_path, fix=fix)
589
+
590
+ def lint_python_project(project_path: str, fix: bool = False, verbose: bool = False) -> Dict[str, Any]:
591
+ """
592
+ Utility function to lint a Python project.
593
+
594
+ Args:
595
+ project_path (str): Path to the project directory.
596
+ fix (bool): Whether to automatically fix fixable issues.
597
+ verbose (bool): Whether to display verbose output.
598
+
599
+ Returns:
600
+ Dict[str, Any]: A dictionary containing lint results.
601
+ """
602
+ linter = PythonLinter(verbose=verbose)
603
+ return linter.lint_project(project_path, fix=fix)
604
+
605
+ def format_lint_result(lint_result: Dict[str, Any]) -> str:
606
+ """
607
+ Format lint results into a human-readable string.
608
+
609
+ Args:
610
+ lint_result (Dict): The lint result dictionary.
611
+
612
+ Returns:
613
+ str: A formatted string representation of the lint results.
614
+ """
615
+ linter = PythonLinter()
616
+ return linter.format_lint_result(lint_result)
autocoder/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.314"
1
+ __version__ = "0.1.315"