diff --git a/src/api.ts b/src/api.ts index f889f10c..b0ab33e1 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1211,6 +1211,31 @@ export interface PythonExecutionApi PythonTaskRunApi, PythonBackgroundRunApi {} +/** + * Enum representing the scope for environment variables. + */ +export enum EnvironmentVariableScope { + /** + * Core environment variables required for Python process execution + * (e.g., PATH, PYTHONPATH, PYTHONHOME). These are essential for + * running Python subprocesses and scripts. + */ + Process = 1, + + /** + * User-defined environment variables from .env files that should be + * injected into terminals. These are additional variables meant for + * development environment customization. + */ + Terminal = 2, + + /** + * Combines both Process and Terminal scopes to get all environment + * variables. + */ + All = Process | Terminal +} + /** * Event arguments for when the monitored `.env` files or any other sources change. */ @@ -1240,11 +1265,13 @@ export interface PythonEnvironmentVariablesApi { * @param uri The URI of the project, workspace or a file in a for which environment variables are required. * @param overrides Additional environment variables to override the defaults. * @param baseEnvVar The base environment variables that should be used as a starting point. + * @param scope The scope of environment variables to return. Defaults to All (both Process and Terminal variables). */ getEnvironmentVariables( uri: Uri, overrides?: ({ [key: string]: string | undefined } | Uri)[], baseEnvVar?: { [key: string]: string | undefined }, + scope?: EnvironmentVariableScope, ): Promise<{ [key: string]: string | undefined }>; /** diff --git a/src/features/execution/envVariableManager.ts b/src/features/execution/envVariableManager.ts index 28db0c43..13aa5933 100644 --- a/src/features/execution/envVariableManager.ts +++ b/src/features/execution/envVariableManager.ts @@ -2,7 +2,7 @@ import * as fsapi from 'fs-extra'; import * as path from 'path'; import { Event, EventEmitter, FileChangeType, Uri } from 'vscode'; import { Disposable } from 'vscode-jsonrpc'; -import { DidChangeEnvironmentVariablesEventArgs, PythonEnvironmentVariablesApi } from '../../api'; +import { DidChangeEnvironmentVariablesEventArgs, EnvironmentVariableScope, PythonEnvironmentVariablesApi } from '../../api'; import { resolveVariables } from '../../common/utils/internalVariables'; import { createFileSystemWatcher, getConfiguration } from '../../common/workspace.apis'; import { PythonProjectManager } from '../../internal.api'; @@ -40,36 +40,45 @@ export class PythonEnvVariableManager implements EnvVarManager { uri: Uri, overrides?: ({ [key: string]: string | undefined } | Uri)[], baseEnvVar?: { [key: string]: string | undefined }, + scope: EnvironmentVariableScope = EnvironmentVariableScope.All, ): Promise<{ [key: string]: string | undefined }> { const project = this.pm.get(uri); const base = baseEnvVar || { ...process.env }; let env = base; - const config = getConfiguration('python', project?.uri ?? uri); - let envFilePath = config.get('envFile'); - envFilePath = envFilePath ? path.normalize(resolveVariables(envFilePath, uri)) : undefined; - - if (envFilePath && (await fsapi.pathExists(envFilePath))) { - const other = await parseEnvFile(Uri.file(envFilePath)); - env = mergeEnvVariables(env, other); + // Handle Process scope - return only the base environment variables + if (scope === EnvironmentVariableScope.Process) { + return env; } - let projectEnvFilePath = project ? path.normalize(path.join(project.uri.fsPath, '.env')) : undefined; - if ( - projectEnvFilePath && - projectEnvFilePath?.toLowerCase() !== envFilePath?.toLowerCase() && - (await fsapi.pathExists(projectEnvFilePath)) - ) { - const other = await parseEnvFile(Uri.file(projectEnvFilePath)); - env = mergeEnvVariables(env, other); - } + // Handle Terminal scope or All scope - include .env files + if (scope & EnvironmentVariableScope.Terminal) { + const config = getConfiguration('python', project?.uri ?? uri); + let envFilePath = config.get('envFile'); + envFilePath = envFilePath ? path.normalize(resolveVariables(envFilePath, uri)) : undefined; - if (overrides) { - for (const override of overrides) { - const other = override instanceof Uri ? await parseEnvFile(override) : override; + if (envFilePath && (await fsapi.pathExists(envFilePath))) { + const other = await parseEnvFile(Uri.file(envFilePath)); env = mergeEnvVariables(env, other); } + + let projectEnvFilePath = project ? path.normalize(path.join(project.uri.fsPath, '.env')) : undefined; + if ( + projectEnvFilePath && + projectEnvFilePath?.toLowerCase() !== envFilePath?.toLowerCase() && + (await fsapi.pathExists(projectEnvFilePath)) + ) { + const other = await parseEnvFile(Uri.file(projectEnvFilePath)); + env = mergeEnvVariables(env, other); + } + + if (overrides) { + for (const override of overrides) { + const other = override instanceof Uri ? await parseEnvFile(override) : override; + env = mergeEnvVariables(env, other); + } + } } return env; diff --git a/src/features/pythonApi.ts b/src/features/pythonApi.ts index cf1e0485..dafa979c 100644 --- a/src/features/pythonApi.ts +++ b/src/features/pythonApi.ts @@ -29,6 +29,7 @@ import { PythonTerminalCreateOptions, DidChangeEnvironmentVariablesEventArgs, CreateEnvironmentOptions, + EnvironmentVariableScope, } from '../api'; import { EnvironmentManagers, @@ -333,8 +334,9 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi { uri: Uri, overrides?: ({ [key: string]: string | undefined } | Uri)[], baseEnvVar?: { [key: string]: string | undefined }, + scope?: EnvironmentVariableScope, ): Promise<{ [key: string]: string | undefined }> { - return this.envVarManager.getEnvironmentVariables(checkUri(uri) as Uri, overrides, baseEnvVar); + return this.envVarManager.getEnvironmentVariables(checkUri(uri) as Uri, overrides, baseEnvVar, scope); } } diff --git a/src/features/terminal/terminalEnvVarInjector.ts b/src/features/terminal/terminalEnvVarInjector.ts index 4bf48039..021f0479 100644 --- a/src/features/terminal/terminalEnvVarInjector.ts +++ b/src/features/terminal/terminalEnvVarInjector.ts @@ -5,11 +5,12 @@ import * as fse from 'fs-extra'; import * as path from 'path'; import { Disposable, - EnvironmentVariableScope, + EnvironmentVariableScope as VSCodeEnvironmentVariableScope, GlobalEnvironmentVariableCollection, workspace, WorkspaceFolder, } from 'vscode'; +import { EnvironmentVariableScope } from '../../api'; import { traceError, traceVerbose } from '../../common/logging'; import { resolveVariables } from '../../common/utils/internalVariables'; import { getConfiguration, getWorkspaceFolder } from '../../common/workspace.apis'; @@ -109,7 +110,12 @@ export class TerminalEnvVarInjector implements Disposable { private async injectEnvironmentVariablesForWorkspace(workspaceFolder: WorkspaceFolder): Promise { const workspaceUri = workspaceFolder.uri; try { - const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri); + const envVars = await this.envVarManager.getEnvironmentVariables( + workspaceUri, + undefined, + undefined, + EnvironmentVariableScope.Terminal + ); // use scoped environment variable collection const envVarScope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder }); @@ -161,7 +167,7 @@ export class TerminalEnvVarInjector implements Disposable { this.envVarCollection.clear(); } - private getEnvironmentVariableCollectionScoped(scope: EnvironmentVariableScope = {}) { + private getEnvironmentVariableCollectionScoped(scope: VSCodeEnvironmentVariableScope = {}) { const envVarCollection = this.envVarCollection as GlobalEnvironmentVariableCollection; return envVarCollection.getScoped(scope); } diff --git a/src/test/features/envVariableManager.unit.test.ts b/src/test/features/envVariableManager.unit.test.ts new file mode 100644 index 00000000..5e609ae2 --- /dev/null +++ b/src/test/features/envVariableManager.unit.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import { EnvironmentVariableScope } from '../../api'; + +suite('Environment Variable Scope Tests', () => { + test('should have correct enum values', () => { + // Test that the enum values are what we expect + sinon.assert.match(EnvironmentVariableScope.Process, 1); + sinon.assert.match(EnvironmentVariableScope.Terminal, 2); + sinon.assert.match(EnvironmentVariableScope.All, 3); // Process | Terminal = 1 | 2 = 3 + }); + + test('should support bitwise operations', () => { + // Test that All combines Process and Terminal + const all = EnvironmentVariableScope.Process | EnvironmentVariableScope.Terminal; + sinon.assert.match(all, EnvironmentVariableScope.All); + + // Test that we can check if All includes Process + const includesProcess = EnvironmentVariableScope.All & EnvironmentVariableScope.Process; + sinon.assert.match(includesProcess, EnvironmentVariableScope.Process); + + // Test that we can check if All includes Terminal + const includesTerminal = EnvironmentVariableScope.All & EnvironmentVariableScope.Terminal; + sinon.assert.match(includesTerminal, EnvironmentVariableScope.Terminal); + }); +}); \ No newline at end of file