Skip to content

Commit 627b3d7

Browse files
python: show unsupported interpreters (#6934)
Addresses #5166. This PR: - stops hiding unsupported interpreters from dropdowns - adds visually distinct ways of indicating when an interpreter has an unsupported version (too new or too old) - adds a diagnostic that pops up when an unsupported python is selected anyway Screenshots: <img width="878" alt="image" src="https://github.com/user-attachments/assets/1c8c9f7c-896b-4995-8a37-24c29fd3c00d" /> <img width="618" alt="image" src="https://github.com/user-attachments/assets/003bb74d-47d5-4a38-8b6b-232f7a7fbcff" /> <img width="463" alt="image" src="https://github.com/user-attachments/assets/772e6939-50ab-48f2-b842-789372c4f869" /> ### Release Notes <!-- Optionally, replace `N/A` with text to be included in the next release notes. The `N/A` bullets are ignored. If you refer to one or more Positron issues, these issues are used to collect information about the feature or bugfix, such as the relevant language pack as determined by Github labels of type `lang: `. The note will automatically be tagged with the language. These notes are typically filled by the Positron team. If you are an external contributor, you may ignore this section. --> #### New Features - #5166 Unsupported versions of Python are now selectable with a warning. #### Bug Fixes - N/A ### QA Notes <!-- Add additional information for QA on how to validate the change, paying special attention to the level of risk, adjacent areas that could be affected by the change, and any important contextual information not present in the linked issues. --> Install Python versions that are 3.8 or below, or 3.14 or above. They should show up in the multisession picker and be labeled `(Unsupported)`. They should also show up in the Python: Select Interpreter picker and be grouped into their own group at the bottom. If clicked, most likely you will be prompted to install ipykernel. If ipykernel is already installed, you should get a diagnostic message that warns you it's unsupported. @:console @:interpreter @:new-project-wizard @:sessions
1 parent 2d3c8cb commit 627b3d7

File tree

15 files changed

+262
-49
lines changed

15 files changed

+262
-49
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2025 Posit Software, PBC. All rights reserved.
3+
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
// Similar to the macPythonInterpreter diagnostic.
7+
8+
// eslint-disable-next-line max-classes-per-file
9+
import { inject, injectable } from 'inversify';
10+
import { DiagnosticSeverity, l10n, Uri } from 'vscode';
11+
import { IDisposableRegistry, Resource } from '../../../common/types';
12+
import { IInterpreterService } from '../../../interpreter/contracts';
13+
import { isVersionSupported } from '../../../interpreter/configuration/environmentTypeComparer';
14+
import { IServiceContainer } from '../../../ioc/types';
15+
import { BaseDiagnostic, BaseDiagnosticsService } from '../base';
16+
import { IDiagnosticsCommandFactory } from '../commands/types';
17+
import { DiagnosticCodes } from '../constants';
18+
import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler';
19+
import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../types';
20+
import { Common } from '../../../common/utils/localize';
21+
22+
const messages = {
23+
[DiagnosticCodes.UnsupportedPythonVersion]: l10n.t(
24+
'The selected Python version {0} is not supported. Some features may not work as expected. Select a different session for the best experience.',
25+
),
26+
};
27+
28+
export class UnsupportedPythonVersionDiagnostic extends BaseDiagnostic {
29+
constructor(code: DiagnosticCodes.UnsupportedPythonVersion, resource: Resource, version: string) {
30+
super(
31+
code,
32+
messages[code].format(version),
33+
DiagnosticSeverity.Error,
34+
DiagnosticScope.WorkspaceFolder,
35+
resource,
36+
true,
37+
'always',
38+
);
39+
}
40+
}
41+
42+
export const UnsupportedPythonVersionServiceId = 'UnsupportedPythonVersionServiceId';
43+
44+
@injectable()
45+
export class UnsupportedPythonVersionService extends BaseDiagnosticsService {
46+
protected changeThrottleTimeout = 1000;
47+
48+
private timeOut?: NodeJS.Timeout | number;
49+
50+
constructor(
51+
@inject(IServiceContainer) serviceContainer: IServiceContainer,
52+
@inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry,
53+
) {
54+
super([DiagnosticCodes.UnsupportedPythonVersion], serviceContainer, disposableRegistry, true);
55+
this.addPythonEnvChangedHandler();
56+
}
57+
58+
public dispose(): void {
59+
if (this.timeOut && typeof this.timeOut !== 'number') {
60+
clearTimeout(this.timeOut);
61+
this.timeOut = undefined;
62+
}
63+
}
64+
65+
public async diagnose(resource: Resource): Promise<IDiagnostic[]> {
66+
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
67+
const interpreter = await interpreterService.getActiveInterpreter(resource);
68+
if (!interpreter?.version?.raw || isVersionSupported(interpreter.version)) {
69+
return [];
70+
}
71+
return [
72+
new UnsupportedPythonVersionDiagnostic(
73+
DiagnosticCodes.UnsupportedPythonVersion,
74+
resource,
75+
interpreter.version.raw,
76+
),
77+
];
78+
}
79+
80+
protected async onHandle(diagnostics: IDiagnostic[]): Promise<void> {
81+
if (diagnostics.length === 0) {
82+
return;
83+
}
84+
const messageService = this.serviceContainer.get<IDiagnosticHandlerService<MessageCommandPrompt>>(
85+
IDiagnosticHandlerService,
86+
DiagnosticCommandPromptHandlerServiceId,
87+
);
88+
await Promise.all(
89+
diagnostics.map(async (diagnostic) => {
90+
const canHandle = await this.canHandle(diagnostic);
91+
const shouldIgnore = await this.filterService.shouldIgnoreDiagnostic(diagnostic.code);
92+
if (!canHandle || shouldIgnore) {
93+
return;
94+
}
95+
const commandPrompts = this.getCommandPrompts(diagnostic);
96+
await messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message });
97+
}),
98+
);
99+
}
100+
101+
protected addPythonEnvChangedHandler(): void {
102+
const disposables = this.serviceContainer.get<IDisposableRegistry>(IDisposableRegistry);
103+
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
104+
disposables.push(interpreterService.onDidChangeInterpreter((e) => this.onDidChangeEnvironment(e)));
105+
}
106+
107+
protected async onDidChangeEnvironment(resource?: Uri): Promise<void> {
108+
if (this.timeOut && typeof this.timeOut !== 'number') {
109+
clearTimeout(this.timeOut);
110+
this.timeOut = undefined;
111+
}
112+
this.timeOut = setTimeout(() => {
113+
this.timeOut = undefined;
114+
this.diagnose(resource)
115+
.then((diagnostics) => this.handle(diagnostics))
116+
.ignoreErrors();
117+
}, this.changeThrottleTimeout);
118+
}
119+
120+
private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] {
121+
const commandFactory = this.serviceContainer.get<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory);
122+
switch (diagnostic.code) {
123+
case DiagnosticCodes.UnsupportedPythonVersion: {
124+
return [
125+
{
126+
prompt: Common.selectNewSession,
127+
command: commandFactory.createCommand(diagnostic, {
128+
type: 'executeVSCCommand',
129+
options: 'workbench.action.language.runtime.openActivePicker',
130+
}),
131+
},
132+
{
133+
prompt: Common.doNotShowAgain,
134+
command: commandFactory.createCommand(diagnostic, {
135+
type: 'ignore',
136+
options: DiagnosticScope.WorkspaceFolder,
137+
}),
138+
},
139+
];
140+
}
141+
default: {
142+
throw new Error("Invalid diagnostic for 'UnsupportedPythonVersionService'");
143+
}
144+
}
145+
}
146+
}

extensions/positron-python/src/client/application/diagnostics/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export enum DiagnosticCodes {
88
InvalidDebuggerTypeDiagnostic = 'InvalidDebuggerTypeDiagnostic',
99
NoPythonInterpretersDiagnostic = 'NoPythonInterpretersDiagnostic',
1010
MacInterpreterSelected = 'MacInterpreterSelected',
11+
// --- Start Positron ---
12+
// Add a new diagnostic code for unsupported Python versions
13+
UnsupportedPythonVersion = 'UnsupportedPythonVersion',
14+
// --- End Positron ---
1115
InvalidPythonPathInDebuggerSettingsDiagnostic = 'InvalidPythonPathInDebuggerSettingsDiagnostic',
1216
InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic',
1317
EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic',

extensions/positron-python/src/client/application/diagnostics/serviceRegistry.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import {
2323
InvalidMacPythonInterpreterService,
2424
InvalidMacPythonInterpreterServiceId,
2525
} from './checks/macPythonInterpreter';
26+
// --- Start Positron ---
27+
import { UnsupportedPythonVersionService, UnsupportedPythonVersionServiceId } from './checks/unsupportedPythonVersion';
28+
// --- End Positron ---
2629
import {
2730
PowerShellActivationHackDiagnosticsService,
2831
PowerShellActivationHackDiagnosticsServiceId,
@@ -79,6 +82,14 @@ export function registerTypes(serviceManager: IServiceManager): void {
7982
InvalidMacPythonInterpreterService,
8083
InvalidMacPythonInterpreterServiceId,
8184
);
85+
// --- Start Positron ---
86+
// Add a new diagnostic service for unsupported Python versions
87+
serviceManager.addSingleton<IDiagnosticsService>(
88+
IDiagnosticsService,
89+
UnsupportedPythonVersionService,
90+
UnsupportedPythonVersionServiceId,
91+
);
92+
// --- End Positron ---
8293

8394
serviceManager.addSingleton<IDiagnosticsService>(
8495
IDiagnosticsService,

extensions/positron-python/src/client/common/application/commands.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ interface ICommandNameWithoutArgumentTypeMapping {
4343
[LSCommands.RestartLS]: [];
4444
// --- Start Positron ---
4545
[Commands.Show_Interpreter_Debug_Info]: [];
46+
// New command that opens the multisession interpreter picker
47+
['workbench.action.language.runtime.openActivePicker']: [];
4648
// --- End Positron ---
4749
}
4850

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ export const UseProposedApi = Symbol('USE_VSC_PROPOSED_API');
149149

150150
// --- Start Positron ---
151151
export const IPYKERNEL_VERSION = '>=6.19.1';
152+
// We support versions where MINIMUM_PYTHON_VERSION <= version < MAXIMUM_PYTHON_VERSION_EXCLUSIVE.
152153
export const MINIMUM_PYTHON_VERSION = { major: 3, minor: 9, patch: 0, raw: '3.9.0' } as PythonVersion;
154+
export const MAXIMUM_PYTHON_VERSION_EXCLUSIVE = { major: 3, minor: 14, patch: 0, raw: '3.14.0' } as PythonVersion;
153155
export const INTERPRETERS_INCLUDE_SETTING_KEY = 'interpreters.include';
154156
export const INTERPRETERS_EXCLUDE_SETTING_KEY = 'interpreters.exclude';
155157
export const INTERPRETERS_OVERRIDE_SETTING_KEY = 'interpreters.override';

extensions/positron-python/src/client/common/utils/localize.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ export namespace Common {
7070
export const alwaysIgnore = l10n.t('Always Ignore');
7171
export const ignore = l10n.t('Ignore');
7272
export const selectPythonInterpreter = l10n.t('Select Python Interpreter');
73+
// --- Start Positron ---
74+
// Add a new button text
75+
export const selectNewSession = l10n.t('Select a new session');
76+
// --- End Positron ---
7377
export const openLaunch = l10n.t('Open launch.json');
7478
export const useCommandPrompt = l10n.t('Use Command Prompt');
7579
export const download = l10n.t('Download');
@@ -252,6 +256,10 @@ export namespace InterpreterQuickPickList {
252256
export const create = {
253257
label: l10n.t('Create Virtual Environment...'),
254258
};
259+
// --- Start Positron ---
260+
// Add a new tooltip text
261+
export const unsupportedVersionTooltip = l10n.t('This version of Python is not supported');
262+
// --- End Positron ---
255263
}
256264

257265
export namespace OutputChannelNames {

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { arePathsSame } from '../../common/platform/fs-paths';
2626
// --- Start Positron ---
2727
import { getPyenvDir } from '../../pythonEnvironments/common/environmentManagers/pyenv';
2828
import { readFileSync, pathExistsSync, checkParentDirs } from '../../pythonEnvironments/common/externalDependencies';
29+
import { MAXIMUM_PYTHON_VERSION_EXCLUSIVE, MINIMUM_PYTHON_VERSION } from '../../common/constants';
2930
// --- End Positron ---
3031

3132
export enum EnvLocationHeuristic {
@@ -67,6 +68,15 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
6768
if (isProblematicCondaEnvironment(b)) {
6869
return -1;
6970
}
71+
// --- Start Positron ---
72+
// Unsupported versions are always less useful
73+
if (!isVersionSupported(a.version)) {
74+
return 1;
75+
}
76+
if (!isVersionSupported(b.version)) {
77+
return -1;
78+
}
79+
// --- End Positron ---
7080
// Check environment location.
7181
const envLocationComparison = compareEnvironmentLocation(a, b, this.workspaceFolderPath);
7282
if (envLocationComparison !== 0) {
@@ -141,6 +151,12 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
141151
if (isProblematicCondaEnvironment(i)) {
142152
return false;
143153
}
154+
// --- Start Positron ---
155+
// Never recommend interpreters with unsupported versions.
156+
if (!isVersionSupported(i.version)) {
157+
return false;
158+
}
159+
// --- End Positron ---
144160
if (
145161
i.envType === EnvironmentType.ActiveState &&
146162
(!i.path ||
@@ -372,4 +388,16 @@ export function getPyenvVersion(workspacePath: string | undefined): string | und
372388
}
373389
return undefined;
374390
}
391+
392+
/**
393+
* Check if a version is supported (i.e. >= the minimum supported version and < the maximum).
394+
* Also returns true if the version could not be determined.
395+
*/
396+
export function isVersionSupported(version: PythonVersion | undefined): boolean {
397+
return (
398+
!version ||
399+
(comparePythonVersionDescending(MINIMUM_PYTHON_VERSION, version) >= 0 &&
400+
comparePythonVersionDescending(MAXIMUM_PYTHON_VERSION_EXCLUSIVE, version) < 0)
401+
);
402+
}
375403
// --- End Positron ---

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ import { CreateEnv } from '../../../../common/utils/localize';
5959
import { IPythonRuntimeManager } from '../../../../positron/manager';
6060
import { showErrorMessage } from '../../../../common/vscodeApis/windowApis';
6161
import { traceError } from '../../../../logging';
62-
import { isVersionSupported, shouldIncludeInterpreter } from '../../../../positron/interpreterSettings';
63-
import { MINIMUM_PYTHON_VERSION } from '../../../../common/constants';
62+
import { shouldIncludeInterpreter } from '../../../../positron/interpreterSettings';
63+
import { isVersionSupported } from '../../environmentTypeComparer';
6464
// --- End Positron ---
6565
import { untildify } from '../../../../common/helpers';
6666
import { useEnvExtension } from '../../../../envExt/api.internal';
@@ -95,6 +95,7 @@ export namespace EnvGroups {
9595
export const Pixi = 'Pixi';
9696
// --- Start Positron ---
9797
export const Uv = 'Uv';
98+
export const Unsupported = 'Unsupported';
9899
// --- End Positron ---
99100
export const VirtualEnvWrapper = 'VirtualEnvWrapper';
100101
export const ActiveState = 'ActiveState';
@@ -340,7 +341,13 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
340341
if (activeInterpreterItem) {
341342
return activeInterpreterItem;
342343
}
343-
const firstInterpreterSuggestion = suggestions.find((s) => isInterpreterQuickPickItem(s));
344+
// --- Start Positron ---
345+
// Don't suggest unsupported interpreters
346+
// const firstInterpreterSuggestion = suggestions.find((s) => isInterpreterQuickPickItem(s));
347+
const firstInterpreterSuggestion = suggestions.find(
348+
(s) => isInterpreterQuickPickItem(s) && isVersionSupported(s.interpreter.version),
349+
);
350+
// --- End Positron ---
344351
if (firstInterpreterSuggestion) {
345352
return firstInterpreterSuggestion;
346353
}
@@ -479,6 +486,15 @@ export class SetInterpreterCommand extends BaseInterpreterSelectorCommand implem
479486
items[i].tooltip = InterpreterQuickPickList.condaEnvWithoutPythonTooltip;
480487
}
481488
}
489+
// --- Start Positron ---
490+
// Add warning label to unsupported interpreters
491+
if (isInterpreterQuickPickItem(item) && !isVersionSupported(item.interpreter.version)) {
492+
if (!items[i].label.includes(Octicons.Warning)) {
493+
items[i].label = `${Octicons.Warning} ${items[i].label}`;
494+
items[i].tooltip = InterpreterQuickPickList.unsupportedVersionTooltip;
495+
}
496+
}
497+
// --- End Positron ---
482498
});
483499
} else {
484500
if (!items.some((i) => isSpecialQuickPickItem(i) && i.label === this.noPythonInstalled.label)) {
@@ -723,6 +739,12 @@ function addSeparatorIfApplicable(
723739
}
724740

725741
function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) {
742+
// --- Start Positron ---
743+
// If the interpreter is not supported, group it in the "Unsupported" category
744+
if (!isVersionSupported(item.interpreter.version)) {
745+
return EnvGroups.Unsupported;
746+
}
747+
// --- End Positron ---
726748
if (workspacePath && isParentPath(item.path, workspacePath)) {
727749
return EnvGroups.Workspace;
728750
}
@@ -749,9 +771,6 @@ function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) {
749771
* @returns A new filter function that includes the original filter function and the additional filtering logic
750772
*/
751773
function filterWrapper(filter: ((i: PythonEnvironment) => boolean) | undefined) {
752-
return (i: PythonEnvironment) =>
753-
(filter ? filter(i) : true) &&
754-
shouldIncludeInterpreter(i.path) &&
755-
isVersionSupported(i.version, MINIMUM_PYTHON_VERSION);
774+
return (i: PythonEnvironment) => (filter ? filter(i) : true) && shouldIncludeInterpreter(i.path);
756775
}
757776
// --- End Positron ---

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

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import { traceError, traceInfo } from '../logging';
1414
import { PythonEnvironment } from '../pythonEnvironments/info';
1515
import { createPythonRuntimeMetadata } from './runtime';
1616
import { comparePythonVersionDescending } from '../interpreter/configuration/environmentTypeComparer';
17-
import { MINIMUM_PYTHON_VERSION } from '../common/constants';
18-
import { isVersionSupported, shouldIncludeInterpreter } from './interpreterSettings';
17+
import { shouldIncludeInterpreter } from './interpreterSettings';
1918
import { hasFiles } from './util';
2019

2120
/**
@@ -109,19 +108,12 @@ export async function* pythonRuntimeDiscoverer(
109108
}
110109

111110
/**
112-
* Returns a list of Python interpreters with unsupported and user-excluded interpreters removed.
111+
* Returns a list of Python interpreters with user-excluded interpreters removed.
113112
* @param interpreters The list of Python interpreters to filter.
114-
* @returns A list of Python interpreters that are supported and not user-excluded.
113+
* @returns A list of Python interpreters that are not user-excluded.
115114
*/
116115
function filterInterpreters(interpreters: PythonEnvironment[]): PythonEnvironment[] {
117116
return interpreters.filter((interpreter) => {
118-
// Check if the interpreter version is supported
119-
const isSupported = isVersionSupported(interpreter.version, MINIMUM_PYTHON_VERSION);
120-
if (!isSupported) {
121-
traceInfo(`pythonRuntimeDiscoverer: filtering out unsupported interpreter ${interpreter.path}`);
122-
return false;
123-
}
124-
125117
// Check if the interpreter is excluded by the user
126118
const shouldInclude = shouldIncludeInterpreter(interpreter.path);
127119
if (!shouldInclude) {

0 commit comments

Comments
 (0)