|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Python Command Executor Module for PythonCommandRunner |
| 4 | + * |
| 5 | + * Command execution methods: runTests, runLinter, runFormatter, runTypeChecker, manageDependencies, runSetup |
| 6 | + */ |
| 7 | + |
| 8 | +const path = require('path'); |
| 9 | +const fs = require('fs'); |
| 10 | +const { LoggingUtils } = require('../../lib'); |
| 11 | + |
| 12 | +class PythonCommandExecutor { |
| 13 | + constructor(projectPath, pythonConfig, initializer, projectAnalyzer, coreExecutor) { |
| 14 | + this.projectPath = projectPath; |
| 15 | + this.pythonConfig = pythonConfig; |
| 16 | + this.initializer = initializer; |
| 17 | + this.projectAnalyzer = projectAnalyzer; |
| 18 | + this.coreExecutor = coreExecutor; |
| 19 | + } |
| 20 | + |
| 21 | + /** |
| 22 | + * Run tests with configured test runner |
| 23 | + */ |
| 24 | + async runTests(options = {}) { |
| 25 | + await this.initializer.initialize(); |
| 26 | + |
| 27 | + const testRunner = this.pythonConfig.testRunner || 'pytest'; |
| 28 | + await this.initializer.checkTool(testRunner); |
| 29 | + |
| 30 | + // Log test information |
| 31 | + const projectInfo = this.projectAnalyzer.getPythonProjectInfo(); |
| 32 | + if (projectInfo) { |
| 33 | + LoggingUtils.debug(`Python files: ${projectInfo.pythonFiles}`); |
| 34 | + } |
| 35 | + |
| 36 | + const args = []; |
| 37 | + |
| 38 | + // Add coverage if requested |
| 39 | + if (options.coverage) { |
| 40 | + if (testRunner === 'pytest') { |
| 41 | + args.push('--cov=.', '--cov-report=term', '--cov-report=html'); |
| 42 | + LoggingUtils.info('📊 Coverage reporting enabled'); |
| 43 | + } |
| 44 | + } |
| 45 | + |
| 46 | + // Add verbose flag |
| 47 | + if (options.verbose) { |
| 48 | + args.push('-v'); |
| 49 | + LoggingUtils.debug('Verbose mode enabled'); |
| 50 | + } |
| 51 | + |
| 52 | + // Add specific test file |
| 53 | + if (options.file) { |
| 54 | + args.push(options.file); |
| 55 | + LoggingUtils.debug(`Testing specific file: ${options.file}`); |
| 56 | + } |
| 57 | + |
| 58 | + // Add test name pattern |
| 59 | + if (options.test) { |
| 60 | + if (testRunner === 'pytest') { |
| 61 | + args.push('-k', options.test); |
| 62 | + LoggingUtils.debug(`Test pattern: ${options.test}`); |
| 63 | + } else if (testRunner === 'unittest') { |
| 64 | + args.push(options.test); |
| 65 | + LoggingUtils.debug(`Test pattern: ${options.test}`); |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + // Log test configuration |
| 70 | + LoggingUtils.info(`Running tests with ${testRunner}...`); |
| 71 | + |
| 72 | + // Execute test runner |
| 73 | + if (testRunner === 'pytest') { |
| 74 | + return this.coreExecutor.executeCommand('pytest', args); |
| 75 | + } else if (testRunner === 'unittest') { |
| 76 | + return this.coreExecutor.executePythonModule('unittest', args); |
| 77 | + } else { |
| 78 | + throw new Error(`Unsupported test runner: ${testRunner}`); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * Run linter with configured tool |
| 84 | + */ |
| 85 | + async runLinter(options = {}) { |
| 86 | + await this.initializer.initialize(); |
| 87 | + |
| 88 | + const linter = this.pythonConfig.linter || 'ruff'; |
| 89 | + await this.initializer.checkTool(linter); |
| 90 | + |
| 91 | + // Log linter information |
| 92 | + LoggingUtils.info(`Running ${linter}...`); |
| 93 | + if (options.fix) { |
| 94 | + LoggingUtils.debug('Fix mode enabled'); |
| 95 | + } |
| 96 | + |
| 97 | + const args = []; |
| 98 | + |
| 99 | + // Check or fix mode |
| 100 | + if (options.fix) { |
| 101 | + if (linter === 'ruff') { |
| 102 | + args.push('check', '--fix'); |
| 103 | + } else if (linter === 'flake8') { |
| 104 | + // flake8 doesn't have fix mode |
| 105 | + args.push('.'); |
| 106 | + LoggingUtils.warn("flake8 doesn't support auto-fix mode"); |
| 107 | + } else if (linter === 'pylint') { |
| 108 | + args.push('.'); |
| 109 | + LoggingUtils.warn("pylint doesn't support auto-fix mode"); |
| 110 | + } |
| 111 | + } else { |
| 112 | + if (linter === 'ruff') { |
| 113 | + args.push('check'); |
| 114 | + } else { |
| 115 | + args.push('.'); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + // Add specific file |
| 120 | + if (options.file) { |
| 121 | + args.push(options.file); |
| 122 | + LoggingUtils.debug(`Linting specific file: ${options.file}`); |
| 123 | + } |
| 124 | + |
| 125 | + // Add exclude patterns |
| 126 | + if (options.exclude) { |
| 127 | + if (linter === 'ruff') { |
| 128 | + args.push('--exclude', options.exclude); |
| 129 | + } else if (linter === 'flake8') { |
| 130 | + args.push('--exclude', options.exclude); |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + // Execute linter |
| 135 | + return this.coreExecutor.executeCommand(linter, args); |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Run formatter with configured tool |
| 140 | + */ |
| 141 | + async runFormatter(options = {}) { |
| 142 | + await this.initializer.initialize(); |
| 143 | + |
| 144 | + const formatter = this.pythonConfig.formatter || 'black'; |
| 145 | + await this.initializer.checkTool(formatter); |
| 146 | + |
| 147 | + // Log formatter information |
| 148 | + LoggingUtils.info(`Running ${formatter}...`); |
| 149 | + if (options.check) { |
| 150 | + LoggingUtils.debug('Check mode (no changes)'); |
| 151 | + } |
| 152 | + |
| 153 | + const args = []; |
| 154 | + |
| 155 | + // Check or format mode |
| 156 | + if (options.check) { |
| 157 | + if (formatter === 'black') { |
| 158 | + args.push('--check'); |
| 159 | + } else if (formatter === 'isort') { |
| 160 | + args.push('--check-only'); |
| 161 | + } else if (formatter === 'yapf') { |
| 162 | + args.push('--diff'); |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + // Add specific file or directory |
| 167 | + if (options.file) { |
| 168 | + args.push(options.file); |
| 169 | + LoggingUtils.debug(`Formatting specific file: ${options.file}`); |
| 170 | + } else { |
| 171 | + args.push('.'); |
| 172 | + } |
| 173 | + |
| 174 | + // Add line length if specified |
| 175 | + if (options.lineLength) { |
| 176 | + if (formatter === 'black') { |
| 177 | + args.push('--line-length', options.lineLength.toString()); |
| 178 | + } else if (formatter === 'yapf') { |
| 179 | + args.push('--style', `{based_on_style: pep8, column_limit: ${options.lineLength}}`); |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + // Execute formatter |
| 184 | + return this.coreExecutor.executeCommand(formatter, args); |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * Run type checker with configured tool |
| 189 | + */ |
| 190 | + async runTypeChecker(options = {}) { |
| 191 | + await this.initializer.initialize(); |
| 192 | + |
| 193 | + const typeChecker = this.pythonConfig.typeChecker || 'mypy'; |
| 194 | + await this.initializer.checkTool(typeChecker); |
| 195 | + |
| 196 | + // Log type checker information |
| 197 | + LoggingUtils.info(`Running ${typeChecker}...`); |
| 198 | + |
| 199 | + const args = []; |
| 200 | + |
| 201 | + // Add specific file or directory |
| 202 | + if (options.file) { |
| 203 | + args.push(options.file); |
| 204 | + LoggingUtils.debug(`Type checking specific file: ${options.file}`); |
| 205 | + } else { |
| 206 | + args.push('.'); |
| 207 | + } |
| 208 | + |
| 209 | + // Add strict mode |
| 210 | + if (options.strict) { |
| 211 | + if (typeChecker === 'mypy') { |
| 212 | + args.push('--strict'); |
| 213 | + } else if (typeChecker === 'pyright') { |
| 214 | + args.push('--lib'); |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + // Add config file if specified |
| 219 | + if (options.config) { |
| 220 | + if (typeChecker === 'mypy') { |
| 221 | + args.push('--config-file', options.config); |
| 222 | + } else if (typeChecker === 'pyright') { |
| 223 | + args.push('--config', options.config); |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + // Execute type checker |
| 228 | + return this.coreExecutor.executeCommand(typeChecker, args); |
| 229 | + } |
| 230 | + |
| 231 | + /** |
| 232 | + * Manage Python dependencies |
| 233 | + */ |
| 234 | + async manageDependencies(action, packages = [], options = {}) { |
| 235 | + await this.initializer.initialize(); |
| 236 | + |
| 237 | + const packageManager = this.pythonConfig.packageManager || 'pip'; |
| 238 | + await this.initializer.checkTool(packageManager); |
| 239 | + |
| 240 | + // Log dependency management information |
| 241 | + LoggingUtils.info(`Managing dependencies with ${packageManager}...`); |
| 242 | + |
| 243 | + const args = []; |
| 244 | + |
| 245 | + // Handle different actions |
| 246 | + switch (action) { |
| 247 | + case 'install': |
| 248 | + args.push('install'); |
| 249 | + if (packages.length === 0) { |
| 250 | + // Install from requirements file |
| 251 | + if (this.projectAnalyzer.hasFile('requirements.txt')) { |
| 252 | + args.push('-r', 'requirements.txt'); |
| 253 | + LoggingUtils.debug('Installing from requirements.txt'); |
| 254 | + } else if (this.projectAnalyzer.hasFile('pyproject.toml')) { |
| 255 | + if (packageManager === 'poetry') { |
| 256 | + args.push('--no-root'); |
| 257 | + } else if (packageManager === 'pip') { |
| 258 | + args.push('.'); |
| 259 | + } |
| 260 | + } |
| 261 | + } else { |
| 262 | + // Install specific packages |
| 263 | + args.push(...packages); |
| 264 | + LoggingUtils.debug(`Installing packages: ${packages.join(', ')}`); |
| 265 | + } |
| 266 | + break; |
| 267 | + |
| 268 | + case 'uninstall': |
| 269 | + args.push('uninstall', ...packages); |
| 270 | + LoggingUtils.debug(`Uninstalling packages: ${packages.join(', ')}`); |
| 271 | + break; |
| 272 | + |
| 273 | + case 'update': |
| 274 | + args.push('update'); |
| 275 | + if (packages.length > 0) { |
| 276 | + args.push(...packages); |
| 277 | + LoggingUtils.debug(`Updating packages: ${packages.join(', ')}`); |
| 278 | + } else { |
| 279 | + LoggingUtils.debug('Updating all packages'); |
| 280 | + } |
| 281 | + break; |
| 282 | + |
| 283 | + case 'list': |
| 284 | + args.push('list'); |
| 285 | + break; |
| 286 | + |
| 287 | + case 'show': |
| 288 | + if (packages.length > 0) { |
| 289 | + args.push('show', ...packages); |
| 290 | + LoggingUtils.debug(`Showing info for: ${packages.join(', ')}`); |
| 291 | + } else { |
| 292 | + throw new Error('Package name required for show action'); |
| 293 | + } |
| 294 | + break; |
| 295 | + |
| 296 | + default: |
| 297 | + throw new Error(`Unknown dependency action: ${action}`); |
| 298 | + } |
| 299 | + |
| 300 | + // Add additional options |
| 301 | + if (options.dev && packageManager === 'poetry') { |
| 302 | + args.push('--dev'); |
| 303 | + } |
| 304 | + |
| 305 | + if (options.noCache && packageManager === 'pip') { |
| 306 | + args.push('--no-cache-dir'); |
| 307 | + } |
| 308 | + |
| 309 | + // Execute package manager |
| 310 | + if (packageManager === 'pip') { |
| 311 | + return this.coreExecutor.executePythonModule('pip', args); |
| 312 | + } else { |
| 313 | + return this.coreExecutor.executeCommand(packageManager, args); |
| 314 | + } |
| 315 | + } |
| 316 | + |
| 317 | + /** |
| 318 | + * Run project setup |
| 319 | + */ |
| 320 | + async runSetup(options = {}) { |
| 321 | + await this.initializer.initialize(); |
| 322 | + |
| 323 | + LoggingUtils.info('Setting up Python project...'); |
| 324 | + |
| 325 | + const setupTasks = []; |
| 326 | + |
| 327 | + // Check Python installation |
| 328 | + try { |
| 329 | + const python = this.projectAnalyzer.getPythonExecutable(); |
| 330 | + setupTasks.push(`✓ Python executable: ${python}`); |
| 331 | + } catch (error) { |
| 332 | + setupTasks.push(`✗ Python not found: ${error.message}`); |
| 333 | + throw error; |
| 334 | + } |
| 335 | + |
| 336 | + // Check virtual environment |
| 337 | + const venvPath = path.join(this.projectPath, 'venv'); |
| 338 | + if (!fs.existsSync(venvPath) && options.createVenv) { |
| 339 | + setupTasks.push('Creating virtual environment...'); |
| 340 | + await this.coreExecutor.executePythonModule('venv', ['venv']); |
| 341 | + setupTasks.push('✓ Virtual environment created'); |
| 342 | + } else if (fs.existsSync(venvPath)) { |
| 343 | + setupTasks.push('✓ Virtual environment found'); |
| 344 | + } |
| 345 | + |
| 346 | + // Install dependencies |
| 347 | + if (options.installDeps) { |
| 348 | + setupTasks.push('Installing dependencies...'); |
| 349 | + await this.manageDependencies('install', [], {}); |
| 350 | + setupTasks.push('✓ Dependencies installed'); |
| 351 | + } |
| 352 | + |
| 353 | + // Run initial tests |
| 354 | + if (options.runTests) { |
| 355 | + setupTasks.push('Running initial tests...'); |
| 356 | + try { |
| 357 | + await this.runTests({ verbose: false }); |
| 358 | + setupTasks.push('✓ Tests passed'); |
| 359 | + } catch (error) { |
| 360 | + setupTasks.push(`⚠ Tests failed: ${error.message}`); |
| 361 | + if (!options.force) { |
| 362 | + throw error; |
| 363 | + } |
| 364 | + } |
| 365 | + } |
| 366 | + |
| 367 | + // Log setup summary |
| 368 | + LoggingUtils.info('Setup completed:'); |
| 369 | + setupTasks.forEach((task) => { |
| 370 | + if (task.startsWith('✓')) { |
| 371 | + LoggingUtils.success(task); |
| 372 | + } else if (task.startsWith('✗')) { |
| 373 | + LoggingUtils.error(task); |
| 374 | + } else if (task.startsWith('⚠')) { |
| 375 | + LoggingUtils.warn(task); |
| 376 | + } else { |
| 377 | + LoggingUtils.info(task); |
| 378 | + } |
| 379 | + }); |
| 380 | + |
| 381 | + return { success: true, tasks: setupTasks }; |
| 382 | + } |
| 383 | +} |
| 384 | + |
| 385 | +module.exports = PythonCommandExecutor; |
0 commit comments