Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/features/common/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { identifyTerminalShell } from './shellDetector';

export function isActivatableEnvironment(environment: PythonEnvironment): boolean {
return !!environment.execInfo?.activation || !!environment.execInfo?.shellActivation;
return !!environment.execInfo?.activation || (environment.execInfo?.shellActivation?.size ?? 0) > 0;
}

export function isActivatedRunAvailable(environment: PythonEnvironment): boolean {
Expand Down
42 changes: 23 additions & 19 deletions src/managers/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,30 +115,34 @@ export async function getShellActivationCommands(binDir: string): Promise<{
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' }]);
}
// Base interpreters (for example `uv python install` toolchains) share this layout but have no
// activation scripts, so only offer activation for scripts that exist, like the shells below.
if (await fs.pathExists(path.join(binDir, 'activate'))) {
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.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.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.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.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' }]);
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, buildPwshActivationCommands(path.join(binDir, 'Activate.ps1')));
Expand Down
26 changes: 26 additions & 0 deletions src/test/features/common/activation.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import assert from 'assert';
import { PythonEnvironment } from '../../../api';
import { isActivatableEnvironment } from '../../../features/common/activation';

suite('isActivatableEnvironment', () => {
function env(execInfo: Partial<PythonEnvironment['execInfo']>): PythonEnvironment {
return { execInfo: { run: { executable: 'python' }, ...execInfo } } as PythonEnvironment;
}

test('Environment without activation is not activatable', () => {
assert.strictEqual(isActivatableEnvironment(env({})), false);
});

test('Environment with an empty shell activation map is not activatable', () => {
assert.strictEqual(isActivatableEnvironment(env({ shellActivation: new Map() })), false);
});

test('Environment with shell activation commands is activatable', () => {
const shellActivation = new Map([['bash', [{ executable: 'source', args: ['activate'] }]]]);
assert.strictEqual(isActivatableEnvironment(env({ shellActivation })), true);
});

test('Environment with an activation command is activatable', () => {
assert.strictEqual(isActivatableEnvironment(env({ activation: [{ executable: 'activate' }] })), true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ suite('getShellActivationCommands', () => {
const pwshActivation = result.shellActivation.get(ShellConstants.PWSH);

assert.ok(pwshActivation, 'PowerShell activation should be defined');
assert.strictEqual(pwshActivation.length, 1, 'Should have only 1 command: activate (no Set-ExecutionPolicy)');
assert.strictEqual(
pwshActivation.length,
1,
'Should have only 1 command: activate (no Set-ExecutionPolicy)',
);
assert.strictEqual(pwshActivation[0].executable, '&');
assert.ok(pwshActivation[0].args);
assert.ok(
Expand All @@ -106,7 +110,11 @@ suite('getShellActivationCommands', () => {
const pwshActivation = result.shellActivation.get(ShellConstants.PWSH);

assert.ok(pwshActivation, 'PowerShell activation should be defined');
assert.strictEqual(pwshActivation.length, 1, 'Should have only 1 command: activate (no Set-ExecutionPolicy)');
assert.strictEqual(
pwshActivation.length,
1,
'Should have only 1 command: activate (no Set-ExecutionPolicy)',
);
assert.strictEqual(pwshActivation[0].executable, '&');
assert.ok(pwshActivation[0].args);
assert.ok(
Expand All @@ -131,6 +139,7 @@ suite('getShellActivationCommands', () => {
suite('Other shells are not affected by execution policy change', () => {
test('Bash activation does not include Set-ExecutionPolicy', async () => {
isWindowsStub.returns(false);
await fs.writeFile(path.join(tmpDir, 'activate'), '');

const result = await getShellActivationCommands(tmpDir);
const bashActivation = result.shellActivation.get(ShellConstants.BASH);
Expand All @@ -156,6 +165,7 @@ suite('getShellActivationCommands', () => {
suite('Windows unknown shell fallback', () => {
test('Windows unknown shell uses activate without Set-ExecutionPolicy', async () => {
isWindowsStub.returns(true);
await fs.writeFile(path.join(tmpDir, 'activate'), '');

const result = await getShellActivationCommands(tmpDir);
const unknownActivation = result.shellActivation.get('unknown');
Expand All @@ -165,4 +175,52 @@ suite('getShellActivationCommands', () => {
assert.ok(unknownActivation[0].executable.endsWith('activate'));
});
});

suite('No activation without activation scripts', () => {
test('No POSIX shell activation when bin has no activate script (e.g. uv python toolchain)', async () => {
isWindowsStub.returns(false);
await fs.writeFile(path.join(tmpDir, 'python3'), '');

const result = await getShellActivationCommands(tmpDir);

for (const shell of [
'unknown',
ShellConstants.SH,
ShellConstants.BASH,
ShellConstants.GITBASH,
ShellConstants.ZSH,
ShellConstants.KSH,
]) {
assert.strictEqual(result.shellActivation.get(shell), undefined, `${shell} should not be activated`);
assert.strictEqual(
result.shellDeactivation.get(shell),
undefined,
`${shell} should not be deactivated`,
);
}
assert.strictEqual(result.shellActivation.size, 0);
});

test('No Windows unknown shell activation when Scripts has no activate script', async () => {
isWindowsStub.returns(true);

const result = await getShellActivationCommands(tmpDir);

assert.strictEqual(result.shellActivation.get('unknown'), undefined);
assert.strictEqual(result.shellActivation.size, 0);
});

test('POSIX shells are activated when the activate script exists', async () => {
isWindowsStub.returns(false);
await fs.writeFile(path.join(tmpDir, 'activate'), '');

const result = await getShellActivationCommands(tmpDir);

for (const shell of [ShellConstants.SH, ShellConstants.BASH, ShellConstants.ZSH]) {
assert.deepStrictEqual(result.shellActivation.get(shell), [
{ executable: 'source', args: [path.join(tmpDir, 'activate')] },
]);
}
});
});
});