Skip to content

Commit ba1d393

Browse files
authored
support poetry terminal activation required for versions > 2.0.0 (#528)
fixes #529
1 parent 2938cb6 commit ba1d393

File tree

4 files changed

+108
-170
lines changed

4 files changed

+108
-170
lines changed

src/managers/builtin/venvUtils.ts

Lines changed: 3 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,14 @@ import * as fsapi from 'fs-extra';
22
import * as os from 'os';
33
import * as path from 'path';
44
import { l10n, LogOutputChannel, ProgressLocation, QuickPickItem, QuickPickItemKind, ThemeIcon, Uri } from 'vscode';
5-
import {
6-
EnvironmentManager,
7-
PythonCommandRunConfiguration,
8-
PythonEnvironment,
9-
PythonEnvironmentApi,
10-
PythonEnvironmentInfo,
11-
} from '../../api';
5+
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api';
126
import { ENVS_EXTENSION_ID } from '../../common/constants';
137
import { Common, VenvManagerStrings } from '../../common/localize';
148
import { traceInfo } from '../../common/logging';
159
import { getWorkspacePersistentState } from '../../common/persistentState';
1610
import { pickEnvironmentFrom } from '../../common/pickers/environments';
1711
import { EventNames } from '../../common/telemetry/constants';
1812
import { sendTelemetryEvent } from '../../common/telemetry/sender';
19-
import { isWindows } from '../../common/utils/platformUtils';
2013
import {
2114
showErrorMessage,
2215
showInputBox,
@@ -26,14 +19,13 @@ import {
2619
withProgress,
2720
} from '../../common/window.apis';
2821
import { getConfiguration } from '../../common/workspace.apis';
29-
import { ShellConstants } from '../../features/common/shellConstants';
3022
import {
3123
isNativeEnvInfo,
3224
NativeEnvInfo,
3325
NativePythonEnvironmentKind,
3426
NativePythonFinder,
3527
} from '../common/nativePythonFinder';
36-
import { pathForGitBash, shortVersion, sortEnvironments } from '../common/utils';
28+
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';
3729
import { isUvInstalled, runPython, runUV } from './helpers';
3830
import { getProjectInstallable, getWorkspacePackagesToInstall, PipPackages } from './pipUtils';
3931
import { resolveSystemPythonEnvironmentPath } from './utils';
@@ -122,79 +114,7 @@ async function getPythonInfo(env: NativeEnvInfo): Promise<PythonEnvironmentInfo>
122114

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

125-
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
126-
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
127-
128-
if (isWindows()) {
129-
shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]);
130-
shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]);
131-
} else {
132-
shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
133-
shellDeactivation.set('unknown', [{ executable: 'deactivate' }]);
134-
}
135-
136-
shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
137-
shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]);
138-
139-
shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
140-
shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]);
141-
142-
shellActivation.set(ShellConstants.GITBASH, [
143-
{ executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] },
144-
]);
145-
shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]);
146-
147-
shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
148-
shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]);
149-
150-
shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]);
151-
shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]);
152-
153-
if (await fsapi.pathExists(path.join(binDir, 'Activate.ps1'))) {
154-
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]);
155-
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
156-
} else if (await fsapi.pathExists(path.join(binDir, 'activate.ps1'))) {
157-
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]);
158-
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
159-
}
160-
161-
if (await fsapi.pathExists(path.join(binDir, 'activate.bat'))) {
162-
shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]);
163-
shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]);
164-
}
165-
166-
if (await fsapi.pathExists(path.join(binDir, 'activate.csh'))) {
167-
shellActivation.set(ShellConstants.CSH, [
168-
{ executable: 'source', args: [path.join(binDir, `activate.csh`)] },
169-
]);
170-
shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]);
171-
172-
shellActivation.set(ShellConstants.FISH, [
173-
{ executable: 'source', args: [path.join(binDir, `activate.csh`)] },
174-
]);
175-
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
176-
}
177-
178-
if (await fsapi.pathExists(path.join(binDir, 'activate.fish'))) {
179-
shellActivation.set(ShellConstants.FISH, [
180-
{ executable: 'source', args: [path.join(binDir, `activate.fish`)] },
181-
]);
182-
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
183-
}
184-
185-
if (await fsapi.pathExists(path.join(binDir, 'activate.xsh'))) {
186-
shellActivation.set(ShellConstants.XONSH, [
187-
{ executable: 'source', args: [path.join(binDir, `activate.xsh`)] },
188-
]);
189-
shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]);
190-
}
191-
192-
if (await fsapi.pathExists(path.join(binDir, 'activate.nu'))) {
193-
shellActivation.set(ShellConstants.NU, [
194-
{ executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] },
195-
]);
196-
shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]);
197-
}
117+
const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir);
198118

199119
return {
200120
name: name,

src/managers/common/utils.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
import { PythonEnvironment } from '../../api';
1+
import * as fs from 'fs-extra';
2+
import path from 'path';
3+
import { PythonCommandRunConfiguration, PythonEnvironment } from '../../api';
24
import { isWindows } from '../../common/utils/platformUtils';
5+
import { ShellConstants } from '../../features/common/shellConstants';
36
import { Installable } from './types';
47

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

113116
return 0;
114117
}
118+
119+
export async function getShellActivationCommands(binDir: string): Promise<{
120+
shellActivation: Map<string, PythonCommandRunConfiguration[]>;
121+
shellDeactivation: Map<string, PythonCommandRunConfiguration[]>;
122+
}> {
123+
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
124+
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
125+
126+
if (isWindows()) {
127+
shellActivation.set('unknown', [{ executable: path.join(binDir, `activate`) }]);
128+
shellDeactivation.set('unknown', [{ executable: path.join(binDir, `deactivate`) }]);
129+
} else {
130+
shellActivation.set('unknown', [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
131+
shellDeactivation.set('unknown', [{ executable: 'deactivate' }]);
132+
}
133+
134+
shellActivation.set(ShellConstants.SH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
135+
shellDeactivation.set(ShellConstants.SH, [{ executable: 'deactivate' }]);
136+
137+
shellActivation.set(ShellConstants.BASH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
138+
shellDeactivation.set(ShellConstants.BASH, [{ executable: 'deactivate' }]);
139+
140+
shellActivation.set(ShellConstants.GITBASH, [
141+
{ executable: 'source', args: [pathForGitBash(path.join(binDir, `activate`))] },
142+
]);
143+
shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'deactivate' }]);
144+
145+
shellActivation.set(ShellConstants.ZSH, [{ executable: 'source', args: [path.join(binDir, `activate`)] }]);
146+
shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'deactivate' }]);
147+
148+
shellActivation.set(ShellConstants.KSH, [{ executable: '.', args: [path.join(binDir, `activate`)] }]);
149+
shellDeactivation.set(ShellConstants.KSH, [{ executable: 'deactivate' }]);
150+
151+
if (await fs.pathExists(path.join(binDir, 'Activate.ps1'))) {
152+
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `Activate.ps1`)] }]);
153+
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
154+
} else if (await fs.pathExists(path.join(binDir, 'activate.ps1'))) {
155+
shellActivation.set(ShellConstants.PWSH, [{ executable: '&', args: [path.join(binDir, `activate.ps1`)] }]);
156+
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'deactivate' }]);
157+
}
158+
159+
if (await fs.pathExists(path.join(binDir, 'activate.bat'))) {
160+
shellActivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `activate.bat`) }]);
161+
shellDeactivation.set(ShellConstants.CMD, [{ executable: path.join(binDir, `deactivate.bat`) }]);
162+
}
163+
164+
if (await fs.pathExists(path.join(binDir, 'activate.csh'))) {
165+
shellActivation.set(ShellConstants.CSH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]);
166+
shellDeactivation.set(ShellConstants.CSH, [{ executable: 'deactivate' }]);
167+
168+
shellActivation.set(ShellConstants.FISH, [{ executable: 'source', args: [path.join(binDir, `activate.csh`)] }]);
169+
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
170+
}
171+
172+
if (await fs.pathExists(path.join(binDir, 'activate.fish'))) {
173+
shellActivation.set(ShellConstants.FISH, [
174+
{ executable: 'source', args: [path.join(binDir, `activate.fish`)] },
175+
]);
176+
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'deactivate' }]);
177+
}
178+
179+
if (await fs.pathExists(path.join(binDir, 'activate.xsh'))) {
180+
shellActivation.set(ShellConstants.XONSH, [
181+
{ executable: 'source', args: [path.join(binDir, `activate.xsh`)] },
182+
]);
183+
shellDeactivation.set(ShellConstants.XONSH, [{ executable: 'deactivate' }]);
184+
}
185+
186+
if (await fs.pathExists(path.join(binDir, 'activate.nu'))) {
187+
shellActivation.set(ShellConstants.NU, [
188+
{ executable: 'overlay', args: ['use', path.join(binDir, 'activate.nu')] },
189+
]);
190+
shellDeactivation.set(ShellConstants.NU, [{ executable: 'overlay', args: ['hide', 'activate'] }]);
191+
}
192+
return {
193+
shellActivation,
194+
shellDeactivation,
195+
};
196+
}

src/managers/poetry/main.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ import { traceInfo } from '../../common/logging';
44
import { showErrorMessage } from '../../common/window.apis';
55
import { getPythonApi } from '../../features/pythonApi';
66
import { NativePythonFinder } from '../common/nativePythonFinder';
7-
import { compareVersions } from '../common/utils';
87
import { PoetryManager } from './poetryManager';
98
import { PoetryPackageManager } from './poetryPackageManager';
10-
import { getPoetry, getPoetryVersion, isPoetryShellPluginInstalled } from './poetryUtils';
9+
import { getPoetry, getPoetryVersion } from './poetryUtils';
1110

1211
export async function registerPoetryFeatures(
1312
nativeFinder: NativePythonFinder,
@@ -18,25 +17,16 @@ export async function registerPoetryFeatures(
1817

1918
try {
2019
const poetryPath = await getPoetry(nativeFinder);
21-
let shellSupported = true;
2220
if (poetryPath) {
2321
const version = await getPoetryVersion(poetryPath);
2422
if (!version) {
2523
showErrorMessage(l10n.t('Poetry version could not be determined.'));
2624
return;
2725
}
28-
if (version && compareVersions(version, '2.0.0') >= 0) {
29-
shellSupported = await isPoetryShellPluginInstalled(poetryPath);
30-
if (!shellSupported) {
31-
showErrorMessage(
32-
l10n.t(
33-
'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)',
34-
),
35-
);
36-
return;
37-
}
38-
}
39-
26+
traceInfo(
27+
'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}.',
28+
version,
29+
);
4030
const envManager = new PoetryManager(nativeFinder, api);
4131
const pkgManager = new PoetryPackageManager(api, outputChannel, envManager);
4232

src/managers/poetry/poetryUtils.ts

Lines changed: 17 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,20 @@ import * as fs from 'fs-extra';
22
import * as path from 'path';
33
import { Uri } from 'vscode';
44
import which from 'which';
5-
import {
6-
EnvironmentManager,
7-
PythonCommandRunConfiguration,
8-
PythonEnvironment,
9-
PythonEnvironmentApi,
10-
PythonEnvironmentInfo,
11-
} from '../../api';
5+
import { EnvironmentManager, PythonEnvironment, PythonEnvironmentApi, PythonEnvironmentInfo } from '../../api';
126
import { ENVS_EXTENSION_ID } from '../../common/constants';
137
import { traceError, traceInfo } from '../../common/logging';
148
import { getWorkspacePersistentState } from '../../common/persistentState';
159
import { getUserHomeDir, untildify } from '../../common/utils/pathUtils';
1610
import { isWindows } from '../../common/utils/platformUtils';
17-
import { ShellConstants } from '../../features/common/shellConstants';
1811
import {
1912
isNativeEnvInfo,
2013
NativeEnvInfo,
2114
NativeEnvManagerInfo,
2215
NativePythonEnvironmentKind,
2316
NativePythonFinder,
2417
} from '../common/nativePythonFinder';
25-
import { shortVersion, sortEnvironments } from '../common/utils';
18+
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';
2619

2720
async function findPoetry(): Promise<string | undefined> {
2821
try {
@@ -229,62 +222,12 @@ export async function getPoetryVersion(poetry: string): Promise<string | undefin
229222
return undefined;
230223
}
231224
}
232-
233-
export async function isPoetryShellPluginInstalled(poetry: string): Promise<boolean> {
234-
try {
235-
const { stdout } = await exec(`"${poetry}" self show plugins`);
236-
// Look for a line like: " - poetry-plugin-shell (1.0.1) Poetry plugin to run subshell..."
237-
return /\s+-\s+poetry-plugin-shell\s+\(\d+\.\d+\.\d+\)/.test(stdout);
238-
} catch {
239-
return false;
240-
}
241-
}
242-
243-
function createShellActivation(
244-
poetry: string,
245-
_prefix: string,
246-
): Map<string, PythonCommandRunConfiguration[]> | undefined {
247-
const shellActivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
248-
249-
shellActivation.set(ShellConstants.BASH, [{ executable: poetry, args: ['shell'] }]);
250-
shellActivation.set(ShellConstants.ZSH, [{ executable: poetry, args: ['shell'] }]);
251-
shellActivation.set(ShellConstants.SH, [{ executable: poetry, args: ['shell'] }]);
252-
shellActivation.set(ShellConstants.GITBASH, [{ executable: poetry, args: ['shell'] }]);
253-
shellActivation.set(ShellConstants.FISH, [{ executable: poetry, args: ['shell'] }]);
254-
shellActivation.set(ShellConstants.PWSH, [{ executable: poetry, args: ['shell'] }]);
255-
if (isWindows()) {
256-
shellActivation.set(ShellConstants.CMD, [{ executable: poetry, args: ['shell'] }]);
257-
}
258-
shellActivation.set(ShellConstants.NU, [{ executable: poetry, args: ['shell'] }]);
259-
shellActivation.set('unknown', [{ executable: poetry, args: ['shell'] }]);
260-
return shellActivation;
261-
}
262-
263-
function createShellDeactivation(): Map<string, PythonCommandRunConfiguration[]> {
264-
const shellDeactivation: Map<string, PythonCommandRunConfiguration[]> = new Map();
265-
266-
// Poetry doesn't have a standard deactivation command like venv does
267-
// The best approach is to exit the shell or start a new one
268-
shellDeactivation.set('unknown', [{ executable: 'exit' }]);
269-
270-
shellDeactivation.set(ShellConstants.BASH, [{ executable: 'exit' }]);
271-
shellDeactivation.set(ShellConstants.ZSH, [{ executable: 'exit' }]);
272-
shellDeactivation.set(ShellConstants.SH, [{ executable: 'exit' }]);
273-
shellDeactivation.set(ShellConstants.GITBASH, [{ executable: 'exit' }]);
274-
shellDeactivation.set(ShellConstants.FISH, [{ executable: 'exit' }]);
275-
shellDeactivation.set(ShellConstants.PWSH, [{ executable: 'exit' }]);
276-
shellDeactivation.set(ShellConstants.CMD, [{ executable: 'exit' }]);
277-
shellDeactivation.set(ShellConstants.NU, [{ executable: 'exit' }]);
278-
279-
return shellDeactivation;
280-
}
281-
282-
function nativeToPythonEnv(
225+
async function nativeToPythonEnv(
283226
info: NativeEnvInfo,
284227
api: PythonEnvironmentApi,
285228
manager: EnvironmentManager,
286229
_poetry: string,
287-
): PythonEnvironment | undefined {
230+
): Promise<PythonEnvironment | undefined> {
288231
if (!(info.prefix && info.executable && info.version)) {
289232
traceError(`Incomplete poetry environment info: ${JSON.stringify(info)}`);
290233
return undefined;
@@ -294,9 +237,6 @@ function nativeToPythonEnv(
294237
const name = info.name || info.displayName || path.basename(info.prefix);
295238
const displayName = info.displayName || `poetry (${sv})`;
296239

297-
const shellActivation = createShellActivation(_poetry, info.prefix);
298-
const shellDeactivation = createShellDeactivation();
299-
300240
// Check if this is a global Poetry virtualenv by checking if it's in Poetry's virtualenvs directory
301241
// We need to use path.normalize() to ensure consistent path format comparison
302242
const normalizedPrefix = path.normalize(info.prefix);
@@ -319,6 +259,10 @@ function nativeToPythonEnv(
319259
}
320260
}
321261

262+
// Get generic python environment info to access shell activation/deactivation commands following Poetry 2.0+ dropping the `shell` command
263+
const binDir = path.dirname(info.executable);
264+
const { shellActivation, shellDeactivation } = await getShellActivationCommands(binDir);
265+
322266
const environment: PythonEnvironmentInfo = {
323267
name: name,
324268
displayName: displayName,
@@ -374,14 +318,16 @@ export async function refreshPoetry(
374318

375319
const collection: PythonEnvironment[] = [];
376320

377-
envs.forEach((e) => {
378-
if (poetry) {
379-
const environment = nativeToPythonEnv(e, api, manager, poetry);
380-
if (environment) {
381-
collection.push(environment);
321+
await Promise.all(
322+
envs.map(async (e) => {
323+
if (poetry) {
324+
const environment = await nativeToPythonEnv(e, api, manager, poetry);
325+
if (environment) {
326+
collection.push(environment);
327+
}
382328
}
383-
}
384-
});
329+
}),
330+
);
385331

386332
return sortEnvironments(collection);
387333
}

0 commit comments

Comments
 (0)