Skip to content

Commit 6c8eddd

Browse files
authored
Fix display and discovery for interpreters included via settings or found in /opt/python (#7015)
### Summary - addresses #6270 - addresses #7058 #### Implementation - set the environment `source` to `PythonEnvSource.UserSettings` for interpreters specified via `python.interpreters.include`/`python.interpreters.override` for both the JS and Native locators - ensure user settings-specified interpreters are tagged as Custom instead of Unknown - if an interpreter was added via the settings, but is a Venv, it will show as a Venv instead of Custom; same goes for Uv/Conda/etc. - ensure `/opt/python`-installed interpreters tagged as Global instead of Unknown #### Known Issues See screenshots in the QA Notes section below for examples - when using the Native locator, the Python: Select Interpreter quickpick does not display as much information as when the JS locator is used. In particular, text like "custom" is not included ### Release Notes #### New Features - Python interpreters installed at `/opt/python` or included via interpreter settings are no longer tagged as "Unknown" (#6270) #### Bug Fixes - Fixed issue with missing or duplicated Python interpreters when installed in `opt/python` or included via interpreter settings (#7058) ### QA Notes @:interpreters #### Setup - ensure at least one python installation exists at `/opt/python`, - e.g. `/opt/python/3.10.4/bin/python` - ensure at least one python installation is specified in `python.interpreters.include` - e.g. `~/scratch/3.10.4/bin/python` exists and the following is set: ``` "python.interpreters.include": [ "~/scratch" ], ``` #### Settings+UI Combinations 1. "python.locator": "js" and "console.multipleConsoleSessions": false ##### Interpreter Dropdown <img width="386" alt="jslocator+nomulticonsole+interpreterdropdown" src="https://github.com/user-attachments/assets/7b941ea0-1308-4012-8b81-64ac4003203f" /> ##### Python: Select Interpreter Quickpick (`console.multipleConsoleSessions` can be true or false) <img width="607" alt="jslocator+pythonquickpick" src="https://github.com/user-attachments/assets/27a74920-e6ff-4cc7-a54a-0d70815b9c4e" /> 2. "python.locator": "js" and "console.multipleConsoleSessions": true ##### Multi Console Start Session Quickpick <img width="612" alt="jslocator+multiconsole+startsessionquickpick" src="https://github.com/user-attachments/assets/1bbc9f68-e2a5-4f21-a46b-6c796d5e6a6f" /> 3. "python.locator": "native" and "console.multipleConsoleSessions": false ##### Interpreter Dropdown <img width="383" alt="locatornative_muticonsolefalse_interpreterdropdown" src="https://github.com/user-attachments/assets/c7626328-ce33-43d4-8129-63db24ceef46" /> ##### Python: Select Interpreter Quickpick (`console.multipleConsoleSessions` can be true or false) <img width="609" alt="locatornative_quickpick" src="https://github.com/user-attachments/assets/eb676ba6-9a15-4792-9382-e426c07207ab" /> 4. "python.locator": "native" and "console.multipleConsoleSessions": true ##### Multi Console Start Session Quickpick <img width="610" alt="locatornative_muticonsoletrue" src="https://github.com/user-attachments/assets/70aad74d-f4c9-47fe-a8ea-8a2ad437e8f9" /> This change is also reflected in the dropdown in the Project Wizard, which retrieves the same information as the Interpreter Dropdown / Start Session Quickpick: <img width="707" alt="image" src="https://github.com/user-attachments/assets/686ee754-0e53-4505-b586-fa6672a2e053" />
1 parent 793e6ca commit 6c8eddd

File tree

18 files changed

+410
-116
lines changed

18 files changed

+410
-116
lines changed

extensions/positron-python/src/client/common/stringUtils.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,14 @@ export function replaceAll(source: string, substr: string, newSubstr: string): s
4444

4545
return source.replace(new RegExp(escapeRegExp(substr), 'g'), newSubstr);
4646
}
47+
48+
// --- Start Positron ---
49+
/**
50+
* Returns the shortest string from an array of strings.
51+
* @param strings - The strings to compare.
52+
* @returns The shortest string.
53+
*/
54+
export function getShortestString(strings: string[]): string {
55+
return strings.reduce((a, b) => (a.length <= b.length ? a : b));
56+
}
57+
// --- End Positron ---

extensions/positron-python/src/client/interpreter/configuration/environmentTypeComparer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,9 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] {
331331
EnvironmentType.MicrosoftStore,
332332
EnvironmentType.Global,
333333
EnvironmentType.System,
334+
// --- Start Positron ---
335+
EnvironmentType.Custom,
336+
// --- End Positron ---
334337
EnvironmentType.Unknown,
335338
];
336339
}

extensions/positron-python/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,10 @@ function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) {
727727
return EnvGroups.Workspace;
728728
}
729729
switch (item.interpreter.envType) {
730+
// --- Start Positron ---
731+
case EnvironmentType.Custom:
732+
return EnvGroups.Global;
733+
// --- End Positron ---
730734
case EnvironmentType.Global:
731735
case EnvironmentType.System:
732736
case EnvironmentType.Unknown:

extensions/positron-python/src/client/positron/interpreterSettings.ts

Lines changed: 59 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,14 @@ function getOverrideInterpreters(): string[] {
9090
* @returns List of custom environment directories to look for environments.
9191
*/
9292
export function getCustomEnvDirs(): string[] {
93-
const overrideDirs = getOverrideInterpreters();
94-
if (overrideDirs.length > 0) {
95-
return mapInterpretersToInstallDirs(overrideDirs);
93+
const overrideInterpreters = getOverrideInterpreters();
94+
if (overrideInterpreters.length > 0) {
95+
return mapInterpretersToInstallDirs(overrideInterpreters);
9696
}
9797

98-
const includeDirs = getIncludedInterpreters();
99-
if (includeDirs.length > 0) {
100-
return mapInterpretersToInstallDirs(includeDirs);
98+
const includedInterpreters = getIncludedInterpreters();
99+
if (includedInterpreters.length > 0) {
100+
return mapInterpretersToInstallDirs(includedInterpreters);
101101
}
102102

103103
return [];
@@ -149,6 +149,19 @@ export function shouldIncludeInterpreter(interpreterPath: string): boolean {
149149
return true;
150150
}
151151

152+
/**
153+
* Check if an interpreter path is a custom environment.
154+
* An interpreter is a custom environment if it exists in any of the custom search directories.
155+
* @param interpreterPath The interpreter path to check
156+
* @returns Whether the interpreter is a custom environment.
157+
*/
158+
export async function isCustomEnvironment(interpreterPath: string): Promise<boolean> {
159+
const overrideInterpreters = getOverrideInterpreters();
160+
const includeInterpreters = getIncludedInterpreters();
161+
const customDirs = mapInterpretersToInstallDirs([...overrideInterpreters, ...includeInterpreters]);
162+
return customDirs.some((dir) => isParentPath(interpreterPath, dir));
163+
}
164+
152165
/**
153166
* Checks if an interpreter path is included in the settings.
154167
* @param interpreterPath The interpreter path to check
@@ -287,54 +300,52 @@ export function printInterpreterDebugInfo(interpreters: PythonEnvironment[]): vo
287300
/**
288301
* Maps a list of interpreter paths to their installation directories.
289302
* @param interpreterPaths List of interpreter paths to map to their installation directories.
290-
* @returns
303+
* @returns List of unique installation directories.
291304
*/
292305
function mapInterpretersToInstallDirs(interpreterPaths: string[]): string[] {
293-
return interpreterPaths.map((interpreterPath) => {
294-
// If it's already a directory, return it as-is.
295-
if (isDirectorySync(interpreterPath)) {
296-
return interpreterPath;
297-
}
306+
return Array.from(
307+
new Set(
308+
interpreterPaths.map((interpreterPath) => {
309+
// If it's already a directory, return it as-is.
310+
if (isDirectorySync(interpreterPath)) {
311+
return interpreterPath;
312+
}
298313

299-
// If it's a file, we need to return the installation directory so that the Python locators can find it.
300-
// e.g. ~/scratch/3.10.4/bin/python -> ~/scratch/3.10.4
301-
// The locators expect a list of environment directories and don't seem to handle individual interpreter files.
302-
// The installation directory is the grandparent directory, which upholds the JS locator's DEFAULT_SEARCH_DEPTH of 2
303-
// see extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/userSpecifiedEnvLocator.ts
304-
// The Native Python Locator seems to use the same search depth of 2, although not explicitly documented in the python extension.
305-
let parentDir: string | undefined;
306-
let installDir: string | undefined;
307-
try {
308-
// parentDir tends to be the bin directory, which is the parent of the interpreter file.
309-
parentDir = path.dirname(interpreterPath);
310-
// installDir tends to be the python version directory, AKA the installation directory, which is the parent of the bin directory.
311-
installDir = path.dirname(parentDir);
312-
} catch (error) {
313-
traceError(
314-
`[mapInterpretersToInterpreterDirs]: Failed to get install directory for Python interpreter ${interpreterPath}`,
315-
error,
316-
);
317-
}
314+
// If it's a file, we need to return the installation directory so that the Python locators can find it.
315+
// e.g. ~/scratch/3.10.4/bin/python -> ~/scratch/3.10.4
316+
let parentDir: string | undefined;
317+
let installDir: string | undefined;
318+
try {
319+
parentDir = path.dirname(interpreterPath);
320+
installDir = path.dirname(parentDir);
321+
} catch (error) {
322+
traceError(
323+
`[mapInterpretersToInterpreterDirs]: Failed to get install directory for Python interpreter ${interpreterPath}`,
324+
error,
325+
);
326+
}
318327

319-
if (installDir) {
320-
traceVerbose(
321-
`[mapInterpretersToInterpreterDirs]: Mapped ${interpreterPath} to installation directory ${installDir}`,
322-
);
323-
return installDir;
324-
}
328+
if (installDir) {
329+
traceVerbose(
330+
`[mapInterpretersToInterpreterDirs]: Mapped ${interpreterPath} to installation directory ${installDir}`,
331+
);
332+
return installDir;
333+
}
325334

326-
if (parentDir) {
327-
traceInfo(
328-
`[mapInterpretersToInterpreterDirs]: Expected ${interpreterPath} to be located in a Python installation directory. It may not be discoverable.`,
329-
);
330-
return parentDir;
331-
}
335+
if (parentDir) {
336+
traceInfo(
337+
`[mapInterpretersToInterpreterDirs]: Expected ${interpreterPath} to be located in a Python installation directory. It may not be discoverable.`,
338+
);
339+
return parentDir;
340+
}
332341

333-
traceInfo(
334-
`[mapInterpretersToInterpreterDirs]: Unable to map ${interpreterPath} to an installation directory. It may not be discoverable.`,
335-
);
336-
return interpreterPath;
337-
});
342+
traceInfo(
343+
`[mapInterpretersToInterpreterDirs]: Unable to map ${interpreterPath} to an installation directory. It may not be discoverable.`,
344+
);
345+
return interpreterPath;
346+
}),
347+
),
348+
);
338349
}
339350

340351
/**

extensions/positron-python/src/client/pythonEnvironments/base/info/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ export enum PythonEnvSource {
102102
*/
103103
WindowsRegistry = 'windows registry',
104104
// If source turns out to be useful we will expand this enum to contain more details sources.
105+
// --- Start Positron ---
106+
/**
107+
* Environment was found via user settings
108+
*/
109+
UserSettings = 'user settings',
110+
// --- End Positron ---
105111
}
106112

107113
/**

extensions/positron-python/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { traceError } from '../../../../logging';
3030
// --- Start Positron ---
3131
import { getCustomEnvDirs } from '../../../../positron/interpreterSettings';
3232
import { traceVerbose } from '../../../../logging';
33+
import { ADDITIONAL_POSIX_BIN_PATHS } from '../../../common/posixUtils';
34+
import { PythonEnvSource } from '../../info/index';
3335
// --- End Positron ---
3436

3537
const PYTHON_ENV_TOOLS_PATH = isWindows()
@@ -53,6 +55,9 @@ export interface NativeEnvInfo {
5355
project?: string;
5456
arch?: 'x64' | 'x86';
5557
symlinks?: string[];
58+
// --- Start Positron ---
59+
source?: PythonEnvSource[];
60+
// --- End Positron ---
5661
}
5762

5863
export interface NativeEnvManagerInfo {
@@ -463,17 +468,17 @@ function getEnvironmentDirs(): string[] {
463468
* Gets the list of additional directories to add to environment directories.
464469
* @returns List of directories to add to environment directories.
465470
*/
466-
function getAdditionalEnvDirs(): string[] {
471+
export function getAdditionalEnvDirs(): string[] {
467472
const additionalDirs: string[] = [];
468473

469474
// Add additional dirs to search for Python environments on non-Windows platforms.
475+
// See JS locator equivalent `getAdditionalPosixBinaries` in extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts
470476
if (!isWindows()) {
471-
// /opt/python is a recommended Python installation location on Posit Workbench.
472-
// see: https://docs.posit.co/ide/server-pro/python/installing_python.html
473-
additionalDirs.push('/opt/python');
477+
additionalDirs.push(...ADDITIONAL_POSIX_BIN_PATHS);
474478
}
475479

476480
// Add user-specified Python search directories.
481+
// See JS locator equivalent in extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/userSpecifiedEnvLocator.ts
477482
const customEnvDirs = getCustomEnvDirs();
478483
additionalDirs.push(...customEnvDirs);
479484

extensions/positron-python/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export enum NativePythonEnvironmentKind {
1616
Poetry = 'Poetry',
1717
// --- Start Positron ---
1818
Uv = 'Uv',
19+
Custom = 'Custom',
1920
// --- End Positron ---
2021
MacPythonOrg = 'MacPythonOrg',
2122
MacCommandLineTools = 'MacCommandLineTools',
@@ -47,6 +48,9 @@ const mapping = new Map<NativePythonEnvironmentKind, PythonEnvKind>([
4748
[NativePythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System],
4849
[NativePythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System],
4950
[NativePythonEnvironmentKind.MacXCode, PythonEnvKind.System],
51+
// --- Start Positron ---
52+
[NativePythonEnvironmentKind.Custom, PythonEnvKind.Custom],
53+
// --- End Positron ---
5054
]);
5155

5256
export function categoryToKind(category?: NativePythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind {

extensions/positron-python/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
// --- Start Positron ---
5-
import path from 'path';
6-
// --- End Positron ---
7-
84
import * as os from 'os';
95
import { gte } from 'semver';
106
import { PythonEnvKind, PythonEnvSource } from '../../info';
117
import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator';
8+
// eslint-disable-next-line import/no-duplicates
129
import { commonPosixBinPaths, getPythonBinFromPosixPaths } from '../../../common/posixUtils';
1310
import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv';
1411
import { getOSType, OSType } from '../../../../common/utils/platform';
@@ -17,7 +14,11 @@ import { traceError, traceInfo, traceVerbose } from '../../../../logging';
1714
import { StopWatch } from '../../../../common/utils/stopWatch';
1815

1916
// --- Start Positron ---
20-
import { findInterpretersInDir, looksLikeBasicGlobalPython } from '../../../common/commonUtils';
17+
// eslint-disable-next-line import/order
18+
import path from 'path';
19+
// eslint-disable-next-line import/no-duplicates
20+
import { ADDITIONAL_POSIX_BIN_PATHS } from '../../../common/posixUtils';
21+
import { findInterpretersInDir } from '../../../common/commonUtils';
2122
// --- End Positron ---
2223

2324
export class PosixKnownPathsLocator extends Locator<BasicEnvInfo> {
@@ -44,15 +45,19 @@ export class PosixKnownPathsLocator extends Locator<BasicEnvInfo> {
4445
const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname));
4546

4647
// --- Start Positron ---
47-
const additionalDirs = getAdditionalPosixDirs();
48-
for await (const dir of additionalDirs) {
49-
knownDirs.push(dir);
50-
}
48+
const additionalDirs = await getAdditionalPosixBinDirs();
49+
knownDirs.push(...additionalDirs);
5150
// --- End Positron ---
5251

5352
let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs);
5453
traceVerbose(`Found ${pythonBinaries.length} python binaries in posix paths`);
5554

55+
// --- Start Positron ---
56+
traceVerbose(
57+
`[PosixKnownPathsLocator] Python binaries found in posix paths: ${pythonBinaries.join(', ')}`,
58+
);
59+
// --- End Positron ---
60+
5661
// Filter out MacOS system installs of Python 2 if necessary.
5762
if (isMacPython2Deprecated) {
5863
pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary));
@@ -78,7 +83,7 @@ export class PosixKnownPathsLocator extends Locator<BasicEnvInfo> {
7883

7984
// --- Start Positron ---
8085
/**
81-
* Gets additional directories to look for Python binaries on Posix systems.
86+
* Gets additional Python bin dirs on Posix systems.
8287
*
8388
* For example, `/opt/python/3.10.4/bin` will be returned if the machine has Python 3.10.4 installed
8489
* in `/opt/python/3.10.4/bin/python`.
@@ -90,20 +95,15 @@ export class PosixKnownPathsLocator extends Locator<BasicEnvInfo> {
9095
* Default is 2 levels.
9196
* @returns Paths to Python binaries found in additional locations for Posix systems.
9297
*/
93-
export async function* getAdditionalPosixDirs(searchDepth = 2): AsyncGenerator<string> {
94-
const additionalLocations = [
95-
// /opt/python is a recommended Python installation location on Posit Workbench.
96-
// see: https://docs.posit.co/ide/server-pro/python/installing_python.html
97-
'/opt/python',
98-
];
99-
for (const location of additionalLocations) {
100-
const additionalDirs = findInterpretersInDir(location, searchDepth);
101-
for await (const dir of additionalDirs) {
102-
const { filename } = dir;
103-
if (await looksLikeBasicGlobalPython(filename)) {
104-
yield path.dirname(filename);
105-
}
98+
async function getAdditionalPosixBinDirs(searchDepth = 2): Promise<string[]> {
99+
const additionalDirs = [];
100+
for (const location of ADDITIONAL_POSIX_BIN_PATHS) {
101+
const executables = findInterpretersInDir(location, searchDepth);
102+
for await (const entry of executables) {
103+
const { filename } = entry;
104+
additionalDirs.push(path.dirname(filename));
106105
}
107106
}
107+
return Array.from(new Set(additionalDirs));
108108
}
109109
// --- End Positron ---

0 commit comments

Comments
 (0)