Skip to content

Suggest Jupyter extension installation when debugging dataframe objects #25341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/client/debugger/extension/adapter/activator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { IConfigurationService, IDisposableRegistry } from '../../../common/type
import { ICommandManager } from '../../../common/application/types';
import { DebuggerTypeName } from '../../constants';
import { IAttachProcessProviderFactory } from '../attachQuickPick/types';
import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory } from '../types';
import { IDebugAdapterDescriptorFactory, IDebugSessionLoggingFactory, IOutdatedDebuggerPromptFactory, IDataFrameTrackerFactory } from '../types';

@injectable()
export class DebugAdapterActivator implements IExtensionSingleActivationService {
Expand All @@ -22,6 +22,7 @@ export class DebugAdapterActivator implements IExtensionSingleActivationService
@inject(IDebugAdapterDescriptorFactory) private descriptorFactory: IDebugAdapterDescriptorFactory,
@inject(IDebugSessionLoggingFactory) private debugSessionLoggingFactory: IDebugSessionLoggingFactory,
@inject(IOutdatedDebuggerPromptFactory) private debuggerPromptFactory: IOutdatedDebuggerPromptFactory,
@inject(IDataFrameTrackerFactory) private dataFrameTrackerFactory: IDataFrameTrackerFactory,
@inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry,
@inject(IAttachProcessProviderFactory)
private readonly attachProcessProviderFactory: IAttachProcessProviderFactory,
Expand All @@ -35,6 +36,10 @@ export class DebugAdapterActivator implements IExtensionSingleActivationService
this.disposables.push(
this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.debuggerPromptFactory),
);
// Register DataFrame tracker to monitor for dataframe variables and suggest Jupyter extension
this.disposables.push(
this.debugService.registerDebugAdapterTrackerFactory(DebuggerTypeName, this.dataFrameTrackerFactory),
);

this.disposables.push(
this.debugService.registerDebugAdapterDescriptorFactory(DebuggerTypeName, this.descriptorFactory),
Expand Down
148 changes: 148 additions & 0 deletions src/client/debugger/extension/adapter/dataFrameTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import {
DebugAdapterTracker,
DebugAdapterTrackerFactory,
DebugSession,
ProviderResult,
window,
l10n,
commands,
} from 'vscode';
import { DebugProtocol } from 'vscode-debugprotocol';

import { IExtensions } from '../../../common/types';
import { JUPYTER_EXTENSION_ID } from '../../../common/constants';

/**
* Debug adapter tracker that monitors for dataframe-like variables during debugging sessions
* and suggests installing the Jupyter extension when they are detected but the Jupyter extension
* is not installed. This helps users discover the data viewer functionality when working with
* dataframes without the Jupyter extension.
*/
class DataFrameVariableTracker implements DebugAdapterTracker {
private readonly extensions: IExtensions;

/** Flag to ensure we only show the notification once per debug session to avoid spam */
private hasNotifiedAboutJupyter = false;

/**
* Known dataframe type patterns from popular Python data processing libraries.
* These patterns are matched against variable type strings in debug protocol responses.
*/
private readonly dataFrameTypes = [
'pandas.core.frame.DataFrame', // Full pandas path
'pandas.DataFrame', // Simplified pandas
'polars.DataFrame', // Polars dataframes
'cudf.DataFrame', // RAPIDS cuDF
'dask.dataframe.core.DataFrame', // Dask distributed dataframes
'modin.pandas.DataFrame', // Modin pandas-compatible
'vaex.dataframe.DataFrame', // Vaex out-of-core dataframes
'geopandas.geodataframe.GeoDataFrame', // GeoPandas geographic data
];

constructor(_session: DebugSession, extensions: IExtensions) {
this.extensions = extensions;
}

/**
* Intercepts debug protocol messages to monitor for variable responses.
* When a variables response is detected, checks for dataframe-like objects.
*
* @param message - Debug protocol message from the debug adapter
*/
public onDidSendMessage(message: DebugProtocol.Message): void {
if (this.hasNotifiedAboutJupyter) {
return; // Only notify once per debug session
}

// Check if this is a variables response from the debug protocol
if ('type' in message && message.type === 'response' && 'command' in message && message.command === 'variables') {
const response = message as unknown as DebugProtocol.VariablesResponse;
if (response.success && response.body?.variables) {
this.checkForDataFrameVariables(response.body.variables);
}
}
}

/**
* Examines an array of debug variables to detect dataframe-like objects.
* Uses multiple detection strategies: type matching, value inspection, and name heuristics.
*
* @param variables - Array of variables from debug protocol variables response
* @returns true if any dataframe-like variables were detected
*/
private checkForDataFrameVariables(variables: DebugProtocol.Variable[]): boolean {
// Check if any variable is a dataframe-like object using multiple detection methods
const hasDataFrame = variables.some((variable) =>
this.dataFrameTypes.some((dfType) =>
variable.type?.includes(dfType) ||
variable.value?.includes(dfType) ||
// Also check if the variable name suggests it's a dataframe (common naming patterns)
(variable.name?.match(/^(df|data|dataframe)/i) && variable.type?.includes('pandas'))
)
);

if (hasDataFrame) {
this.checkAndNotifyJupyterExtension();
}

return hasDataFrame;
}

/**
* Checks if the Jupyter extension is installed and shows notification if not.
* This is the core logic that determines whether the user needs the suggestion.
*/
private checkAndNotifyJupyterExtension(): void {
// Check if Jupyter extension is installed using VS Code extension API
const jupyterExtension = this.extensions.getExtension(JUPYTER_EXTENSION_ID);

if (!jupyterExtension) {
this.hasNotifiedAboutJupyter = true;
this.showJupyterInstallNotification();
}
}

/**
* Displays an information message suggesting Jupyter extension installation.
* Provides a direct action button to open the extension marketplace.
*/
private showJupyterInstallNotification(): void {
const message = l10n.t('Install Jupyter extension to inspect dataframe objects in the data viewer.');
const installAction = l10n.t('Install Jupyter Extension');
const dismissAction = l10n.t('Dismiss');

window.showInformationMessage(message, installAction, dismissAction).then((selection) => {
if (selection === installAction) {
// Open the extension marketplace for the Jupyter extension
commands.executeCommand('extension.open', JUPYTER_EXTENSION_ID);
}
});
}
}

/**
* Factory for creating DataFrameVariableTracker instances for debug sessions.
* This factory is registered with VS Code's debug adapter tracker system to
* automatically monitor all Python debug sessions for dataframe variables.
*/
@injectable()
export class DataFrameTrackerFactory implements DebugAdapterTrackerFactory {
constructor(@inject(IExtensions) private readonly extensions: IExtensions) {}

/**
* Creates a new DataFrameVariableTracker for each debug session.
* Each debug session gets its own tracker instance to maintain session-specific state.
*
* @param session - The debug session that this tracker will monitor
* @returns A new DataFrameVariableTracker instance
*/
public createDebugAdapterTracker(session: DebugSession): ProviderResult<DebugAdapterTracker> {
return new DataFrameVariableTracker(session, this.extensions);
}
}
8 changes: 8 additions & 0 deletions src/client/debugger/extension/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DebugAdapterActivator } from './adapter/activator';
import { DebugAdapterDescriptorFactory } from './adapter/factory';
import { DebugSessionLoggingFactory } from './adapter/logging';
import { OutdatedDebuggerPromptFactory } from './adapter/outdatedDebuggerPrompt';
import { DataFrameTrackerFactory } from './adapter/dataFrameTracker';
import { AttachProcessProviderFactory } from './attachQuickPick/factory';
import { IAttachProcessProviderFactory } from './attachQuickPick/types';
import { PythonDebugConfigurationService } from './configuration/debugConfigurationService';
Expand All @@ -26,6 +27,7 @@ import {
IDebugConfigurationService,
IDebugSessionLoggingFactory,
IOutdatedDebuggerPromptFactory,
IDataFrameTrackerFactory,
} from './types';

export function registerTypes(serviceManager: IServiceManager): void {
Expand Down Expand Up @@ -62,6 +64,12 @@ export function registerTypes(serviceManager: IServiceManager): void {
IOutdatedDebuggerPromptFactory,
OutdatedDebuggerPromptFactory,
);
// Register DataFrameTrackerFactory to monitor debug sessions for dataframe variables
// and suggest Jupyter extension installation when needed
serviceManager.addSingleton<IDataFrameTrackerFactory>(
IDataFrameTrackerFactory,
DataFrameTrackerFactory,
);
serviceManager.addSingleton<IAttachProcessProviderFactory>(
IAttachProcessProviderFactory,
AttachProcessProviderFactory,
Expand Down
9 changes: 9 additions & 0 deletions src/client/debugger/extension/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFac

export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {}

/** Symbol identifier for the DataFrameTrackerFactory service */
export const IDataFrameTrackerFactory = Symbol('IDataFrameTrackerFactory');

/**
* Interface for debug adapter tracker factory that monitors dataframe variables
* and suggests Jupyter extension installation when dataframes are detected.
*/
export interface IDataFrameTrackerFactory extends DebugAdapterTrackerFactory {}

export enum PythonPathSource {
launchJson = 'launch.json',
settingsJson = 'settings.json',
Expand Down
170 changes: 170 additions & 0 deletions src/test/debugger/extension/adapter/dataFrameTracker.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { expect } from 'chai';
import { instance, mock, verify, when } from 'ts-mockito';
import { Extension } from 'vscode';
import { DebugProtocol } from 'vscode-debugprotocol';

import { IExtensions } from '../../../../client/common/types';
import { JUPYTER_EXTENSION_ID } from '../../../../client/common/constants';
import { DataFrameTrackerFactory } from '../../../../client/debugger/extension/adapter/dataFrameTracker';

/**
* Test suite for DataFrame Tracker functionality.
* Tests the detection of dataframe variables in debug sessions and
* Jupyter extension installation suggestions.
*/
suite('DataFrame Tracker', () => {
let extensions: IExtensions;
let mockExtensions: IExtensions;
let trackerFactory: DataFrameTrackerFactory;

setup(() => {
mockExtensions = mock<IExtensions>();
extensions = instance(mockExtensions);
trackerFactory = new DataFrameTrackerFactory(extensions);
});

test('Should create debug adapter tracker', () => {
const mockSession = {} as any;
const tracker = trackerFactory.createDebugAdapterTracker(mockSession);
expect(tracker).to.not.be.undefined;
});

/**
* Test that pandas DataFrame variables are correctly detected
* from debug protocol variable responses.
*/
test('Should detect pandas DataFrame variable', () => {
const mockSession = {} as any;
const tracker = trackerFactory.createDebugAdapterTracker(mockSession) as any;

// Mock Jupyter extension not being installed
when(mockExtensions.getExtension(JUPYTER_EXTENSION_ID)).thenReturn(undefined);

const variablesResponse: DebugProtocol.VariablesResponse = {
type: 'response',
seq: 1,
request_seq: 1,
success: true,
command: 'variables',
body: {
variables: [
{
name: 'df',
value: '<pandas.core.frame.DataFrame object>',
type: 'pandas.core.frame.DataFrame',
variablesReference: 0,
},
{
name: 'x',
value: '42',
type: 'int',
variablesReference: 0,
}
]
}
};

// This should detect the DataFrame and try to show notification
tracker.onDidSendMessage(variablesResponse);

// Verify that extension check was called
verify(mockExtensions.getExtension(JUPYTER_EXTENSION_ID)).once();
});

/**
* Test that the tracker doesn't show notifications when Jupyter extension is already installed.
* This prevents unnecessary notifications for users who already have the data viewer available.
*/
test('Should not show notification if Jupyter extension is installed', () => {
const mockSession = {} as any;
const tracker = trackerFactory.createDebugAdapterTracker(mockSession) as any;

// Mock Jupyter extension being installed
const mockJupyterExt = mock<Extension<any>>();
when(mockExtensions.getExtension(JUPYTER_EXTENSION_ID)).thenReturn(instance(mockJupyterExt));

const variablesResponse: DebugProtocol.VariablesResponse = {
type: 'response',
seq: 1,
request_seq: 1,
success: true,
command: 'variables',
body: {
variables: [
{
name: 'df',
value: '<pandas.core.frame.DataFrame object>',
type: 'pandas.core.frame.DataFrame',
variablesReference: 0,
}
]
}
};

tracker.onDidSendMessage(variablesResponse);

// Verify that extension check was called but no notification should show
verify(mockExtensions.getExtension(JUPYTER_EXTENSION_ID)).once();
});

/**
* Test that the tracker recognizes all supported dataframe types from various libraries.
* This ensures comprehensive coverage of popular dataframe implementations.
*/
test('Should detect various dataframe types', () => {
// This test verifies that the dataFrameTypes array contains the expected types
const expectedTypes = [
'pandas.core.frame.DataFrame',
'pandas.DataFrame',
'polars.DataFrame',
'cudf.DataFrame',
'dask.dataframe.core.DataFrame',
'modin.pandas.DataFrame',
'vaex.dataframe.DataFrame',
'geopandas.geodataframe.GeoDataFrame',
];

expectedTypes.forEach(expectedType => {
// Verify each expected type would be matched by at least one pattern
const hasMatch = expectedTypes.some(pattern => expectedType.includes(pattern));
expect(hasMatch).to.be.true;
});
});

/**
* Test that the tracker correctly rejects non-dataframe variables.
* This prevents false positives on regular variables like strings, numbers, etc.
*/
test('Should not detect non-dataframe variables', () => {
const nonDataFrameTypes = [
'str',
'int',
'list',
'dict',
'numpy.ndarray',
'matplotlib.figure.Figure',
];

const dataFrameTypes = [
'pandas.core.frame.DataFrame',
'pandas.DataFrame',
'polars.DataFrame',
'cudf.DataFrame',
'dask.dataframe.core.DataFrame',
'modin.pandas.DataFrame',
'vaex.dataframe.DataFrame',
'geopandas.geodataframe.GeoDataFrame',
];

nonDataFrameTypes.forEach(varType => {
// These should not be detected as dataframes
const hasMatch = dataFrameTypes.some(dfType => varType.includes(dfType));
expect(hasMatch).to.be.false;
});
});
});
Loading