Skip to content

Commit ad46fb3

Browse files
Show user deprecated message for unsupported Python versions when launching the debugger (#20172)
Closed: #19799 Closes #19988
1 parent 78e136f commit ad46fb3

3 files changed

Lines changed: 90 additions & 4 deletions

File tree

src/client/common/utils/localize.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,10 @@ export namespace Interpreters {
282282
'Interpreters.installPythonTerminalMessage',
283283
'💡 Please try installing the python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads',
284284
);
285+
export const changePythonInterpreter = localize(
286+
'Interpreters.changePythonInterpreter',
287+
'Change Python Interpreter',
288+
);
285289
}
286290

287291
export namespace InterpreterQuickPickList {

src/client/debugger/extension/adapter/factory.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,25 @@ import { AttachRequestArguments, LaunchRequestArguments } from '../../types';
2222
import { IDebugAdapterDescriptorFactory } from '../types';
2323
import * as nls from 'vscode-nls';
2424
import { showErrorMessage } from '../../../common/vscodeApis/windowApis';
25+
import { Common, Interpreters } from '../../../common/utils/localize';
26+
import { IPersistentStateFactory } from '../../../common/types';
27+
import { Commands } from '../../../common/constants';
28+
import { ICommandManager } from '../../../common/application/types';
2529

2630
const localize: nls.LocalizeFunc = nls.loadMessageBundle();
2731

32+
// persistent state names, exported to make use of in testing
33+
export enum debugStateKeys {
34+
doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain',
35+
}
36+
2837
@injectable()
2938
export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory {
30-
constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {}
39+
constructor(
40+
@inject(ICommandManager) private readonly commandManager: ICommandManager,
41+
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
42+
@inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory,
43+
) {}
3144

3245
public async createDebugAdapterDescriptor(
3346
session: DebugSession,
@@ -142,8 +155,42 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac
142155
return this.getExecutableCommand(interpreters[0]);
143156
}
144157

158+
private async showDeprecatedPythonMessage() {
159+
const notificationPromptEnabled = this.persistentState.createGlobalPersistentState(
160+
debugStateKeys.doNotShowAgain,
161+
false,
162+
);
163+
if (notificationPromptEnabled.value) {
164+
return;
165+
}
166+
const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain];
167+
const selection = await showErrorMessage(
168+
localize(
169+
'Debug.deprecatedDebuggerError',
170+
'The debugger in the python extension no longer supports python versions minor than 3.7.',
171+
),
172+
{ modal: true },
173+
...prompts,
174+
);
175+
if (!selection) {
176+
return;
177+
}
178+
if (selection == Interpreters.changePythonInterpreter) {
179+
await this.commandManager.executeCommand(Commands.Set_Interpreter);
180+
}
181+
if (selection == Common.doNotShowAgain) {
182+
// Never show the message again
183+
await this.persistentState
184+
.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false)
185+
.updateValue(true);
186+
}
187+
}
188+
145189
private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise<string[]> {
146190
if (interpreter) {
191+
if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) {
192+
this.showDeprecatedPythonMessage();
193+
}
147194
return interpreter.path.length > 0 ? [interpreter.path] : [];
148195
}
149196
return [];

src/test/debugger/extension/adapter/factory.unit.test.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,30 @@ import { SemVer } from 'semver';
1313
import { anything, instance, mock, verify, when } from 'ts-mockito';
1414
import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode';
1515
import { ConfigurationService } from '../../../../client/common/configuration/service';
16-
import { IPythonSettings } from '../../../../client/common/types';
16+
import { IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types';
1717
import { Architecture } from '../../../../client/common/utils/platform';
1818
import { EXTENSION_ROOT_DIR } from '../../../../client/constants';
19-
import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory';
19+
import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../../client/debugger/extension/adapter/factory';
2020
import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types';
2121
import { IInterpreterService } from '../../../../client/interpreter/contracts';
2222
import { InterpreterService } from '../../../../client/interpreter/interpreterService';
2323
import { EnvironmentType } from '../../../../client/pythonEnvironments/info';
2424
import { clearTelemetryReporter } from '../../../../client/telemetry';
2525
import { EventName } from '../../../../client/telemetry/constants';
2626
import * as windowApis from '../../../../client/common/vscodeApis/windowApis';
27+
import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState';
28+
import { ICommandManager } from '../../../../client/common/application/types';
29+
import { CommandManager } from '../../../../client/common/application/commandManager';
2730

2831
use(chaiAsPromised);
2932

3033
suite('Debugging - Adapter Factory', () => {
3134
let factory: IDebugAdapterDescriptorFactory;
3235
let interpreterService: IInterpreterService;
36+
let stateFactory: IPersistentStateFactory;
37+
let state: PersistentState<boolean | undefined>;
3338
let showErrorMessageStub: sinon.SinonStub;
39+
let commandManager: ICommandManager;
3440

3541
const nodeExecutable = undefined;
3642
const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter');
@@ -62,8 +68,16 @@ suite('Debugging - Adapter Factory', () => {
6268
process.env.VSC_PYTHON_CI_TEST = undefined;
6369
rewiremock.enable();
6470
rewiremock('@vscode/extension-telemetry').with({ default: Reporter });
71+
stateFactory = mock(PersistentStateFactory);
72+
state = mock(PersistentState) as PersistentState<boolean | undefined>;
73+
commandManager = mock(CommandManager);
74+
6575
showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage');
6676

77+
when(
78+
stateFactory.createGlobalPersistentState<boolean | undefined>(debugStateKeys.doNotShowAgain, false),
79+
).thenReturn(instance(state));
80+
6781
const configurationService = mock(ConfigurationService);
6882
when(configurationService.getSettings(undefined)).thenReturn(({
6983
experiments: { enabled: true },
@@ -74,7 +88,11 @@ suite('Debugging - Adapter Factory', () => {
7488
when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter);
7589
when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]);
7690

77-
factory = new DebugAdapterDescriptorFactory(instance(interpreterService));
91+
factory = new DebugAdapterDescriptorFactory(
92+
instance(commandManager),
93+
instance(interpreterService),
94+
instance(stateFactory),
95+
);
7896
});
7997

8098
teardown(() => {
@@ -138,7 +156,24 @@ suite('Debugging - Adapter Factory', () => {
138156
await expect(promise).to.eventually.be.rejectedWith('Debug Adapter Executable not provided');
139157
sinon.assert.calledOnce(showErrorMessageStub);
140158
});
159+
test('Display a message if python version is less than 3.7', async () => {
160+
when(interpreterService.getInterpreters(anything())).thenReturn([]);
161+
const session = createSession({});
162+
const deprecatedInterpreter = {
163+
architecture: Architecture.Unknown,
164+
path: pythonPath,
165+
sysPrefix: '',
166+
sysVersion: '',
167+
envType: EnvironmentType.Unknown,
168+
version: new SemVer('3.6.12-test'),
169+
};
170+
when(state.value).thenReturn(false);
171+
when(interpreterService.getActiveInterpreter(anything())).thenResolve(deprecatedInterpreter);
141172

173+
await factory.createDebugAdapterDescriptor(session, nodeExecutable);
174+
175+
sinon.assert.calledOnce(showErrorMessageStub);
176+
});
142177
test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => {
143178
const session = createSession({ request: 'attach', port: 5678, host: 'localhost' });
144179
const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host);

0 commit comments

Comments
 (0)