Skip to content

Commit

Permalink
Show user deprecated message for unsupported Python versions when lau…
Browse files Browse the repository at this point in the history
…nching the debugger (#20172)

Closed: #19799 Closes
#19988
  • Loading branch information
paulacamargo25 authored Nov 17, 2022
1 parent 78e136f commit ad46fb3
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 4 deletions.
4 changes: 4 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
49 changes: 48 additions & 1 deletion src/client/debugger/extension/adapter/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<string[]> {
if (interpreter) {
if ((interpreter.version?.major ?? 0) < 3 || (interpreter.version?.minor ?? 0) <= 6) {
this.showDeprecatedPythonMessage();
}
return interpreter.path.length > 0 ? [interpreter.path] : [];
}
return [];
Expand Down
41 changes: 38 additions & 3 deletions src/test/debugger/extension/adapter/factory.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,30 @@ 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';
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<boolean | undefined>;
let showErrorMessageStub: sinon.SinonStub;
let commandManager: ICommandManager;

const nodeExecutable = undefined;
const debugAdapterPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'lib', 'python', 'debugpy', 'adapter');
Expand Down Expand Up @@ -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<boolean | undefined>;
commandManager = mock(CommandManager);

showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage');

when(
stateFactory.createGlobalPersistentState<boolean | undefined>(debugStateKeys.doNotShowAgain, false),
).thenReturn(instance(state));

const configurationService = mock(ConfigurationService);
when(configurationService.getSettings(undefined)).thenReturn(({
experiments: { enabled: true },
Expand All @@ -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(() => {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit ad46fb3

Please sign in to comment.