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
26 changes: 23 additions & 3 deletions src/features/interpreterSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,8 @@ async function toAbsoluteInterpreterPath(interpreterPath: string, scope: Uri | u
if (path.isAbsolute(interpreterPath)) {
return interpreterPath;
}
if (!/[\\/]/.test(interpreterPath)) {
const isBareCommand = !/[\\/]/.test(interpreterPath);
if (isBareCommand) {
const onPath = await findCommandOnPath(interpreterPath);
if (onPath) {
return onPath;
Expand All @@ -596,6 +597,24 @@ async function toAbsoluteInterpreterPath(interpreterPath: string, scope: Uri | u
if (!scope) {
return interpreterPath;
}
if (isBareCommand) {
// Keep bare commands intact when they are absent from the extension host's PATH.
// The native finder can resolve them using its own environment (for example, the
// integrated terminal's shell environment).
const workspaceCandidate = path.resolve(
getWorkspaceFolder(scope)?.uri.fsPath ?? scope.fsPath,
interpreterPath,
);
try {
if ((await fs.stat(workspaceCandidate)).isDirectory()) {
return workspaceCandidate;
}
} catch {
// A missing bare name may still be resolvable by the native finder (for example, by
// an integrated-terminal environment manager), so pass it through unchanged.
}
return interpreterPath;
Comment thread
edvilme marked this conversation as resolved.
Resolved
}
const workspaceFolder = getWorkspaceFolder(scope);
return path.resolve(workspaceFolder?.uri.fsPath ?? scope.fsPath, interpreterPath);
}
Expand Down Expand Up @@ -666,6 +685,7 @@ async function tryResolveInterpreterPath(
}

if (manager && resolvedEnv) {
const isBareCommand = !/[\\/]/.test(interpreterPath);
// Create a wrapper environment that uses the user's specified path, named like the
// resolved environment so the status bar and views show a normal interpreter label.
const newEnv: PythonEnvironment = {
Expand All @@ -677,11 +697,11 @@ async function tryResolveInterpreterPath(
displayName: resolvedEnv.displayName,
version: resolved.version ?? '',
displayPath: interpreterPath,
environmentPath: Uri.file(interpreterPath),
environmentPath: isBareCommand ? resolvedEnv.environmentPath : Uri.file(interpreterPath),
sysPrefix: resolved.prefix ?? '',
execInfo: {
run: {
executable: interpreterPath,
executable: isBareCommand ? resolved.executable : interpreterPath,
},
},
};
Expand Down
37 changes: 33 additions & 4 deletions src/test/features/interpreterSelection.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -488,14 +488,16 @@ suite('Interpreter Selection - Priority Chain', () => {
});
}

test('should resolve a command name that is not on PATH against the workspace folder', async () => {
test('should pass a command name that is not on PATH to the native finder unchanged', async () => {
const workspaceUri = Uri.file(path.resolve('test', 'workspace'));
const workspaceFolder = { name: 'workspace', uri: workspaceUri } as WorkspaceFolder;
const expected = path.resolve(workspaceUri.fsPath, 'pyenvs-test-missing-python');
const command = 'pyenvs-test-missing-python';
originalPath = process.env.PATH;
process.env.PATH = '';
sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration);
sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder);
sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]);
useDefaultInterpreterPath('pyenvs-test-missing-python');
useDefaultInterpreterPath(command);
mockNativeFinder.resolve.rejects(new Error('not found'));
mockVenvManager.get.resolves(undefined);

Expand All @@ -507,7 +509,34 @@ suite('Interpreter Selection - Priority Chain', () => {
mockApi as unknown as PythonEnvironmentApi,
);

assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(expected));
assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(command));
});

test('should resolve a bare workspace directory to its absolute path', async () => {
const workspaceUri = Uri.file(await fs.mkdtemp(path.join(os.tmpdir(), 'dip-workspace-')));
const workspaceFolder = { name: 'workspace', uri: workspaceUri } as WorkspaceFolder;
const envDirectory = path.join(workspaceUri.fsPath, '.venv');
await fs.mkdir(envDirectory);
sandbox.stub(workspaceApis, 'getConfiguration').returns(createMockConfig([]) as WorkspaceConfiguration);
sandbox.stub(workspaceApis, 'getWorkspaceFolder').returns(workspaceFolder);
sandbox.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]);
useDefaultInterpreterPath('.venv');
mockNativeFinder.resolve.rejects(new Error('not found'));
mockVenvManager.get.resolves(undefined);

try {
await resolveEnvironmentByPriority(
workspaceUri,
mockEnvManagers as unknown as EnvironmentManagers,
mockProjectManager as unknown as PythonProjectManager,
mockNativeFinder as unknown as NativePythonFinder,
mockApi as unknown as PythonEnvironmentApi,
);

assert.ok(mockNativeFinder.resolve.calledOnceWithExactly(envDirectory));
} finally {
await fs.remove(workspaceUri.fsPath);
}
});

test('should treat the default value "python" as not configured', async () => {
Expand Down
Loading