From 520e4ef34543b96bdd70d7fce6740cbaf77dea2f Mon Sep 17 00:00:00 2001 From: inimaz <93inigo93@gmail.com> Date: Tue, 31 Mar 2026 18:32:55 +0200 Subject: [PATCH 1/4] fix: mac/linux default python is python3 --- package.json | 2 +- src/utils/configHelpers.ts | 5 ++++- tests/ts/configService.test.ts | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 052addb..4e7c4bb 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ }, "codecarbon.interpreter": { "default": "", - "description": "Python executable path. Leave empty to use system default.", + "description": "Python executable path. Leave empty to use platform default (python3 on macOS/Linux, python on Windows).", "type": "string" }, "codecarbon.autoInstall": { diff --git a/src/utils/configHelpers.ts b/src/utils/configHelpers.ts index 1e1027f..2ac2926 100644 --- a/src/utils/configHelpers.ts +++ b/src/utils/configHelpers.ts @@ -4,7 +4,10 @@ export function resolvePythonPath(interpreter: unknown): string { if (typeof interpreter === 'string' && interpreter.trim()) { return interpreter.trim(); } - return 'python'; + if (process.platform === 'win32') { + return 'python'; + } + return 'python3'; } export function resolveLaunchOnStartup( diff --git a/tests/ts/configService.test.ts b/tests/ts/configService.test.ts index f4725d9..b282979 100644 --- a/tests/ts/configService.test.ts +++ b/tests/ts/configService.test.ts @@ -6,9 +6,10 @@ test('resolvePythonPath returns configured interpreter string', () => { assert.equal(resolvePythonPath('/usr/local/bin/python3'), '/usr/local/bin/python3'); }); -test('resolvePythonPath falls back to python when empty', () => { - assert.equal(resolvePythonPath(''), 'python'); - assert.equal(resolvePythonPath(undefined), 'python'); +test('resolvePythonPath falls back to platform default when empty', () => { + const expected = process.platform === 'win32' ? 'python' : 'python3'; + assert.equal(resolvePythonPath(''), expected); + assert.equal(resolvePythonPath(undefined), expected); }); test('resolveLaunchOnStartup uses config getter default', () => { From 1ea6654c619726f059e9311dd93c3f0a6adbd080 Mon Sep 17 00:00:00 2001 From: inimaz <93inigo93@gmail.com> Date: Wed, 1 Apr 2026 21:56:01 +0200 Subject: [PATCH 2/4] fix: improve no python handling --- package.json | 2 +- src/services/interpreterCandidates.ts | 43 +++++++++++++ src/services/interpreterResolver.ts | 83 ++++++++++++++++++++++++++ src/services/interpreterSelection.ts | 11 ++++ src/services/pythonService.ts | 64 ++++++++++++++------ src/services/trackerService.ts | 6 +- src/utils/config.ts | 11 ++++ tests/ts/interpreterCandidates.test.ts | 21 +++++++ tests/ts/interpreterSelection.test.ts | 33 ++++++++++ tsconfig.json | 1 + tsconfig.tests.json | 1 + 11 files changed, 254 insertions(+), 22 deletions(-) create mode 100644 src/services/interpreterCandidates.ts create mode 100644 src/services/interpreterResolver.ts create mode 100644 src/services/interpreterSelection.ts create mode 100644 tests/ts/interpreterCandidates.test.ts create mode 100644 tests/ts/interpreterSelection.test.ts diff --git a/package.json b/package.json index 4e7c4bb..91a88f0 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ }, "codecarbon.interpreter": { "default": "", - "description": "Python executable path. Leave empty to use platform default (python3 on macOS/Linux, python on Windows).", + "description": "Python executable path. Leave empty to auto-discover from Python extension (if available), then system commands (python3/python on macOS/Linux, python/py on Windows).", "type": "string" }, "codecarbon.autoInstall": { diff --git a/src/services/interpreterCandidates.ts b/src/services/interpreterCandidates.ts new file mode 100644 index 0000000..73f62d3 --- /dev/null +++ b/src/services/interpreterCandidates.ts @@ -0,0 +1,43 @@ +export function buildInterpreterCandidates( + configuredPath: string, + pythonExtensionPath: string | undefined, + preferPythonExtension: boolean, +): string[] { + const seen = new Set(); + const candidates: string[] = []; + + const add = (value: string | undefined): void => { + const normalized = (value ?? '').trim(); + if (!normalized || seen.has(normalized)) { + return; + } + seen.add(normalized); + candidates.push(normalized); + }; + + if (preferPythonExtension) { + add(pythonExtensionPath); + add(configuredPath); + } else { + add(configuredPath); + add(pythonExtensionPath); + } + + if (process.platform === 'win32') { + if (seen.has('python')) { + add('py'); + } + if (seen.has('py')) { + add('python'); + } + return candidates; + } + + if (seen.has('python3')) { + add('python'); + } + if (seen.has('python')) { + add('python3'); + } + return candidates; +} diff --git a/src/services/interpreterResolver.ts b/src/services/interpreterResolver.ts new file mode 100644 index 0000000..d054a1d --- /dev/null +++ b/src/services/interpreterResolver.ts @@ -0,0 +1,83 @@ +import * as vscode from 'vscode'; +import { LogService } from './logService'; +import { buildInterpreterCandidates } from './interpreterCandidates'; + +type PythonEnvironmentApi = { + getActiveEnvironmentPath?: (resource?: vscode.Uri) => Promise; +}; + +type PythonExtensionApi = { + environments?: PythonEnvironmentApi; +}; + +export class InterpreterResolver { + private logService: LogService; + + constructor() { + this.logService = LogService.getInstance(); + } + + public async getCandidates(configuredPath: string, preferPythonExtension: boolean): Promise { + const pythonExtensionPath = await this.resolveFromPythonExtension(); + const candidates = buildInterpreterCandidates(configuredPath, pythonExtensionPath, preferPythonExtension); + + this.logService.log(`Interpreter candidates: ${candidates.join(', ')}`); + return candidates; + } + + private async resolveFromPythonExtension(): Promise { + const extension = vscode.extensions.getExtension('ms-python.python'); + if (!extension) { + return undefined; + } + + const workspaceUri = vscode.workspace.workspaceFolders?.[0]?.uri; + try { + const api = (await extension.activate()) as PythonExtensionApi; + const active = await api.environments?.getActiveEnvironmentPath?.(workspaceUri); + const value = this.readPathLikeValue(active); + if (value) { + this.logService.log(`Interpreter resolved from Python extension API: ${value}`); + return value; + } + } catch (error) { + this.logService.logWarning(`Python extension API interpreter resolution failed: ${String(error)}`); + } + + const config = vscode.workspace.getConfiguration('python', workspaceUri); + const settingPath = this.cleanConfiguredPath( + config.get('defaultInterpreterPath') ?? config.get('pythonPath'), + ); + if (settingPath) { + this.logService.log(`Interpreter resolved from Python extension settings: ${settingPath}`); + return settingPath; + } + return undefined; + } + + private readPathLikeValue(value: unknown): string | undefined { + if (typeof value === 'string') { + return this.cleanConfiguredPath(value); + } + if (!value || typeof value !== 'object') { + return undefined; + } + const record = value as Record; + if (typeof record.path === 'string') { + return this.cleanConfiguredPath(record.path); + } + return undefined; + } + + private cleanConfiguredPath(value: string | undefined): string | undefined { + const normalized = (value ?? '').trim(); + if (!normalized) { + return undefined; + } + // Ignore unresolved command variables from settings. + if (normalized.startsWith('${command:')) { + return undefined; + } + return normalized; + } +} diff --git a/src/services/interpreterSelection.ts b/src/services/interpreterSelection.ts new file mode 100644 index 0000000..72674c5 --- /dev/null +++ b/src/services/interpreterSelection.ts @@ -0,0 +1,11 @@ +export async function selectWorkingInterpreter( + candidates: string[], + probe: (candidate: string) => Promise, +): Promise { + for (const candidate of candidates) { + if (await probe(candidate)) { + return candidate; + } + } + return null; +} diff --git a/src/services/pythonService.ts b/src/services/pythonService.ts index 8792d0b..bea292b 100644 --- a/src/services/pythonService.ts +++ b/src/services/pythonService.ts @@ -4,6 +4,8 @@ import { execFile } from 'child_process'; import { LogService } from './logService'; import { NotificationService } from './notificationService'; +import { InterpreterResolver } from './interpreterResolver'; +import { selectWorkingInterpreter } from './interpreterSelection'; import { ConfigService } from '../utils/config'; import { INSTALL_STRATEGIES, MESSAGES, PYTHON_PACKAGE_NAME } from '../utils/constants'; @@ -25,6 +27,10 @@ interface RuntimePreflight { details: string[]; } +interface ResolvedRuntime { + pythonPath: string; +} + interface ExecResult { ok: boolean; stdout: string; @@ -42,12 +48,14 @@ interface InstallCounters { export class PythonService { private static readonly MIN_SUPPORTED_PYTHON_MINOR = 8; private logService: LogService; + private interpreterResolver: InterpreterResolver; private installerState: InstallerState = 'not_installed'; private counters: InstallCounters = { attempts: 0, success: 0, failure: 0, lastFailure: 'none' }; private startupHealthChecked = false; constructor() { this.logService = LogService.getInstance(); + this.interpreterResolver = new InterpreterResolver(); } public getInstallerState(): InstallerState { @@ -63,13 +71,13 @@ export class PythonService { } this.startupHealthChecked = true; - const preflight = await this.runRuntimePreflight(pythonPath); - if (!preflight.ok) { + const runtime = await this.resolveRuntime(pythonPath); + if (!runtime) { this.logService.logWarning('Startup health check: Python preflight failed.'); return; } - const installed = await this.isPackageInstalled(pythonPath, PYTHON_PACKAGE_NAME); + const installed = await this.isPackageInstalled(runtime.pythonPath, PYTHON_PACKAGE_NAME); this.installerState = installed ? 'installed' : 'not_installed'; this.logService.log(`Startup health check: installer_state=${this.installerState}`); } @@ -77,38 +85,41 @@ export class PythonService { /** * Ensure runtime is ready and codecarbon package is available before tracking starts. */ - public async ensureCodecarbonInstalled(pythonPath: string): Promise { + public async ensureCodecarbonInstalled(pythonPath: string): Promise { this.logService.log(`Preparing Python runtime: ${pythonPath}`); - const preflight = await this.runRuntimePreflight(pythonPath); - if (!preflight.ok) { + const runtime = await this.resolveRuntime(pythonPath); + if (!runtime) { NotificationService.showError(MESSAGES.PREFLIGHT_FAILED); - return false; + return null; } + const resolvedPythonPath = runtime.pythonPath; - const isInstalled = await this.isPackageInstalled(pythonPath, PYTHON_PACKAGE_NAME); + const isInstalled = await this.isPackageInstalled(resolvedPythonPath, PYTHON_PACKAGE_NAME); if (isInstalled) { this.installerState = 'installed'; - return true; + return resolvedPythonPath; } this.installerState = 'not_installed'; if (!ConfigService.isAutoInstallEnabled()) { NotificationService.showWarning(MESSAGES.INSTALL_DISABLED); - return false; + return null; } - return this.installOrRepairCodecarbon(pythonPath, true); + const installedNow = await this.installOrRepairCodecarbon(resolvedPythonPath, true); + return installedNow ? resolvedPythonPath : null; } /** * Explicit command entrypoint to install/repair codecarbon package. */ public async installOrRepairCodecarbon(pythonPath: string, silentSuccess = false): Promise { - const preflight = await this.runRuntimePreflight(pythonPath); - if (!preflight.ok) { + const runtime = await this.resolveRuntime(pythonPath); + if (!runtime) { NotificationService.showError(MESSAGES.PREFLIGHT_FAILED); return false; } + const resolvedPythonPath = runtime.pythonPath; this.installerState = 'installing'; this.counters.attempts += 1; @@ -116,7 +127,7 @@ export class PythonService { const strategies = this.resolveInstallStrategies(); for (const args of strategies) { - const result = await this.execPython(pythonPath, ['-m', 'pip', 'install', ...args, PYTHON_PACKAGE_NAME]); + const result = await this.execPython(resolvedPythonPath, ['-m', 'pip', 'install', ...args, PYTHON_PACKAGE_NAME]); this.logPipAttempt(args, result); if (result.ok) { this.installerState = 'installed'; @@ -144,13 +155,14 @@ export class PythonService { * Check codecarbon version and display information. */ public async checkCodecarbonVersion(pythonPath: string): Promise { - const preflight = await this.runRuntimePreflight(pythonPath); - if (!preflight.ok) { + const runtime = await this.resolveRuntime(pythonPath); + if (!runtime) { NotificationService.showError(MESSAGES.PREFLIGHT_FAILED); return; } + const resolvedPythonPath = runtime.pythonPath; - const result = await this.execPython(pythonPath, ['-m', 'pip', 'show', PYTHON_PACKAGE_NAME]); + const result = await this.execPython(resolvedPythonPath, ['-m', 'pip', 'show', PYTHON_PACKAGE_NAME]); if (!result.ok) { NotificationService.showWarning(MESSAGES.CHECK_VERSION_NOT_INSTALLED); this.logService.log(MESSAGES.NOT_INSTALLED); @@ -169,7 +181,23 @@ export class PythonService { NotificationService.showInfo(`Codecarbon ${version} is installed`); this.logService.log(`Codecarbon version: ${version}`); this.logService.log(`Installation location: ${location}`); - this.logService.log(`Python interpreter: ${pythonPath}`); + this.logService.log(`Python interpreter: ${resolvedPythonPath}`); + } + + private async resolveRuntime(pythonPath: string): Promise { + const preferPythonExtension = !ConfigService.hasExplicitPythonPath(); + const candidates = await this.interpreterResolver.getCandidates(pythonPath, preferPythonExtension); + const selected = await selectWorkingInterpreter(candidates, async (candidate) => { + const preflight = await this.runRuntimePreflight(candidate); + return preflight.ok; + }); + if (!selected) { + return null; + } + if (selected !== pythonPath) { + this.logService.log(`Python runtime fallback selected: ${selected} (configured: ${pythonPath})`); + } + return { pythonPath: selected }; } private async runRuntimePreflight(pythonPath: string): Promise { diff --git a/src/services/trackerService.ts b/src/services/trackerService.ts index 94b025b..bb5fcb4 100644 --- a/src/services/trackerService.ts +++ b/src/services/trackerService.ts @@ -72,8 +72,8 @@ export class TrackerService { try { // Ensure codecarbon is installed - const isInstalled = await this.pythonService.ensureCodecarbonInstalled(pythonPath); - if (!isInstalled) { + const resolvedPythonPath = await this.pythonService.ensureCodecarbonInstalled(pythonPath); + if (!resolvedPythonPath) { return false; } @@ -83,7 +83,7 @@ export class TrackerService { if (emissionsFilePath) { trackerArgs.push('--emissions-file', emissionsFilePath); } - this.pythonProcess = spawn(pythonPath, trackerArgs, workspacePath ? { cwd: workspacePath } : undefined); + this.pythonProcess = spawn(resolvedPythonPath, trackerArgs, workspacePath ? { cwd: workspacePath } : undefined); if (workspacePath && this.pythonProcess.pid) { await this.writePidFile(workspacePath, this.pythonProcess.pid); } diff --git a/src/utils/config.ts b/src/utils/config.ts index 4b7618d..ae6748c 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -24,6 +24,17 @@ export class ConfigService { return resolvePythonPath(rawInterpreter); } + public static hasExplicitPythonPath(): boolean { + const config = this.getConfiguration(); + const inspection = config.inspect(CONFIGURATION_KEYS.INTERPRETER); + const values = [ + inspection?.globalValue, + inspection?.workspaceValue, + inspection?.workspaceFolderValue, + ]; + return values.some((value) => typeof value === 'string' && value.trim().length > 0); + } + /** * Check if launch on startup is enabled */ diff --git a/tests/ts/interpreterCandidates.test.ts b/tests/ts/interpreterCandidates.test.ts new file mode 100644 index 0000000..c08a0f9 --- /dev/null +++ b/tests/ts/interpreterCandidates.test.ts @@ -0,0 +1,21 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { buildInterpreterCandidates } from '../../src/services/interpreterCandidates'; + +test('prefers ms-python interpreter when not explicitly configured', () => { + const candidates = buildInterpreterCandidates('python3', '/opt/venv/bin/python', true); + assert.equal(candidates[0], '/opt/venv/bin/python'); + assert.equal(candidates[1], 'python3'); +}); + +test('prefers explicit interpreter over ms-python interpreter', () => { + const candidates = buildInterpreterCandidates('/custom/python', '/opt/venv/bin/python', false); + assert.equal(candidates[0], '/custom/python'); + assert.equal(candidates[1], '/opt/venv/bin/python'); +}); + +test('includes fallback commands after primary candidates', () => { + const candidates = buildInterpreterCandidates('python3', undefined, false); + assert.equal(candidates[0], 'python3'); + assert.equal(candidates.includes('python'), true); +}); diff --git a/tests/ts/interpreterSelection.test.ts b/tests/ts/interpreterSelection.test.ts new file mode 100644 index 0000000..1b73ed8 --- /dev/null +++ b/tests/ts/interpreterSelection.test.ts @@ -0,0 +1,33 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { selectWorkingInterpreter } from '../../src/services/interpreterSelection'; + +test('selectWorkingInterpreter returns null when no interpreter is available', async () => { + const tried: string[] = []; + const selected = await selectWorkingInterpreter(['python3', 'python'], async (candidate) => { + tried.push(candidate); + return false; + }); + assert.equal(selected, null); + assert.deepEqual(tried, ['python3', 'python']); +}); + +test('selectWorkingInterpreter falls back to second candidate', async () => { + const tried: string[] = []; + const selected = await selectWorkingInterpreter(['python3', 'python'], async (candidate) => { + tried.push(candidate); + return candidate === 'python'; + }); + assert.equal(selected, 'python'); + assert.deepEqual(tried, ['python3', 'python']); +}); + +test('selectWorkingInterpreter stops on first working candidate', async () => { + const tried: string[] = []; + const selected = await selectWorkingInterpreter(['python3', 'python'], async (candidate) => { + tried.push(candidate); + return candidate === 'python3'; + }); + assert.equal(selected, 'python3'); + assert.deepEqual(tried, ['python3']); +}); diff --git a/tsconfig.json b/tsconfig.json index 4a8670c..e735a72 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "target": "ES2020", "outDir": "out", "lib": ["ES2020"], + "types": ["node", "vscode"], "sourceMap": true, "rootDir": "src", "strict": true, diff --git a/tsconfig.tests.json b/tsconfig.tests.json index bc67609..c2a12b4 100644 --- a/tsconfig.tests.json +++ b/tsconfig.tests.json @@ -4,6 +4,7 @@ "target": "ES2020", "outDir": "out-tests", "lib": ["ES2020"], + "types": ["node"], "sourceMap": true, "rootDir": ".", "strict": true, From 0089f307abe06be8d45d175810feef0428078b0e Mon Sep 17 00:00:00 2001 From: inimaz <93inigo93@gmail.com> Date: Wed, 1 Apr 2026 22:02:40 +0200 Subject: [PATCH 3/4] fix: add warning if user misconfigured --- src/services/pythonService.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/pythonService.ts b/src/services/pythonService.ts index bea292b..540b820 100644 --- a/src/services/pythonService.ts +++ b/src/services/pythonService.ts @@ -185,7 +185,8 @@ export class PythonService { } private async resolveRuntime(pythonPath: string): Promise { - const preferPythonExtension = !ConfigService.hasExplicitPythonPath(); + const hasExplicitPythonPath = ConfigService.hasExplicitPythonPath(); + const preferPythonExtension = !hasExplicitPythonPath; const candidates = await this.interpreterResolver.getCandidates(pythonPath, preferPythonExtension); const selected = await selectWorkingInterpreter(candidates, async (candidate) => { const preflight = await this.runRuntimePreflight(candidate); @@ -195,6 +196,11 @@ export class PythonService { return null; } if (selected !== pythonPath) { + if (hasExplicitPythonPath) { + this.logService.logWarning( + `Configured interpreter failed; using fallback interpreter instead: ${selected} (configured: ${pythonPath})`, + ); + } this.logService.log(`Python runtime fallback selected: ${selected} (configured: ${pythonPath})`); } return { pythonPath: selected }; From 4335a6c7710ca19a22c2ad8b061f40f44117d4be Mon Sep 17 00:00:00 2001 From: inimaz <93inigo93@gmail.com> Date: Sun, 26 Apr 2026 11:25:53 +0200 Subject: [PATCH 4/4] fix: windows python discovery --- src/services/interpreterCandidates.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/interpreterCandidates.ts b/src/services/interpreterCandidates.ts index 73f62d3..222ca19 100644 --- a/src/services/interpreterCandidates.ts +++ b/src/services/interpreterCandidates.ts @@ -24,6 +24,10 @@ export function buildInterpreterCandidates( } if (process.platform === 'win32') { + if (seen.has('python3')) { + add('python'); + add('py'); + } if (seen.has('python')) { add('py'); }