Skip to content
Merged
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
86 changes: 3 additions & 83 deletions src/managers/builtin/venvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,14 @@ import * as fsapi from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { l10n, LogOutputChannel, ProgressLocation, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode';
import {
EnvironmentManager,
PythonCommandRunConfiguration,
PythonEnvironment,
PythonEnvironmentApi,
PythonEnvironmentInfo,
} from '../../api';
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api';
import { ENVS_EXTENSION_ID } from '../../common/constants';
import { Common, VenvManagerStrings } from '../../common/localize';
import { traceInfo } from '../../common/logging';
import { getWorkspacePersistentState } from '../../common/persistentState';
import { pickEnvironmentFrom } from '../../common/pickers/environments';
import { EventNames } from '../../common/telemetry/constants';
import { sendTelemetryEvent } from '../../common/telemetry/sender';
import { isWindows } from '../../common/utils/platformUtils';
import {
showErrorMessage,
showInputBox,
Expand All @@ -26,14 +19,13 @@ import {
withProgress,
} from '../../common/window.apis';
import { getConfiguration } from '../../common/workspace.apis';
import { ShellConstants } from '../../features/common/shellConstants';
import {
isNativeEnvInfo,
NativeEnvInfo,
NativePythonEnvironmentKind,
NativePythonFinder,
} from '../common/nativePythonFinder';
import { pathForGitBash, shortVersion, sortEnvironments } from '../common/utils';
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';
import { isUvInstalled, runPython, runUV } from './helpers';
import { getProjectInstallable, getWorkspacePackagesToInstall, PipPackages } from './pipUtils';
import { resolveSystemPythonEnvironmentPath } from './utils';
Expand Down Expand Up @@ -122,79 +114,7 @@ async function getPythonInfo(env: NativeEnvInfo): Promise<PythonEnvironmentInfo>

const binDir = path.dirname(env.executable);

const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();

if (isWindows()) {
shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]);
shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]);
} else {
shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set('unknown', [{ executable: 'deactivate' }]);
}

shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.GITBASH, [
{ executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] },
]);
shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]);

if (await fsapi.pathExists(path.join(binDir, 'Activate.ps1'))) {
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]);
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
} else if (await fsapi.pathExists(path.join(binDir, 'activate.ps1'))) {
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]);
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
}

if (await fsapi.pathExists(path.join(binDir, 'activate.bat'))) {
shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]);
shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]);
}

if (await fsapi.pathExists(path.join(binDir, 'activate.csh'))) {
shellActivation.set(ShellConstants.CSH, [
{ executable: 'source', args: [path.join(binDir, `activate.csh`)] },
]);
shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.FISH, [
{ executable: 'source', args: [path.join(binDir, `activate.csh`)] },
]);
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
}

if (await fsapi.pathExists(path.join(binDir, 'activate.fish'))) {
shellActivation.set(ShellConstants.FISH, [
{ executable: 'source', args: [path.join(binDir, `activate.fish`)] },
]);
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
}

if (await fsapi.pathExists(path.join(binDir, 'activate.xsh'))) {
shellActivation.set(ShellConstants.XONSH, [
{ executable: 'source', args: [path.join(binDir, `activate.xsh`)] },
]);
shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]);
}

if (await fsapi.pathExists(path.join(binDir, 'activate.nu'))) {
shellActivation.set(ShellConstants.NU, [
{ executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] },
]);
shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]);
}
const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir);

return {
name: name,
Expand Down
84 changes: 83 additions & 1 deletion src/managers/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { PythonEnvironment } from '../../api';
import * as fs from 'fs-extra';
import path from 'path';
import { PythonCommandRunConfiguration, PythonEnvironment } from '../../api';
import { isWindows } from '../../common/utils/platformUtils';
import { ShellConstants } from '../../features/common/shellConstants';
import { Installable } from './types';

export function noop() {
Expand Down Expand Up @@ -112,3 +115,82 @@ export function compareVersions(version1: string, version2: string): number {

return 0;
}

export async function getShellActivationCommands(binDir: string): Promise<{
shellActivation: Map<string, PythonCommandRunConfiguration[]>;
shellDeactivation: Map<string, PythonCommandRunConfiguration[]>;
}> {
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();

if (isWindows()) {
shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]);
shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]);
} else {
shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set('unknown', [{ executable: 'deactivate' }]);
}

shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.GITBASH, [
{ executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] },
]);
shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]);
shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]);

if (await fs.pathExists(path.join(binDir, 'Activate.ps1'))) {
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]);
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
} else if (await fs.pathExists(path.join(binDir, 'activate.ps1'))) {
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]);
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
}

if (await fs.pathExists(path.join(binDir, 'activate.bat'))) {
shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]);
shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]);
}

if (await fs.pathExists(path.join(binDir, 'activate.csh'))) {
shellActivation.set(ShellConstants.CSH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]);
shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]);

shellActivation.set(ShellConstants.FISH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]);
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
}

if (await fs.pathExists(path.join(binDir, 'activate.fish'))) {
shellActivation.set(ShellConstants.FISH, [
{ executable: 'source', args: [path.join(binDir, `activate.fish`)] },
]);
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
}

if (await fs.pathExists(path.join(binDir, 'activate.xsh'))) {
shellActivation.set(ShellConstants.XONSH, [
{ executable: 'source', args: [path.join(binDir, `activate.xsh`)] },
]);
shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]);
}

if (await fs.pathExists(path.join(binDir, 'activate.nu'))) {
shellActivation.set(ShellConstants.NU, [
{ executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] },
]);
shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]);
}
return {
shellActivation,
shellDeactivation,
};
}
20 changes: 5 additions & 15 deletions src/managers/poetry/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { traceInfo } from '../../common/logging';
import { showErrorMessage } from '../../common/window.apis';
import { getPythonApi } from '../../features/pythonApi';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { compareVersions } from '../common/utils';
import { PoetryManager } from './poetryManager';
import { PoetryPackageManager } from './poetryPackageManager';
import { getPoetry, getPoetryVersion, isPoetryShellPluginInstalled } from './poetryUtils';
import { getPoetry, getPoetryVersion } from './poetryUtils';

export async function registerPoetryFeatures(
nativeFinder: NativePythonFinder,
Expand All @@ -18,25 +17,16 @@ export async function registerPoetryFeatures(

try {
const poetryPath = await getPoetry(nativeFinder);
let shellSupported = true;
if (poetryPath) {
const version = await getPoetryVersion(poetryPath);
if (!version) {
showErrorMessage(l10n.t('Poetry version could not be determined.'));
return;
}
if (version && compareVersions(version, '2.0.0') >= 0) {
shellSupported = await isPoetryShellPluginInstalled(poetryPath);
if (!shellSupported) {
showErrorMessage(
l10n.t(
'Poetry 2.0.0+ detected. The `shell` command is not available by default. Please install the shell plugin to enable shell activation. See [here](https://python-poetry.org/docs/managing-environments/#activating-the-environment), shell [plugin](https://github.com/python-poetry/poetry-plugin-shell)',
),
);
return;
}
}

traceInfo(
'The `shell` command is not available by default in Poetry versions 2.0.0 and above. Therefore all shell activation will be handled by calling `source <path-to-activate>`. If you face any problems with shell activation, please file an issue at https://github.com/microsoft/vscode-python-environments/issues to help us improve this implementation. Note the current version of Poetry is {0}.',
version,
);
const envManager = new PoetryManager(nativeFinder, api);
const pkgManager = new PoetryPackageManager(api, outputChannel, envManager);

Expand Down
88 changes: 17 additions & 71 deletions src/managers/poetry/poetryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,20 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import { Uri } from 'vscode';
import which from 'which';
import {
EnvironmentManager,
PythonCommandRunConfiguration,
PythonEnvironment,
PythonEnvironmentApi,
PythonEnvironmentInfo,
} from '../../api';
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api';
import { ENVS_EXTENSION_ID } from '../../common/constants';
import { traceError, traceInfo } from '../../common/logging';
import { getWorkspacePersistentState } from '../../common/persistentState';
import { getUserHomeDir, untildify } from '../../common/utils/pathUtils';
import { isWindows } from '../../common/utils/platformUtils';
import { ShellConstants } from '../../features/common/shellConstants';
import {
isNativeEnvInfo,
NativeEnvInfo,
NativeEnvManagerInfo,
NativePythonEnvironmentKind,
NativePythonFinder,
} from '../common/nativePythonFinder';
import { shortVersion, sortEnvironments } from '../common/utils';
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';

async function findPoetry(): Promise<string | undefined> {
try {
Expand Down Expand Up @@ -229,62 +222,12 @@ export async function getPoetryVersion(poetry: string): Promise<string | undefin
return undefined;
}
}

export async function isPoetryShellPluginInstalled(poetry: string): Promise<boolean> {
try {
const { stdout } = await exec(`"${poetry}" self show plugins`);
// Look for a line like: " - poetry-plugin-shell (1.0.1) Poetry plugin to run subshell..."
return /\s+-\s+poetry-plugin-shell\s+\(\d+\.\d+\.\d+\)/.test(stdout);
} catch {
return false;
}
}

function createShellActivation(
poetry: string,
_prefix: string,
): Map<string, PythonCommandRunConfiguration[]> | undefined {
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();

shellActivation.set(ShellConstants.BASH, [{ executable: poetry, args: ['shell'] }]);
shellActivation.set(ShellConstants.ZSH, [{ executable: poetry, args: ['shell'] }]);
shellActivation.set(ShellConstants.SH, [{ executable: poetry, args: ['shell'] }]);
shellActivation.set(ShellConstants.GITBASH, [{ executable: poetry, args: ['shell'] }]);
shellActivation.set(ShellConstants.FISH, [{ executable: poetry, args: ['shell'] }]);
shellActivation.set(ShellConstants.PWSH, [{ executable: poetry, args: ['shell'] }]);
if (isWindows()) {
shellActivation.set(ShellConstants.CMD, [{ executable: poetry, args: ['shell'] }]);
}
shellActivation.set(ShellConstants.NU, [{ executable: poetry, args: ['shell'] }]);
shellActivation.set('unknown', [{ executable: poetry, args: ['shell'] }]);
return shellActivation;
}

function createShellDeactivation(): Map<string, PythonCommandRunConfiguration[]> {
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();

// Poetry doesn't have a standard deactivation command like venv does
// The best approach is to exit the shell or start a new one
shellDeactivation.set('unknown', [{ executable: 'exit' }]);

shellDeactivation.set(ShellConstants.BASH, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.SH, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.CMD, [{ executable: 'exit' }]);
shellDeactivation.set(ShellConstants.NU, [{ executable: 'exit' }]);

return shellDeactivation;
}

function nativeToPythonEnv(
async function nativeToPythonEnv(
info: NativeEnvInfo,
api: PythonEnvironmentApi,
manager: EnvironmentManager,
_poetry: string,
): PythonEnvironment | undefined {
): Promise<PythonEnvironment | undefined> {
if (!(info.prefix && info.executable && info.version)) {
traceError(`Incomplete poetry environment info: ${JSON.stringify(info)}`);
return undefined;
Expand All @@ -294,9 +237,6 @@ function nativeToPythonEnv(
const name = info.name || info.displayName || path.basename(info.prefix);
const displayName = info.displayName || `poetry (${sv})`;

const shellActivation = createShellActivation(_poetry, info.prefix);
const shellDeactivation = createShellDeactivation();

// Check if this is a global Poetry virtualenv by checking if it's in Poetry's virtualenvs directory
// We need to use path.normalize() to ensure consistent path format comparison
const normalizedPrefix = path.normalize(info.prefix);
Expand All @@ -319,6 +259,10 @@ function nativeToPythonEnv(
}
}

// Get generic python environment info to access shell activation/deactivation commands following Poetry 2.0+ dropping the `shell` command
const binDir = path.dirname(info.executable);
const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir);

const environment: PythonEnvironmentInfo = {
name: name,
displayName: displayName,
Expand Down Expand Up @@ -374,14 +318,16 @@ export async function refreshPoetry(

const collection: PythonEnvironment[] = [];

envs.forEach((e) => {
if (poetry) {
const environment = nativeToPythonEnv(e, api, manager, poetry);
if (environment) {
collection.push(environment);
await Promise.all(
envs.map(async (e) => {
if (poetry) {
const environment = await nativeToPythonEnv(e, api, manager, poetry);
if (environment) {
collection.push(environment);
}
}
}
});
}),
);

return sortEnvironments(collection);
}
Expand Down