diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 5d1eb52514c7..1f15bdc77bf1 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -282,6 +282,10 @@ export namespace Interpreters { 'Interpreters.installPythonTerminalMessage', '💡 Please try installing the python package using your package manager. Alternatively you can also download it from https://www.python.org/downloads', ); + export const changePythonInterpreter = localize( + 'Interpreters.changePythonInterpreter', + 'Change Python Interpreter', + ); } export namespace InterpreterQuickPickList { diff --git a/src/client/debugger/extension/adapter/factory.ts b/src/client/debugger/extension/adapter/factory.ts index 1a84b73f2003..10985fb97097 100644 --- a/src/client/debugger/extension/adapter/factory.ts +++ b/src/client/debugger/extension/adapter/factory.ts @@ -22,12 +22,25 @@ import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; import { IDebugAdapterDescriptorFactory } from '../types'; import * as nls from 'vscode-nls'; import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { Common, Interpreters } from '../../../common/utils/localize'; +import { IPersistentStateFactory } from '../../../common/types'; +import { Commands } from '../../../common/constants'; +import { ICommandManager } from '../../../common/application/types'; const localize: nls.LocalizeFunc = nls.loadMessageBundle(); +// persistent state names, exported to make use of in testing +export enum debugStateKeys { + doNotShowAgain = 'doNotShowPython36DebugDeprecatedAgain', +} + @injectable() export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFactory { - constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {} + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, + ) {} public async createDebugAdapterDescriptor( session: DebugSession, @@ -142,8 +155,42 @@ export class DebugAdapterDescriptorFactory implements IDebugAdapterDescriptorFac return this.getExecutableCommand(interpreters[0]); } + private async showDeprecatedPythonMessage() { + const notificationPromptEnabled = this.persistentState.createGlobalPersistentState( + debugStateKeys.doNotShowAgain, + false, + ); + if (notificationPromptEnabled.value) { + return; + } + const prompts = [Interpreters.changePythonInterpreter, Common.doNotShowAgain]; + const selection = await showErrorMessage( + localize( + 'Debug.deprecatedDebuggerError', + 'The debugger in the python extension no longer supports python versions minor than 3.7.', + ), + { modal: true }, + ...prompts, + ); + if (!selection) { + return; + } + if (selection == Interpreters.changePythonInterpreter) { + await this.commandManager.executeCommand(Commands.Set_Interpreter); + } + if (selection == Common.doNotShowAgain) { + // Never show the message again + await this.persistentState + .createGlobalPersistentState(debugStateKeys.doNotShowAgain, false) + .updateValue(true); + } + } + private async getExecutableCommand(interpreter: PythonEnvironment | undefined): Promise { if (interpreter) { + if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) { + this.showDeprecatedPythonMessage(); + } return interpreter.path.length > 0 ? [interpreter.path] : []; } return []; diff --git a/src/test/debugger/extension/adapter/factory.unit.test.ts b/src/test/debugger/extension/adapter/factory.unit.test.ts index 9686588b96d3..6828ea96d451 100644 --- a/src/test/debugger/extension/adapter/factory.unit.test.ts +++ b/src/test/debugger/extension/adapter/factory.unit.test.ts @@ -13,10 +13,10 @@ import { SemVer } from 'semver'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import { DebugAdapterExecutable, DebugAdapterServer, DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IPythonSettings } from '../../../../client/common/types'; +import { IPersistentStateFactory, IPythonSettings } from '../../../../client/common/types'; import { Architecture } from '../../../../client/common/utils/platform'; import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { DebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/adapter/factory'; +import { DebugAdapterDescriptorFactory, debugStateKeys } from '../../../../client/debugger/extension/adapter/factory'; import { IDebugAdapterDescriptorFactory } from '../../../../client/debugger/extension/types'; import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; @@ -24,13 +24,19 @@ import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; import { clearTelemetryReporter } from '../../../../client/telemetry'; import { EventName } from '../../../../client/telemetry/constants'; import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; +import { ICommandManager } from '../../../../client/common/application/types'; +import { CommandManager } from '../../../../client/common/application/commandManager'; use(chaiAsPromised); suite('Debugging - Adapter Factory', () => { let factory: IDebugAdapterDescriptorFactory; let interpreterService: IInterpreterService; + let stateFactory: IPersistentStateFactory; + let state: PersistentState; let showErrorMessageStub: sinon.SinonStub; + let commandManager: ICommandManager; const nodeExecutable = undefined; const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter'); @@ -62,8 +68,16 @@ suite('Debugging - Adapter Factory', () => { process.env.VSC_PYTHON_CI_TEST = undefined; rewiremock.enable(); rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); + stateFactory = mock(PersistentStateFactory); + state = mock(PersistentState) as PersistentState; + commandManager = mock(CommandManager); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + when( + stateFactory.createGlobalPersistentState(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); + const configurationService = mock(ConfigurationService); when(configurationService.getSettings(undefined)).thenReturn(({ experiments: { enabled: true }, @@ -74,7 +88,11 @@ suite('Debugging - Adapter Factory', () => { when(interpreterService.getInterpreterDetails(pythonPath)).thenResolve(interpreter); when(interpreterService.getInterpreters(anything())).thenReturn([interpreter]); - factory = new DebugAdapterDescriptorFactory(instance(interpreterService)); + factory = new DebugAdapterDescriptorFactory( + instance(commandManager), + instance(interpreterService), + instance(stateFactory), + ); }); teardown(() => { @@ -138,7 +156,24 @@ suite('Debugging - Adapter Factory', () => { await expect(promise).to.eventually.be.rejectedWith('Debug Adapter Executable not provided'); sinon.assert.calledOnce(showErrorMessageStub); }); + test('Display a message if python version is less than 3.7', async () => { + when(interpreterService.getInterpreters(anything())).thenReturn([]); + const session = createSession({}); + const deprecatedInterpreter = { + architecture: Architecture.Unknown, + path: pythonPath, + sysPrefix: '', + sysVersion: '', + envType: EnvironmentType.Unknown, + version: new SemVer('3.6.12-test'), + }; + when(state.value).thenReturn(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(deprecatedInterpreter); + await factory.createDebugAdapterDescriptor(session, nodeExecutable); + + sinon.assert.calledOnce(showErrorMessageStub); + }); test('Return Debug Adapter server if request is "attach", and port is specified directly', async () => { const session = createSession({ request: 'attach', port: 5678, host: 'localhost' }); const debugServer = new DebugAdapterServer(session.configuration.port, session.configuration.host);