From 210ba807d7d94119b7668cfc63ef2dcdc43b577d Mon Sep 17 00:00:00 2001 From: Alan Su Date: Tue, 14 Jul 2026 03:32:55 -0400 Subject: [PATCH 1/4] Implement initial PyTA functionality --- package.json | 12 +++ pnpm-workspace.yaml | 3 + src/extension.ts | 194 ++++++++++++++++++++++++++++++++++++-------- 3 files changed, 177 insertions(+), 32 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/package.json b/package.json index 52a08cb..bf1218f 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,13 @@ "vsce-package": "vsce package -o python-ta.vsix" }, "contributes": { + "keybindings": [ + { + "command": "python-ta.check", + "key": "ctrl+shift+t", + "when": "editorTextFocus && editorLangId == 'python'" + } + ], "configuration": { "properties": { "python-ta.cwd": { @@ -138,6 +145,11 @@ "title": "Restart Server", "category": "PythonTA VS Code Extension", "command": "python-ta.restart" + }, + { + "title": "Run PythonTA", + "category": "PythonTA VS Code Extension", + "command": "python-ta.check" } ] }, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..02de15a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@vscode/vsce-sign': true + keytar: true diff --git a/src/extension.ts b/src/extension.ts index 3048325..bbbcef4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,9 +15,16 @@ import { LS_SERVER_RESTART_DELAY } from './common/constants'; import { getLSClientTraceLevel } from './common/utilities'; import { createOutputChannel, onDidChangeConfiguration, registerCommand } from './common/vscodeapi'; +import { spawn } from 'child_process'; +import * as path from 'path'; + let lsClient: LanguageClient | undefined; let isRestarting = false; let restartTimer: NodeJS.Timeout | undefined; + +let diagnosticCollection: vscode.DiagnosticCollection; +let statusBarItem: vscode.StatusBarItem; + export async function activate(context: vscode.ExtensionContext): Promise { // This is required to get server name and module. This should be // the first thing that we do in this extension. @@ -43,46 +50,57 @@ export async function activate(context: vscode.ExtensionContext): Promise }), ); + diagnosticCollection = vscode.languages.createDiagnosticCollection('python-ta'); + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); + + context.subscriptions.push( + diagnosticCollection, + statusBarItem, + vscode.commands.registerCommand(`${serverId}.check`, runPythonTA) + ); + // Log Server information traceLog(`Name: ${serverInfo.name}`); traceLog(`Module: ${serverInfo.module}`); traceVerbose(`Full Server Info: ${JSON.stringify(serverInfo)}`); const runServer = async () => { - if (isRestarting) { - if (restartTimer) { - clearTimeout(restartTimer); - } - restartTimer = setTimeout(runServer, LS_SERVER_RESTART_DELAY); - return; - } - isRestarting = true; - try { - const interpreter = getInterpreterFromSetting(serverId); - if (interpreter && interpreter.length > 0) { - if (checkVersion(await resolveInterpreter(interpreter))) { - traceVerbose(`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(' ')}`); - lsClient = await restartServer(serverId, serverName, outputChannel, lsClient); - } - return; - } + // TODO: Comment back this code when LSP is ready to be implemented - const interpreterDetails = await getInterpreterDetails(); - if (interpreterDetails.path) { - traceVerbose(`Using interpreter from Python extension: ${interpreterDetails.path.join(' ')}`); - lsClient = await restartServer(serverId, serverName, outputChannel, lsClient); - return; - } + // if (isRestarting) { + // if (restartTimer) { + // clearTimeout(restartTimer); + // } + // restartTimer = setTimeout(runServer, LS_SERVER_RESTART_DELAY); + // return; + // } + // isRestarting = true; + // try { + // const interpreter = getInterpreterFromSetting(serverId); + // if (interpreter && interpreter.length > 0) { + // if (checkVersion(await resolveInterpreter(interpreter))) { + // traceVerbose(`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(' ')}`); + // lsClient = await restartServer(serverId, serverName, outputChannel, lsClient); + // } + // return; + // } - traceError( - 'Python interpreter missing:\r\n' + - '[Option 1] Select python interpreter using the ms-python.python.\r\n' + - `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` + - 'Please use Python 3.10 or greater.', - ); - } finally { - isRestarting = false; - } + // const interpreterDetails = await getInterpreterDetails(); + // if (interpreterDetails.path) { + // traceVerbose(`Using interpreter from Python extension: ${interpreterDetails.path.join(' ')}`); + // lsClient = await restartServer(serverId, serverName, outputChannel, lsClient); + // return; + // } + + // traceError( + // 'Python interpreter missing:\r\n' + + // '[Option 1] Select python interpreter using the ms-python.python.\r\n' + + // `[Option 2] Set an interpreter using "${serverId}.interpreter" setting.\r\n` + + // 'Please use Python 3.10 or greater.', + // ); + // } finally { + // isRestarting = false; + // } }; context.subscriptions.push( @@ -120,3 +138,115 @@ export async function deactivate(): Promise { } } } + +async function runPythonTA(): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor || editor.document.languageId !== 'python') { + vscode.window.showWarningMessage('PythonTA: Open a Python file first.'); + return; + } + + if (editor.document.isUntitled) { + vscode.window.showWarningMessage('PythonTA: Please save the file before running the linter.'); + return; + } + + const filePath = editor.document.uri.fsPath; + + statusBarItem.text = '$(loading~spin) Running PythonTA...'; + statusBarItem.show(); + + const serverId = 'python-ta'; + let pythonPath: string | undefined; + + const settingsInterpreter = getInterpreterFromSetting(serverId); + if (settingsInterpreter && settingsInterpreter.length > 0) { + pythonPath = settingsInterpreter[0]; + } + else { + const interpreterDetails = await getInterpreterDetails(editor.document.uri); + if (interpreterDetails.path && interpreterDetails.path.length > 0) { + pythonPath = interpreterDetails.path[0]; + } + } + + const python = pythonPath || 'python'; + + const env = Object.assign({}, process.env); + if (python !== 'python') { + const pythonDir = path.dirname(python); + const venvDir = path.dirname(pythonDir); + env.PATH = `${pythonDir}${path.delimiter}${env.PATH || ''}`; + env.VIRTUAL_ENV = venvDir; + delete env.PYTHONHOME; + } + + const args = ['-m', 'python_ta', '--output-format', 'pyta-lsp', filePath]; + + const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri); + const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + + const proc = spawn(python, args, { cwd: cwd, env: env }); + + let stdout = ''; + let stderr = ''; + let spawnFailed = false; + + proc.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); + proc.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); + + proc.on('error', () => { + spawnFailed = true; + statusBarItem.hide(); + vscode.window.showErrorMessage(`PythonTA: Failed to start process. Could not find executable '${python}'.`); + }); + + proc.on('close', (code: number | null) => { + statusBarItem.hide(); + + if (spawnFailed) { + return; + } + + if (code !== 0 && stdout.trim() === '') { + const detail = stderr.trim() ? `\nDetails: ${stderr.trim()}` : ''; + vscode.window.showErrorMessage(`PythonTA: Process failed (exit ${code}).${detail}`); + return; + } + + let results: any[]; + try { + results = JSON.parse(stdout); + } catch { + if (stdout.includes('[INFO] Your PythonTA report is being opened')) { + vscode.window.showInformationMessage('PythonTA generated a web report instead of LSP data.'); + diagnosticCollection.set(editor.document.uri, []); + } else { + vscode.window.showErrorMessage('PythonTA: Received non-JSON output.'); + console.error("Raw Output:", stdout, stderr); + } + return; + } + + diagnosticCollection.set(editor.document.uri, []); + for (const { uri, diagnostics } of results) { + const vscodeDiags = diagnostics.map((d: any) => { + const diag = new vscode.Diagnostic( + new vscode.Range( + d.range.start.line, d.range.start.character, + d.range.end.line, d.range.end.character + ), + d.message, + d.severity === 1 ? vscode.DiagnosticSeverity.Error : + d.severity === 2 ? vscode.DiagnosticSeverity.Warning : + d.severity === 3 ? vscode.DiagnosticSeverity.Information : + vscode.DiagnosticSeverity.Hint + ); + diag.code = d.code; + diag.source = d.source ?? 'python-ta'; + return diag; + }); + diagnosticCollection.set(vscode.Uri.parse(uri), vscodeDiags); + } + }); +} \ No newline at end of file From 760785d19f77c5bc694a25200bdcf377f4dba293 Mon Sep 17 00:00:00 2001 From: Alan Su Date: Tue, 14 Jul 2026 03:37:11 -0400 Subject: [PATCH 2/4] Update the changelog with initial implementation additions --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db769b6..58846c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### ✨ New features and improvements +- Add initial PythonTA implementation that simply calls the CLI + ### 🐛 Bug fixes ### 🔧 Internal changes From b9743a18d99245afe155573e48a09c7daf7a6781 Mon Sep 17 00:00:00 2001 From: Alan Su Date: Tue, 14 Jul 2026 03:42:56 -0400 Subject: [PATCH 3/4] Add configuration support --- src/extension.ts | 47 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index bbbcef4..1cb0798 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,6 +24,34 @@ let restartTimer: NodeJS.Timeout | undefined; let diagnosticCollection: vscode.DiagnosticCollection; let statusBarItem: vscode.StatusBarItem; +const serverId = 'python-ta'; + +interface LspRange { + start: { line: number; character: number }; + end: { line: number; character: number }; +} + +interface LspDiagnostic { + range: LspRange; + message: string; + severity: number; + code?: string; + source?: string; +} + +interface PublishDiagnosticsParams { + uri: string; + diagnostics: LspDiagnostic[]; +} + +function lspSeverityToVscode(severity: number): vscode.DiagnosticSeverity { + switch (severity) { + case 1: return vscode.DiagnosticSeverity.Error; + case 3: return vscode.DiagnosticSeverity.Information; + case 4: return vscode.DiagnosticSeverity.Hint; + default: return vscode.DiagnosticSeverity.Warning; + } +} export async function activate(context: vscode.ExtensionContext): Promise { // This is required to get server name and module. This should be @@ -162,8 +190,7 @@ async function runPythonTA(): Promise { const settingsInterpreter = getInterpreterFromSetting(serverId); if (settingsInterpreter && settingsInterpreter.length > 0) { pythonPath = settingsInterpreter[0]; - } - else { + } else { const interpreterDetails = await getInterpreterDetails(editor.document.uri); if (interpreterDetails.path && interpreterDetails.path.length > 0) { pythonPath = interpreterDetails.path[0]; @@ -181,7 +208,12 @@ async function runPythonTA(): Promise { delete env.PYTHONHOME; } + const configPath = vscode.workspace.getConfiguration('python-ta').get('configPath'); const args = ['-m', 'python_ta', '--output-format', 'pyta-lsp', filePath]; + + if (configPath) { + args.push('--config', configPath); + } const workspaceFolder = vscode.workspace.getWorkspaceFolder(editor.document.uri); const cwd = workspaceFolder ? workspaceFolder.uri.fsPath : undefined; @@ -214,9 +246,9 @@ async function runPythonTA(): Promise { return; } - let results: any[]; + let results: PublishDiagnosticsParams[]; try { - results = JSON.parse(stdout); + results = JSON.parse(stdout) as PublishDiagnosticsParams[]; } catch { if (stdout.includes('[INFO] Your PythonTA report is being opened')) { vscode.window.showInformationMessage('PythonTA generated a web report instead of LSP data.'); @@ -230,17 +262,14 @@ async function runPythonTA(): Promise { diagnosticCollection.set(editor.document.uri, []); for (const { uri, diagnostics } of results) { - const vscodeDiags = diagnostics.map((d: any) => { + const vscodeDiags = diagnostics.map((d) => { const diag = new vscode.Diagnostic( new vscode.Range( d.range.start.line, d.range.start.character, d.range.end.line, d.range.end.character ), d.message, - d.severity === 1 ? vscode.DiagnosticSeverity.Error : - d.severity === 2 ? vscode.DiagnosticSeverity.Warning : - d.severity === 3 ? vscode.DiagnosticSeverity.Information : - vscode.DiagnosticSeverity.Hint + lspSeverityToVscode(d.severity) ); diag.code = d.code; diag.source = d.source ?? 'python-ta'; From 356c681c6b4dd7a36b8760561de1e30d50de7dd6 Mon Sep 17 00:00:00 2001 From: Alan Su Date: Tue, 14 Jul 2026 04:00:36 -0400 Subject: [PATCH 4/4] Update package to allow for configPath --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index bf1218f..578b199 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,12 @@ ], "configuration": { "properties": { + "python-ta.configPath": { + "type": "string", + "default": "", + "description": "Absolute or relative path to your PythonTA configuration file (e.g., .pyta_config.ini).", + "scope": "resource" + }, "python-ta.cwd": { "default": "${workspaceFolder}", "description": "Sets the working directory for python-ta. Supported variables: `${workspaceFolder}` (workspace root) and `${fileDirname}` (directory of the current file).",