Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to the `@vscode/python-environments` API package are documen
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.1]

### Fixed

- Clarified that `PythonEnvironmentInfo.environmentPath` always identifies the Python executable.

## [1.4.0]

### Changed
Expand Down
4 changes: 2 additions & 2 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@vscode/python-environments",
"description": "An API facade for the Python Environments extension in VS Code",
"version": "1.4.0",
"version": "1.4.1",
"author": {
"name": "Microsoft Corporation"
},
Expand Down
9 changes: 4 additions & 5 deletions src/managers/builtin/venvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ async function getPythonInfo(env: NativeEnvInfo, nameStyle: VenvNameStyle = 'def
version: env.version ?? 'Unknown',
description: env.error,
tooltip: env.error,
environmentPath: Uri.file(env.prefix ?? env.executable ?? ''),
environmentPath: Uri.file(
env.executable ?? (env.prefix ? getVenvPythonPath(env.prefix) : ''),
),
iconPath: new ThemeIcon('warning'),
sysPrefix: env.prefix ?? '',
execInfo: {
Expand Down Expand Up @@ -616,10 +618,7 @@ export async function removeVenv(
log: LogOutputChannel,
options?: RemoveEnvironmentOptions,
): Promise<boolean> {
const pythonPath = os.platform() === 'win32' ? 'python.exe' : 'python';

const envFsPath = path.normalize(environment.environmentPath.fsPath);
const envPath = envFsPath.endsWith(pythonPath) ? path.dirname(path.dirname(envFsPath)) : envFsPath;
const envPath = path.normalize(environment.sysPrefix);

const validationError = await validateVenvRemovalPath(envPath, log);
if (validationError) {
Expand Down
4 changes: 2 additions & 2 deletions src/managers/conda/condaEnvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
// If the environment is inside the workspace, add a .gitignore file
try {
const projectUris = this.api.getPythonProjects().map((p) => p.uri.fsPath);
const envPath = result.environmentPath?.fsPath;
const envPath = result.sysPrefix;
if (envPath && projectUris.some((root) => envPath.startsWith(root))) {
const gitignorePath = path.join(envPath, '.gitignore');
await fs.writeFile(gitignorePath, '*\n', { flag: 'w' });
Expand Down Expand Up @@ -394,7 +394,7 @@ export class CondaEnvManager implements EnvironmentManager, Disposable {
// Notify user if CONDA_PREFIX is set and they're trying to select a different environment
if (process.env.CONDA_PREFIX && checkedEnv) {
const condaPrefixPath = process.env.CONDA_PREFIX;
const selectedPath = checkedEnv.environmentPath.fsPath;
const selectedPath = checkedEnv.sysPrefix;
// Only show notification if they selected a different environment
if (condaPrefixPath !== selectedPath) {
showInformationMessage(CondaStrings.condaCondaPrefixActive);
Expand Down
4 changes: 2 additions & 2 deletions src/managers/conda/condaPackageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export class CondaPackageManager implements PackageManager, Disposable {
try {
const commandOptions = {
pythonExecutable: 'conda',
condaEnvironmentPath: environment.environmentPath.fsPath,
condaEnvironmentPath: environment.sysPrefix,
log: this.log,
};

Expand Down Expand Up @@ -172,7 +172,7 @@ export class CondaPackageManager implements PackageManager, Disposable {
private async fetchPackages(environment: PythonEnvironment): Promise<Package[] | undefined> {
const listCmd = new CondaListCommand({
pythonExecutable: 'conda',
condaEnvironmentPath: environment.environmentPath.fsPath,
condaEnvironmentPath: environment.sysPrefix,
log: this.log,
});
try {
Expand Down
23 changes: 15 additions & 8 deletions src/managers/conda/condaUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ export async function getNamedCondaPythonInfo(

return {
name: name,
environmentPath: Uri.file(prefix),
environmentPath: Uri.file(executable),
displayName: `${name} (${sv})`,
shortDisplayName: `${name}:${sv}`,
displayPath: prefix,
Expand Down Expand Up @@ -376,7 +376,7 @@ export async function getPrefixesCondaPythonInfo(
const basename = path.basename(prefix);
return {
name: basename,
environmentPath: Uri.file(prefix),
environmentPath: Uri.file(executable),
displayName: `${basename} (${sv})`,
shortDisplayName: `${basename}:${sv}`,
displayPath: prefix,
Expand Down Expand Up @@ -749,7 +749,9 @@ export function nonWindowsGenerateConfig(
function getCondaWithoutPython(name: string, prefix: string, conda: string): PythonEnvironmentInfo {
return {
name: name,
environmentPath: Uri.file(prefix),
environmentPath: Uri.file(
isWindows() ? path.join(prefix, 'python.exe') : path.join(prefix, 'bin', 'python'),
),
displayName: `${name} (no-python)`,
shortDisplayName: `${name} (no-python)`,
displayPath: prefix,
Expand All @@ -765,7 +767,8 @@ function getCondaWithoutPython(name: string, prefix: string, conda: string): Pyt
};
}

async function nativeToPythonEnv(
/** Converts native finder data to a Conda environment. @internal Exported for testing. */
export async function nativeToPythonEnv(
e: NativeEnvInfo,
api: PythonEnvironmentApi,
manager: EnvironmentManager,
Expand All @@ -777,10 +780,14 @@ async function nativeToPythonEnv(
traceWarn('nativeToPythonEnv received null/undefined NativeEnvInfo');
return undefined;
}
if (!(e.prefix && e.executable && e.version)) {
if (!e.prefix) {
traceWarn('Ignoring Conda environment without a prefix');
return undefined;
}
if (!(e.executable && e.version)) {
let name = e.name;
const environment = api.createPythonEnvironmentItem(
getCondaWithoutPython(name ?? '', e.prefix ?? '', conda),
getCondaWithoutPython(name ?? '', e.prefix, conda),
manager,
);
log.info(`Found a No-Python conda environment: ${e.executable ?? e.prefix ?? 'conda-no-python'}`);
Expand Down Expand Up @@ -1224,11 +1231,11 @@ export async function quickCreateConda(
}

export async function deleteCondaEnvironment(environment: PythonEnvironment, log: LogOutputChannel): Promise<boolean> {
let args = ['env', 'remove', '--yes', '--prefix', environment.environmentPath.fsPath];
let args = ['env', 'remove', '--yes', '--prefix', environment.sysPrefix];
return await withProgress(
{
location: ProgressLocation.Notification,
title: l10n.t('Deleting conda environment: {0}', environment.environmentPath.fsPath),
title: l10n.t('Deleting conda environment: {0}', environment.sysPrefix),
},
async () => {
try {
Expand Down
1 change: 1 addition & 0 deletions src/managers/pipenv/pipenvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export class PipenvManager implements EnvironmentManager, Disposable {
return this.collection.find(
(env) =>
normalizePath(env.environmentPath.fsPath) === normalized ||
normalizePath(env.sysPrefix) === normalized ||
(env.execInfo?.run.executable && normalizePath(env.execInfo.run.executable) === normalized),
);
}
Expand Down
5 changes: 3 additions & 2 deletions src/managers/pipenv/pipenvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export async function getPipenv(): Promise<string | undefined> {
}
}

async function nativeToPythonEnv(
/** Converts native finder data to a Pipenv environment. @internal Exported for testing. */
export async function nativeToPythonEnv(
info: NativeEnvInfo,
api: PythonEnvironmentApi,
manager: EnvironmentManager,
Expand Down Expand Up @@ -139,7 +140,7 @@ async function nativeToPythonEnv(
shortDisplayName: displayName,
displayPath: info.prefix,
version: info.version,
environmentPath: Uri.file(info.prefix),
environmentPath: Uri.file(info.executable),
description: undefined,
tooltip: info.prefix,
execInfo: {
Expand Down
2 changes: 1 addition & 1 deletion src/managers/poetry/poetryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export async function nativeToPythonEnv(
shortDisplayName: displayName,
displayPath: info.prefix,
version: info.version,
environmentPath: Uri.file(info.prefix),
environmentPath: Uri.file(info.executable),
description: undefined,
tooltip: info.prefix,
execInfo: {
Expand Down
5 changes: 3 additions & 2 deletions src/managers/pyenv/pyenvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ export async function getPyenv(): Promise<string | undefined> {
}
}

function nativeToPythonEnv(
/** Converts native finder data to a pyenv environment. @internal Exported for testing. */
export function nativeToPythonEnv(
info: NativeEnvInfo,
api: PythonEnvironmentApi,
manager: EnvironmentManager,
Expand Down Expand Up @@ -211,7 +212,7 @@ function nativeToPythonEnv(
shortDisplayName: displayName,
displayPath: info.prefix,
version: info.version,
environmentPath: Uri.file(info.prefix),
environmentPath: Uri.file(info.executable),
description: undefined,
tooltip: info.prefix,
execInfo: {
Expand Down
7 changes: 6 additions & 1 deletion src/test/managers/builtin/venvUtils.removeVenv.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,19 @@ suite('venvUtils removeVenv validation integration', () => {
test('headless removal skips confirmation and removes the environment', async () => {
const tempRoot = await fse.mkdtemp(path.join(os.tmpdir(), 'remove-venv-'));
const envPath = path.join(tempRoot, '.venv');
const executable = path.join(
envPath,
os.platform() === 'win32' ? 'Scripts' : 'bin',
os.platform() === 'win32' ? 'python3.12.exe' : 'python3.12',
);
await fse.outputFile(path.join(envPath, 'pyvenv.cfg'), 'home = base');
const showWarningMessageStub = sinon.stub(windowApis, 'showWarningMessage');
sinon.stub(windowApis, 'withProgress').callsFake(async (_options, task) => task({} as never, {} as never));
sinon.stub(uvEnvironments, 'removeUvEnvironment').resolves();

try {
const removed = await removeVenv(
createMockPythonEnvironment({ name: '.venv', envPath }),
createMockPythonEnvironment({ name: '.venv', envPath: executable, sysPrefix: envPath }),
createMockLogOutputChannel(),
{ runHeadless: true },
);
Expand Down
28 changes: 28 additions & 0 deletions src/test/managers/conda/condaPackageManager.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,34 @@ suite('CondaPackageManager', () => {
assert.ok(logError.calledOnceWithExactly('Error parsing installed Conda packages', parseError));
});

test('uses sysPrefix for Conda package commands', async () => {
const prefix = Uri.file('environment').fsPath;
const executable = Uri.joinPath(Uri.file(prefix), 'bin', 'python').fsPath;
const environment = {
envId: { id: 'test-environment', managerId: 'test-manager' },
environmentPath: Uri.file(executable),
sysPrefix: prefix,
} as PythonEnvironment;
const manager = new CondaPackageManager(
{ createPackageItem: sinon.stub() } as unknown as PythonEnvironmentApi,
{ error: sinon.stub() } as unknown as LogOutputChannel,
);
let installPrefix: string | undefined;
let listPrefix: string | undefined;
sinon.stub(CondaInstallCommand.prototype, 'execute').callsFake(async function (this: CondaInstallCommand) {
installPrefix = (this as unknown as { condaEnvironmentPath: string }).condaEnvironmentPath;
});
sinon.stub(CondaListCommand.prototype, 'execute').callsFake(async function (this: CondaListCommand) {
listPrefix = (this as unknown as { condaEnvironmentPath: string }).condaEnvironmentPath;
return [];
});

await manager.manage(environment, { install: ['requests'], runHeadless: true });

assert.strictEqual(installPrefix, prefix);
assert.strictEqual(listPrefix, prefix);
});

test('propagates package version lookup failures', async () => {
const environment = {
envId: { id: 'test-environment', managerId: 'test-manager' },
Expand Down
3 changes: 2 additions & 1 deletion src/test/managers/conda/condaUtils.createNamed.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ suite('Conda Utils - createNamedCondaEnvironment', () => {
assert.ok(!mockState.get.calledWith(CONDA_PREFIXES_KEY));

const info = createEnvironmentItem.firstCall.args[0] as PythonEnvironmentInfo;
assert.strictEqual(info.environmentPath.fsPath, Uri.file(envPrefix).fsPath);
const executable = path.join(envPrefix, os.platform() === 'win32' ? 'python.exe' : path.join('bin', 'python'));
assert.strictEqual(info.environmentPath.fsPath, Uri.file(executable).fsPath);
});
});
});
4 changes: 4 additions & 0 deletions src/test/managers/conda/condaUtils.pythonExePath.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import assert from 'assert';
import * as path from 'path';
import * as sinon from 'sinon';
import { Uri } from 'vscode';
import { EnvironmentManager } from '../../../api';
import { CondaEnvManager } from '../../../managers/conda/condaEnvManager';
import { getNamedCondaPythonInfo, getPrefixesCondaPythonInfo } from '../../../managers/conda/condaUtils';
Expand Down Expand Up @@ -30,6 +31,7 @@ suite('Conda Python executable path construction', () => {
const executable = path.posix.join(prefix, 'bin', 'python');
const info = await getNamedCondaPythonInfo('myenv', prefix, executable, '3.12.0', '/usr/bin/conda', mockManager);

assert.strictEqual(info.environmentPath.fsPath, Uri.file(executable).fsPath);
assert.ok(
info.execInfo.run.executable.includes(path.join('bin', 'python')) ||
info.execInfo.run.executable.endsWith('python'),
Expand All @@ -44,6 +46,7 @@ suite('Conda Python executable path construction', () => {
const executable = path.posix.join(prefix, 'bin', 'python');
const info = await getPrefixesCondaPythonInfo(prefix, executable, '3.12.0', '/usr/bin/conda', mockManager);

assert.strictEqual(info.environmentPath.fsPath, Uri.file(executable).fsPath);
assert.ok(
info.execInfo.run.executable.includes(path.join('bin', 'python')) ||
info.execInfo.run.executable.endsWith('python'),
Expand All @@ -58,6 +61,7 @@ suite('Conda Python executable path construction', () => {
const executable = path.win32.join(prefix, 'python.exe');
const info = await getNamedCondaPythonInfo('myenv', prefix, executable, '3.12.0', 'C:\\conda\\conda.exe', mockManager);

assert.strictEqual(info.environmentPath.fsPath, Uri.file(executable).fsPath);
assert.ok(
info.execInfo.run.executable.endsWith('python.exe'),
`executable should end with python.exe, got: ${info.execInfo.run.executable}`,
Expand Down
Loading
Loading